コード進行アプリの楽譜表示を自作した(大譜表・コード名どおりの綴り・転調で調号を変える)
- Chord Eureka
- JavaScript
- SVG
- 音楽理論
- Claude Code
Chord Eureka は、AI がコード進行を作って、その場で鳴らせる Web アプリです。結果画面には、ピアノロールと楽譜(Score View)が出ます。
この楽譜は、もともと html-midi-player に付いている楽譜表示(中身は Magenta の staffrender)をそのまま使っていました。手軽に出せる反面、音楽的におかしな表示がいくつかありました。そこで、楽譜の部分だけを自作に置き換えました。
実装は Claude Code に任せています。この記事では、何が問題で、どう直したかをまとめます(リポジトリは非公開なので、コードは説明用に書き直したものです)。
環境:Node.js(Express)/ 素の JavaScript / tonal(コード解析)/ Claude Code(Opus)
何がおかしかったか
本番のスクリーンショットを Claude に見せて、問題を洗い出してもらいました。
| 問題 | 原因 |
|---|---|
| 進行によって、ト音記号になったりヘ音記号になったりする | 1段の五線に、音域から音部記号を自動で選んでいた |
| E♭m の進行が、D# のようにシャープで書かれる | MIDI には音の高さしかなく、綴り(E♭か D#か)の情報がない |
| 調号がない | 同上。MIDI から調は分からない |
| 220px の枠のうち、五線は約60px しか使っていない | 1段譜で、加線が多い |
| ダークモードで色がおかしい | filter: invert で色を反転していて、再生中の赤がシアン寄りになる |
2番目と3番目は、MIDI から楽譜を描いている限り直せません。MIDI ファイルに入っているのは「63番の音を鳴らす」という高さだけで、それを E♭ と書くか D# と書くかは分からないからです。
置き換える前に、反対側から考えさせた
いきなり作り始めたわけではありません。まず「MIDI プレイヤーごと置き換えるべきか」を Claude に調べてもらい、そのあとで「あえて反対側から、置き換える価値があるかを考えて」と頼みました(敵対的レビューと呼んでいるやり方です)。
返ってきた指摘はこうです。
- 利用者から見て、今の表示に不具合報告はない
- 楽譜を誰が見ているかのデータがない。初心者は五線を読めないし、上級者は MIDI を DAW に持っていく
- 大譜表(ト音記号とヘ音記号の2段)にすると高さが増えて、スマホで画面がさらに縦に長くなる
結論は「プレイヤー本体は置き換えない。楽譜の自作は見た目が変わるので、作るかどうかはオーナー(自分)が決める」でした。
楽譜は自分でも見るので、自作することにしました。そのかわり、再生とピアノロールは今の html-midi-player のままにして、楽譜の高さは今の枠に収めることを条件にしました。音は既存の利用者が一番敏感なところなので、触らないことにしています。
自作しやすい条件がそろっていた
楽譜の自作というと大変そうですが、このアプリでは入力がとても単純でした。
- どの小節も 4/4 拍子で、全音符の和音が1つだけ
- 小節数は 4・8・12 のどれか
- どの音を鳴らすかは、サーバーのボイシング処理がすでに決めている
リズムも連桁も符尾もありません。VexFlow や abcjs のような本格的な楽譜ライブラリも候補に挙がりましたが、gzip で 138〜304KB あり、全音符を並べるだけには重すぎます。SVG を直接組み立てることにしました。
綴りは、コード名からサーバーで決める
一番大事なのは、E♭ と書くか D# と書くかです。これは MIDI ではなく、コード名 から決めます。
E♭m7 の構成音は E♭・G♭・B♭・D♭ です。サーバーではすでに、コード名を解析してボイシング(実際に鳴らす音の高さ)を決めています。そこで、鳴らす音の高さはそのままに、コード名から出した綴りの候補と、半音の位置が一致するもの を選ぶようにしました。
// 説明用。コードの構成音(綴り)から、鳴る高さ midi に合うものを選ぶ
function spell(midi, pitchClasses) {
// pitchClasses: E♭m7 なら ["Eb", "Gb", "Bb", "Db"]
const pc = pitchClasses.find((p) => chroma(p) === midi % 12);
return pc ? withOctave(pc, midi) : null;
}
オクターブの境目には注意が要りました。C♭4 は B3 と同じ高さ(MIDI 59)なので、「高さからオクターブを計算して、綴りを付ける」と1オクターブずれます。綴りに合わせてオクターブを直す処理を入れています。
サーバーは MIDI と一緒に、和音ごとの音名の配列(["Eb3", "Gb3", "Bb3", "Db4"] のような形)を返します。フロントはこれを描くだけです。
大譜表の組み立て
ト音記号とヘ音記号の振り分け
中央ハ(C4)以上はト音記号、未満はヘ音記号 に固定しました。音域から音部記号を選ばないので、進行によって記号が変わることはなくなります。
音の縦位置
楽譜の縦位置は、半音ではなく 音名(CDEFGAB)の段 で決まります。C4 と C#4 は同じ位置で、臨時記号が付くかどうかだけが違います。そこで、音名を「C0 から数えて何段目か」に直して、そこから y 座標を出します。
// 説明用。"Eb3" → 段数(C0 を 0 として、線と間を1段ずつ数える)
const LETTERS = "CDEFGAB";
const step = LETTERS.indexOf(letter) + 7 * octave; // Eb3 なら 2 + 21 = 23
const y = staffMiddleY - (step - staffMiddleStep) * (SPACE / 2);
五線の外に出た音には、段数を2つずつたどって加線を引きます。
2度でぶつかる音符をずらす
コードには、隣り合った音(2度)が入ることがあります。たとえば add9 や sus2 です。同じ x 座標に描くと符頭が重なるので、下から見て、1つ下の音と1段しか離れていない音は右にずらします。ずらした音のすぐ上の音は、元の位置に戻します。
臨時記号の列
臨時記号は、調号と違う音にだけ 付けます。E♭メジャーの調で E♭ が出てきても、♭は書きません。
臨時記号が縦に近いと重なるので、上の音から順に、近くの記号とぶつからない列を探して左に並べます。ダブルフラットは幅が2列分あるので、そのぶん列を取ります。
調号と転調
調号は、アプリがすでに持っている 度数解析 の結果から付けます。度数解析は、進行のキーを推定して、各コードを「Ⅳ△7」のような度数で表示する機能です。転調があると、区間ごとにキーが分かれています。
- 最初の調号は、最初の区間のキーから付ける
- キーが変わる小節の前に複縦線を引き、新しい調号を書く
- シャープの調からフラットの調に変わるときは、消える調号をナチュラルで打ち消してから 新しい調号を書く
度数解析の区間の始まりは「何番目のコード名か」で持っているので、そこから「何小節目か」に直す処理が要りました。コード名として読めなかったトークンは鳴らさないので、トークンの番号と小節の番号はずれることがあるからです。
再生と連動させる
各小節は、MIDI の1小節と1対1で対応します。小節ごとに data-midi-index を持たせ、プレイヤーの note イベントを受けて、再生中の小節の背景を強調します。
小節をタップすると、その小節から再生します。キーボードでも操作できるように、各小節に tabindex と role="button"、読み上げ用に「3小節目 Am7 から再生」のような aria-label を付けました。
細かいところ
- 記号の形は staffrender から流用。ト音記号・ヘ音記号・♯・♭・♮・全音符の SVG パスは、staffrender(Apache-2.0)のものを使っています。ダブルシャープだけは staffrender になかったので作りました
- ダークモードはテーマの色で描く。反転フィルタをやめて、CSS 変数で線と音符の色を指定しました。再生中の強調色も正しく出ます
innerHTMLは使わない。SVG の要素はcreateElementNSで1つずつ作っています
結果
| 置き換え前 | 置き換え後 | |
|---|---|---|
| 五線 | 1段(記号が進行で変わる) | 大譜表(ト音・ヘ音で固定) |
| 綴り | MIDI の高さから自動(E♭m が D# で出る) | コード名どおり |
| 調号 | なし | あり(転調にも対応) |
| ダークモード | 色を反転 | テーマの色 |
| タップ | — | その小節から再生 |
変更は14ファイル・約440行で、楽譜の描画そのもの(scoreView.js)は230行ほどでした。テストは8件足しました。綴り(E♭m7 が E♭・G♭・B♭・D♭、C♭7 の7度が B𝄫 になるか)、調号と違う音だけに臨時記号が付くか、2度のずらし、転調の小節の割り当てなどです。どのボイシングでも、楽譜の音と実際に鳴る音が同じ高さ・同じ小節数になるか も確かめています。楽譜と音がずれるのが一番困るからです。
楽譜ライブラリを入れるより、入力の単純さに合わせて自作したほうが軽く、直したいところも直せました。全音符しか出てこないアプリならではの選択です。
まとめ
- MIDI から描く楽譜は、綴りと調号が分からない。綴りはコード名から決めてサーバーで返す
- 入力が「全音符の和音が1小節に1つ」だけなので、楽譜ライブラリを使わず SVG で組み立てた
- 大譜表は C4 で振り分けて固定。縦位置は音名の段で、2度は右にずらし、臨時記号は調号と違う音だけ
- 転調は度数解析の区間から、ナチュラルで打ち消してから新しい調号を書く
- 置き換える前に反対意見を聞き、「音は変えない」「高さは増やさない」を条件にした
参考文献
この記事をシェア