M.

コード進行アプリにボイシングのプリセットを10種類足した(Drop 2・シェル・4度堆積など。既定の音は1バイトも変えない)

Chord Eureka は、AI がコード進行を作って、その場で鳴らせる Web アプリです。作った進行は MIDI で書き出して、DAW に取り込めます。

鳴らすときの ボイシング(和音の音をどの高さに、どう並べるか)は、ずっと1種類だけでした。これに、Drop 2 やシェルなどのプリセットを10種類足しました。実装は Claude Code に任せています。この記事では、その経緯と組み立て方をまとめます(リポジトリは非公開なので、コードは説明用に書き直したものです)。

環境:Node.js(Express)/ tonal(コード解析)/ Jest / Claude Code(Opus)/ 2026年9月

既定の音を変えて、戻した

もともとのボイシングは、ルートをオクターブ3に置いて、構成音を下から 5音 積むものです。三和音なら、オクターブ上のルートと3度を重ねて5音にします。

C     → C3 E3 G3 C4 E4
Cmaj7 → C3 E3 G3 B3 C4

これだと、C13 のように6音以上あるコードでは、上のテンション(13th)が落ちます。レビューでこれを指摘されて、一度 構成音をすべて鳴らす 形に変えました。

ところが、翌日に元に戻しています。理由は、このアプリを使ってくれている人がいるからです。

  • 聴き慣れた響きが、ある日突然変わる
  • DAW に取り込んだときの音の数が変わる
  • 「直した」つもりでも、利用者から見れば「音が変わった」

そこで方針を変えて、既定の音はそのままにして、ほかの鳴らし方を選べるようにする ことにしました。CLAUDE.md にも「既定のボイシングは、既存の利用者の音と DAW への取り込みを保つために固定。オーナーの了承なしに変えない」と書いています。

既定は1バイトも変えない

プリセットを足すと、ボイシングの処理を通る経路が変わります。そこで、既定(クラシック)を選んだときは 今までとまったく同じ MIDI ファイル が出ることを確かめました。

  • 過去に評価用に作った 930件の進行 で、追加前と追加後の MIDI のバイト列が一致することを確認
  • テストでも、「既定を指定したとき」と「何も指定しないとき」の MIDI が同じ文字列になることを確かめる
test('既定(classic・mid)は省略したときと同じ音・同じMIDI', () => {
  const options = { voicing: 'classic', register: 'mid' };
  expect(AnalyzeChord(PROGRESSION, options).dataUri()).toBe(AnalyzeChord(PROGRESSION).dataUri());
});

プリセットを選ばない限り、利用者の音は変わりません。

選べるようにしたもの

画面では、再生バーの横にボイシングと音域の選択を置きました。選んだものは端末に保存して、共有 URL にも &v=(ボイシング)と &r=(音域)として載せます。

グループプリセット中身
—クラシック(既定)オクターブ3から5音積み(今までどおり)
クローズ密集ルートを一番下にして、1オクターブ内に詰める
スムーズ前の和音から、動きが一番小さい転回形を選ぶ
ベース+上声ベースはルート、上声からはルートを抜く
オープンDrop 2 / Drop 3 / Drop 2&4密集配置の上から2番目(3番目、2・4番目)の音を1オクターブ下げる
スプレッド左手はルートと7度(なければ5度)、右手は残りの音
ジャズシェルルートと5度を省き、3度・7度・テンションで弾く
ルートレスルートを抜いた構成音を、前の和音から近い形で置く
4度堆積音の間隔ができるだけ完全4度になる順に積む

音域は「低め・標準・高め」の3つで、全体を1オクターブずつ上下させます。

クラシック以外のプリセットは、どれも ベースをオクターブ2のルート(スラッシュコードなら指定した音)に置き、その上に上声を積みます。

最初は「スムーズ・オープン・ベース+上声」の3つだけでした。試聴してから「もっと種類がほしい」と頼んで、10分後のコミットで今の10種類に増えています。

決まり:書かれていない音は足さない

ジャズのボイシングでは、maj7 に 9th を足すように、コード名にない音を加えることがよくあります。でも、このアプリではそれをしないことにしました。

  • 足さない:コード名に書かれていない音は鳴らさない
  • 削らない:テンションは落とさない。Em7b9 を Em7 として鳴らすと、別のコードになってしまう
  • 省いてよいのは2つだけ:ルートと完全5度。どちらも響きを大きく変えずに省ける音です。6音以上のコードでは、完全5度を省きます

クラシックは5音までなので、C13 の 13th が落ちます。プリセットではこの決まりのおかげで、13th まで鳴ります。

この決まりは、テストで全プリセットに対して確かめています。

test.each(PRESETS)('%s: 音は足さず、省くのはルート・完全5度だけ', (voicing) => {
  // 構成音にない音が鳴っていないか、省いてはいけない音が欠けていないかを、コードごとに確かめる
});

