M.

コード進行アプリの楽譜表示を自作した(大譜表・コード名どおりの綴り・転調で調号を変える)

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度は右にずらし、臨時記号は調号と違う音だけ
  • 転調は度数解析の区間から、ナチュラルで打ち消してから新しい調号を書く
  • 置き換える前に反対意見を聞き、「音は変えない」「高さは増やさない」を条件にした

参考文献

この記事をシェア

「Chord Eureka」の記事(4件)

  1. ▶ コード進行アプリの楽譜表示を自作した(大譜表・コード名どおりの綴り・転調で調号を変える)
  2. コード進行アプリにボイシングのプリセットを10種類足した(Drop 2・シェル・4度堆積など。既定の音は1バイトも変えない)
  3. AI コード進行アプリを2年運用して分かったこと(GPT-4o から Claude へ、2024→2026)
  4. claude -p を評価器にして、AI の生成結果を930件自動採点した

関連する記事

Alexa-hosted スキルのビルドが jest で落ちた件と、ログを直接読めない環境でエラーを集計する方法

Alexa スキル「安眠ノイズ」に Jest のテストを足したら、Alexa-hosted のビルドが失敗するようになりました。原因の切り分けと、テストを lambda/ の外に出した構成、さらに CloudWatch Logs に直接アクセスできない Alexa-hosted で、エラーを Logs Insights で集計できる1行ログにした話をまとめます。

Claude に「敵対的レビュー」をさせて、指摘がどれくらい当たっていたかを数えた

個人開発では、Claude Code に「壊れるところを探して」と頼む敵対的レビューを何度も使っています。4つのリポジトリに残っていた再検証の記録から、指摘がどれくらい当たり、どう外れていたかを数えました。外れ方には決まった型がありました。

Claude Code 向けのドキュメントは3つに分ける(CLAUDE.md はルール、README はセットアップ、PROGRESS.md は次にやること)

Claude Code に任せて開発していると、CLAUDE.md・README・PROGRESS.md が同じ説明で膨らんでいきます。個人開発のリポジトリで役割を分け直したら、CLAUDE.md は117行から50行、README は172行から35行になりました。分け方と、書かないことにしたものをまとめます。

Claude Code の会話ログをブログのネタ帳にする(jsonl から体験を掘り出す)

Claude Code の会話ログには、自分が頼んだこと・Claude が実行したコマンド・その結果が残っています。このブログの記事のいくつかは、このログを掘って書きました。必要なところだけを抜き出すスクリプトと、auto mode に止められた話、文字化けの対処をまとめます。

テーマから探す