🎵

wasmで使えるコード進行パーサーを作りました

2023/11/28に公開

NOTE: code進行でなくchord進行です。開発時に結構typoしました

紹介(chord-progression-parser)

Rustでコード進行の文字列をそれっぽいAST(?)に変換にして返してくれるchord-progression-parserというものを作ってみました。ライセンスはMITにしてみているのでもし用途があれば使ってもらえればという感じです。

https://github.com/lainNao/chord-progression-parser

Rust用のパッケージをcrates.ioから、JavaScript(wasm)用のパッケージをnpmからリリースしています。

Rust: https://crates.io/crates/chord-progression-parser
JS/TS(bundler): https://www.npmjs.com/package/@lainnao/chord-progression-parser-bundler
JS/TS(server): https://www.npmjs.com/package/@lainnao/chord-progression-parser-server
JS(CDN): https://www.npmjs.com/package/@lainnao/chord-progression-parser-web


動作的には、例えば以下のような形式のコード進行の文字列があるとして、

C - Dm - Em - F
G - Am - Bm(o) - C

[key=Gm]F(9,13) - Fm(6) - Bb(add9,13) - Bbaug
F#m(b5,7),Gm(M9)/E - Bm(7,9) - E(M13) - Edim

このライブラリでパースにかけたら

parseChordProgressionString(↑の文字列)

以下のようなjsonを返します。