ほかにも、どのプリセットでも鳴る和音の数が同じか、ベースが一番低い音になっているか、画面の選択肢とサーバーのプリセット一覧が一致しているかをテストしています。

組み立て方

Drop 系

Drop 2 は、密集配置の 上から2番目の音を1オクターブ下げる ボイシングです。ギターやピアノでよく使われる、広がりのある響きになります。

// 説明用。上から数えて positions 番目(1始まり)の音を1オクターブ下げる
function drop(stacked, positions) {
  return stacked
    .map((n, i) => (positions.includes(stacked.length - i) ? n - 12 : n))
    .sort((a, b) => a - b);
}

// Cmaj7 の密集配置 C4 E4 G4 B4 → 上から2番目の G4 を下げる → G3 C4 E4 B4

Drop 系は4声が前提なので、三和音はルートをオクターブ上に重ねて4声にしてから下げます。

スムーズ(最小移動)

和音の 転回形をすべて作り、前の和音から一番動きが小さいもの を選びます。動きの大きさは、各音から相手の和音の一番近い音までの距離を、両方向に足したものです。

// 説明用。from の各音から to の一番近い音までの距離と、その逆を足す
function movement(from, to) {
  const nearest = (notes, note) => Math.min(...notes.map((n) => Math.abs(n - note)));
  return to.reduce((sum, n) => sum + nearest(from, n), 0)
       + from.reduce((sum, n) => sum + nearest(to, n), 0);
}

最初の和音には前がないので、上声の平均が D4 に近い形を選びます。テストでは、Cmaj7 → Am7 で共通音が3つ以上そのまま残ることを確かめています。

シェルとルートレスも、省く音を決めたあとは同じ選び方をしています。

4度堆積

構成音(4音以上ならルートを抜く)を、隣り合う音の間隔ができるだけ完全4度(半音5つ)になる順 に並べます。どの音から始めるかを全部試して、完全4度からのずれが一番小さい並びを選びます。

// 説明用。start から始めて、次の音は「上に半音5つ」に一番近いものを選んでいく
for (const start of pitchClasses) {
  const order = [start];
  let cost = 0;
  while (order.length < pitchClasses.length) {
    const up = (pc) => (pc - order.at(-1) + 12) % 12;
    const next = remaining(order).reduce((a, b) => (Math.abs(up(b) - 5) < Math.abs(up(a) - 5) ? b : a));
    cost += Math.abs(up(next) - 5);
    order.push(next);
  }
  // cost が一番小さい order を使う
}

Dm7 なら、ルートを抜いた F・A・C を C → F → A の順に積みます。C から F は完全4度、F から A は長3度で、ずれは合計1です。完全4度だけで積めないコードでも、一番近い並びになります。

スプレッド(両手)

左手はベースのルートと、7度(なければ5度)。右手は残りの音を C4 より上に詰めます。

左手の2音目は、低すぎると濁ります。C3 より下になったら1オクターブ上げるようにしました。

試聴して直したところ

実装が終わった時点で、Claude は気になりそうな点を PROGRESS.md に書き残していました。

  • オープンとベース+上声は、ルートが G〜G# のときに上声が高めになる(上声の一番下を A3〜G#4 に置くため)
  • スムーズは、平行移動が続くと低い音域で密集して響きが濁ることがある

音の置き場所は voicing.js の先頭の定数(上声の一番下の範囲など)で調整できるようにしてあります。試聴して、このままで良しとしました。

試聴のあと、上の「足さない」決まりを PROGRESS.md の決定事項に移しました。ジャズ流儀のテンションを足すボイシングがほしくなったら、別のプリセットとして足すことにしています。

結果

  • プリセットは10種類 + 音域3段階。最初のコミットで353行、拡充のコミットで voicing.js は188行になりました
  • 既定の音は、930件の進行で MIDI が1バイトも変わらないことを確かめた
  • どのプリセットでも、鳴る和音の数とベースの位置は同じ。音は足さず、省くのはルートと完全5度だけ

楽譜の表示も、この直後に自作に置き換えています。どのボイシングを選んでも、楽譜の音と鳴る音が一致するようにしました(楽譜表示を自作した話)。

まとめ

  • 既定の音を変えると、利用者には「音が変わった」としか見えない。変えずに、選べるようにした
  • 既定を選んだときの MIDI がバイト単位で同じことを、930件の進行とテストで確かめた
  • 「書かれていない音は足さない。省くのはルートと完全5度だけ」という決まりを、全プリセットのテストで守らせた
  • Drop 系は「上から n 番目を1オクターブ下げる」、スムーズは「動きの合計が最小の転回形」、4度堆積は「間隔が完全4度に近い並び」で組み立てた

参考文献

この記事をシェア

「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 に止められた話、文字化けの対処をまとめます。

テーマから探す