{
  success: true,
  ast: [
    {
      chordBlocks: [
        [
          {
            chordExpression: {
              type: 'chord',
              value: {
                detailed: {
                  accidental: null,
                  base: 'C',
                  chordType: 'M',
                  extensions: []
                },
                plain: 'C'
              }
            },
            denominator: null,
            metaInfos: []
          }
        ],
        ...(かなり長いので先頭コード以外は省略)

出力するASTの定義について

AST(?)の定義の説明を書いてみます。特にプログラミング技術的には真新しいことをしていないのでカテゴリーをIdeaにしているレベルなのであれなのですが、コード進行を知っている方には読んでもらえると理解してもらえる気がします。

parseChordProgressionString関数の戻り値でもらえるASTの定義は現状以下のようにしています。

type Ast = Section[];

まずAST自体は上述の通りセクションの配列にしています。そのセクションは「Aメロ」「Bメロ」「サビ」とかを指します。

interface Section {
    metaInfos: SectionMeta[];
    chordBlocks: ChordBlock[];
}

セクションはメタ情報の配列と、コード進行の配列を持っています。
メタ情報は例えば「セクションはAメロ」とか「記載したコード進行を2回繰り返す」とかのセクション全体に対するメタ情報です。現状はその2種類の情報のみ定めています。

type SectionMeta = 
    | { type: "section", value: string }
    | { type: "repeat", value: number };

以下はコード進行の部分です。

export type ChordBlock = ChordInfo[];

これはさらにコード進行の配列になっています。「コード進行は「コード進行の配列」の配列です」みたいな説明になってしまっていますが実際そのような型にしています。そのように二次元配列にした理由は、以下のようにハイフン区切りの1ブロックの中にカンマ区切りの複数コードを書くことも許したいためです。ハイフン区切りが外側の配列、カンマ区切りが内側の配列です。

C,Dm - Em,F - G,G7 - C

そして一つのコードの型は以下のようにしています。メタ情報の配列と、コード情報と、あと(分数コード等の)分母部分を詰め込んでいるのをコードとしています。

interface ChordInfo {
    metaInfos: ChordInfoMeta[];
    chordExpression: ChordExpression;
    denominator?: string;
}

メタ情報は以下のようにしています。

type ChordInfoMeta = 
    | { type: "key", value: Key };
    
enum Key {
    Cb_M = "Cb",
    Cb_m = "Cbm",
    C_M = "C",
    C_m = "Cm",
    Cs_M = "C#",
    Cs_m = "C#m",
    Db_M = "Db",
    Db_m = "Dbm",
    D_M = "D",
    D_m = "Dm",
    Ds_M = "D#",
    Ds_m = "D#m",
    Eb_M = "Eb",
    Eb_m = "Ebm",
    E_M = "E",
    E_m = "Em",
    Fb_M = "E#",
    Fb_m = "E#m",
    F_M = "F",
    F_m = "Fm",
    Fs_M = "F#",
    Fs_m = "F#m",
    Gb_M = "Gb",
    Gb_m = "Gbm",
    G_M = "G",
    G_m = "Gm",
    Gs_M = "G#",
    Gs_m = "G#m",
    Ab_M = "Ab",
    Ab_m = "Abm",
    A_M = "A",
    A_m = "Am",
    As_M = "A#",
    As_m = "A#m",
    Bb_M = "Bb",
    Bb_m = "Bbm",
    B_M = "B",
    B_m = "Bm",
    UnIdentified = "?",
}

これは「コード進行するたびにキーが変わりうるので、そこにキーの変更を差し込めるようにしたい」という理由でキーをコードの横に書けるようにしています。

ChordExpressionは以下のタグつきユニオンにしています。

type ChordExpression = 
    | { type: "chord", value: Chord }
    | { type: "unIdentified", value?: undefined }
    | { type: "noChord", value?: undefined }
    | { type: "same", value?: undefined };

// 型はtypeshareというライブラリで自動生成してるのでまだちょっと無駄がありますが…

わからない場合は?と書けばunIdentifiedに、コードが無い場合は_と書けばnoChordに、前のコードと同じ場合は%と書けばsameにパースされます。普通のchordの場合は以下のオブジェクトにパースされます。

interface Chord {
    plain: string;
    detailed: ChordDetailed;
}

interface ChordDetailed {
    base: Base;
    accidental?: Accidental;
    chordType: ChordType;
    extensions: Extension[];
}

enum Base {
    A = "A",
    B = "B",
    C = "C",
    D = "D",
    E = "E",
    F = "F",
    G = "G",
}

enum Accidental {
    Sharp = "#",
    Flat = "b",
}

enum ChordType {
    Minor = "m",
    Major = "M",
    Augmented = "aug",
    Diminished = "dim",
}

enum Extension {
    Two = "2",
    Three = "3",
    FlatThree = "b3",
    Four = "4",
    FlatFive = "b5",
    Five = "5",
    SharpFive = "#5",
    FlatSix = "b6",
    Six = "6",
    Seven = "7",
    FlatNine = "b9",
    Nine = "9",
    SharpNine = "#9",
    FlatEleven = "b11",
    Eleven = "11",
    SharpEleven = "#11",
    FlatThirteen = "b13",
    Thirteen = "13",
    SharpThirteen = "#13",
    MajorSeven = "M7",
    MajorNine = "M9",
    MajorEleven = "M11",
    MajorThirteen = "M13",
    Add9 = "add9",
    Add11 = "add11",
    Add13 = "add13",
    Sus2 = "sus2",
    Sus4 = "sus4",
    HalfDiminish = "o",
}

大体このような型に、上記のような文字列で書けば変換されます。

仕様面で考える必要があったところ

Cに7thをつけたものの表記を「C7」とせず「C(7)」でパースできるようにしている点

C7だけ考えればC7でよかったんですが、Cm7-5C(b9)を見比べると「どういう法則性でカッコがあったりなかったりするんだい。もしかして法則性って無かったりするのかい、文化的な流れで決まっているだけなのかい」のようになってしまいました。たぶんそうなのかもと思ったので、「テンション的なやつは全部カッコの中にカンマ区切りで入れる」というシンプルな形にしてしまいました。

なので

  • C(7)
  • Cm(7,b5)

のようになりますし、テンション(的なやつ)がものすごく多いなら

  • C(3,5,7,9,b9,11,13,b13)

のように書きます。

たしかにC7と書きたいけれどもまあしょうがないかなという感じです。

D#とEbの違いをどう考えるか

実質同じですよね?(調性によって使うの変える人がいる程度?)
パーサー作る上ではどっちも存在していていいと思うのですが、このパーサーを使うプログラム側でどう扱うべきか若干ですが工夫が強いられる気がします。例えば検索用のテーブルでは必ずD#をEbに変換して状態で保存しておいて…のような…。そういうのはどうにもならない気がしたのでスルーしています

-5(ハイフン5)が少し面倒

ハイフンはコード同士を分ける記号に使ってるので、Cm7-5のようなコードが出てきたら「そのハイフンはフラットの意味?それともコードを分けるやつの意味?」となりパースの難易度が少し上がります。また、-5って同じ意味のb5があるので、同じオブジェクトにパースしないといけない気がします。
そこまで難しい話でもないはずですが若干面倒だったので後に対応としました。

コードの区切りを|にするか-にするか

コード進行を分ける記号を最初は|にしていたのですが、できあがったものの所感でやっぱりハイフンにしたいなと心変わりして一気にハイフンに書き換えました。縦棒だと| |のように空白を入れることができるというよさはあるのですが、それよりもハイフンの一般性を取りました。

※例えばハイフンだとC-F-G-ここの「ここ」の部分を空白にしたい時に空白にしたらパーサー的にはそこが何なのか分からなくなってしまいます。ハイフンで行く場合は諦め、別の記号で空白を表すようにしました。よくわからない場合は不明を示す?を使って、本当にコードが無い小節ならば_(アンダースコア、アンダーバー)を使ってください。N.C.という記号もあったりしますが、長すぎて悪目立ちするので_にしています。よりよい形ありますか…

開発面のあれこれ

  • Rust何も使ったことなかった&パーサーも作ったことも読んだこともなかったので、ASTの構造だけ先に決めて、あとの内部実装はChatGPTに聞きまくる戦法でなんとかやった。そんな感じなのでRustで語れることは特にない状態です。
  • wasm化もwasm-packを使えばできました。ただ気になるのは、もしかしてpanicを止められずJS側でエラーとなってしまう…?
  • typeshareはRustのenumからTSの型を自動出力するのに使用中。なるべく型の二重管理はしたくなかった。
  • 全体的に自動テストかなり盛り込んだので、ほぼ動作確認せずにコミット&プッシュできるようになってよかった
  • 同じリポジトリ内でRust(wasm開発用)とBun(ツールとe2eテスト用)を使っているので多少カオスになった
  • Cargo.tomlのバージョンの値を変えたら自動でnpm、cargo.io、GitHubにリリースされるようにしてみた。そこらへんのライブラリとかは前回使ってあまり体験よくなかったので使わなかった
  • _toolsフォルダ配下にランダムなコード進行のジェネレータを作ってみた。これは後々作れたらと思っているコード進行系のサービスのベンチマークにも使いたい
  • 一旦スケール(ドリアンなどの云々)は置いといた。大抵メジャーマイナーで語れるはずなので後で必要に応じて追加したい
  • パーサーなので、パースエラーが起きた行数、列番号などもエラーメッセージで返す必要がありますが、それの必要性に気づいたのが大体できてきた後だったので大変だった(対応で一気にコードがカオスになった)
  • エラーコードの管理はTSの型で重複が起こらないようにした。具体的には https://github.com/lainNao/chord-progression-parser/blob/main/resources/error_code_message_map.ts で定義し、そこからRustの型を出力するといういいのか悪いのか分からないことをしています。

あとがき

実際試すにはこちらのCodeSandboxを開いてもらえれば試せます。

このパーサー自体はコード進行をシステム内で扱う時はこういう感じでどうですかという軽い提案的な意味もあるので、一応コミットメッセージとかは全部英語でやっています。その結果コミットメッセージがdocschoreばかりになっています。

また、このパーサーは後に作ろうと考えているコード進行検索サイト内で使おうと考えています。作り終わるのに時間がかかりそうなので一旦このパーサーの存在だけでも見てもらえればと思い投稿してみました。

※追記:一応この記事を書いた後も若干バージョンアップしてます

Discussion