ポップス、ジャズ、パッヘルベルのカノン。よく聞くコード進行は ダイアトニックスケール上の度数の組み合わせ で説明できる。本ツールは 12 キー × 2 スケール × 7 種類のジャンルプリセットでコード進行を生成し、Web Audio API でその場で鳴らす。300 行ちょっとの Vanilla JS、依存ライブラリゼロ。
🌐 Demo: https://sen.ltd/portfolio/chord-progression-gen/
📦 GitHub: https://github.com/sen-ltd/chord-progression-gen
音楽理論の最小知識
「コード進行」と聞いて構えるかもしれないが、実装に必要な部分は驚くほど狭い。
1. スケール = 7 つの音の集まり
C メジャースケールは「C, D, E, F, G, A, B」の 7 音。半音単位(セミトーン)で書くと [0, 2, 4, 5, 7, 9, 11]。これだけ覚えれば十分。
const MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
const NATURAL_MINOR = [0, 2, 3, 5, 7, 8, 10];
ナチュラルマイナーは「C, D, E♭, F, G, A♭, B♭」で [0, 2, 3, 5, 7, 8, 10]。
2. コード = スケールの 1, 3, 5 度を重ねる(ダイアトニックトライアド)
スケールの 1 番目、3 番目、5 番目を取ると三和音(トライアド)になる。C メジャーなら C・E・G で C メジャートライアド。同じことをスケールの 2 番目(D)から始めると D・F・A で D マイナー。
何度目から始めるかを「度数(ディグリー)」と呼び、ローマ数字で表記する:
| 度数 | C メジャー | 品質 |
|---|---|---|
| I | C | Major |
| ii | Dm | minor |
| iii | Em | minor |
| IV | F | Major |
| V | G | Major |
| vi | Am | minor |
| vii° | B° | diminished |
大文字 = メジャー、小文字 = マイナー、° = ディミニッシュ。ここから先のコード進行はすべてこの 7 つの組み合わせでしかない。
3. 進行 = 度数の並び
- Pop: I → V → vi → IV(C メジャーなら C → G → Am → F)
- Pachelbel のカノン: I → V → vi → iii → IV → I → IV → V
- ジャズの ii-V-I: ii → V → I → vi
コード構築のロジック
度数 d(0–6)からトライアドを作る関数:
export function chordTones(tonic, scaleName, degree, seventh = false) {
const scale = SCALES[scaleName];
const positions = seventh ? [0, 2, 4, 6] : [0, 2, 4];
return positions.map((p) => {
const idx = degree + p;
const octaveShift = Math.floor(idx / 7) * 12;
return tonic + octaveShift + scale.intervals[idx % 7];
});
}
tonic はトニック(主音)の MIDI 番号(C4 = 60)。degree + p がスケールの 7 音を超えたらオクターブをまたぐので Math.floor(idx / 7) * 12 で底上げする。Cメジャーで vi (Am) を作ると chordTones(60, "major", 5) → [69, 72, 76] (A4, C5, E5)。
7th コードは 1, 3, 5, 7 度なので positions を [0, 2, 4, 6] にするだけ。コードの種類(maj7 / 7 / m7 / m7♭5)はあとから音程を測って判定する。
コード名の自動判定(テーブルを持たない設計)
Am、Bm7♭5、G7 のような表記はハードコードしたくない。理由: 12 キー × 2 スケール × 4 種類の品質を表で持つと冗長で、転調や応用が効かない。
代わりに 構築したトライアドの音程関係から名前を逆算する:
export function chordName(tonic, scaleName, degree, seventh = false) {
const tones = chordTones(tonic, scaleName, degree, seventh);
const rootPc = ((tones[0] % 12) + 12) % 12;
const third = tones[1] - tones[0];
const fifth = tones[2] - tones[0];
let quality = "";
if (third === 4 && fifth === 7) quality = ""; // Major
else if (third === 3 && fifth === 7) quality = "m"; // minor
else if (third === 3 && fifth === 6) quality = "°"; // dim
else if (third === 4 && fifth === 8) quality = "+"; // aug
// ... 7th 部分省略
return NOTE_NAMES[rootPc] + quality;
}
長 3 度 + 完全 5 度(4 + 7 半音)ならメジャー。短 3 度 + 完全 5 度(3 + 7)ならマイナー。短 3 度 + 減 5 度(3 + 6)ならディミニッシュ。音程の事実から名前が決まる 設計だと、どんなスケールを追加してもコード名は自動で正しく出る(ハーモニックマイナー、ドリアン、フリジアンを足す将来計画もある)。
Web Audio で鳴らす
純粋に音を出すだけなら 30 行で書ける。
import { midiToHz } from "./theory.js";
class ChordPlayer {
constructor() {
this.ctx = null;
}
ensureContext() {
if (this.ctx) return;
this.ctx = new AudioContext();
this.master = this.ctx.createGain();
this.master.gain.value = 0.18;
const filter = this.ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.value = 2400;
this.master.connect(filter);
filter.connect(this.ctx.destination);
}
scheduleChord(tones, startTime, duration) {
for (const midi of tones) {
const osc = this.ctx.createOscillator();
osc.type = "triangle";
osc.frequency.value = midiToHz(midi);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0, startTime);
gain.gain.linearRampToValueAtTime(1 / tones.length, startTime + 0.02);
gain.gain.linearRampToValueAtTime(0, startTime + duration);
osc.connect(gain);
gain.connect(this.master);
osc.start(startTime);
osc.stop(startTime + duration + 0.05);
}
}
}
ポイントが 3 つ:
1. オシレータは triangle 波
sawtooth は派手すぎ、sine は薄すぎ、square はチープ。triangle がアコースティックっぽい中庸な響き。
2. ADSR は最小構成で十分
ピアノやストリングスを再現する必要がないなら、Attack 20ms、Release で音を切るだけで「自然に聞こえる」レベルになる:
volume
1.0 │ /‾‾‾‾‾‾‾‾‾‾‾\
│ / \
0.0 │_/ \____
├──┼──────────────┼─┼──→ time
20ms release start
3. ゲインを 1 / tones.length で割る
3 音や 4 音を同時に鳴らすとき、足し算でクリッピングしないよう各オシレータを音数で割る。これだけでミックスバランスが破綻しない。
4. ブラウザの autoplay policy に対応する
Chrome は ユーザー操作なしに AudioContext が音を出すのを禁じている。Play ボタンクリック時に await ctx.resume() を呼ばないと最初のクリック分が無音で終わる:
async resume() {
this.ensureContext();
if (this.ctx.state === "suspended") await this.ctx.resume();
}
これを忘れると「ローカルだと鳴るのに本番だと鳴らない」というデバッグ地獄が始まる。
アーキテクチャ
theory.js ← 音楽理論ロジック(純粋、DOM/Audio 依存ゼロ)
audio.js ← Web Audio スケジューリング(theory.js から MIDI を受け取る)
app.js ← UI グルー(DOM イベント → theory → audio)
theory.js には DOM も AudioContext も登場しない。だから Node の組み込みテストランナーで 25 件のテストを通せる:
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildProgression, tonicMidi, PRESETS } from "../theory.js";
test("Pop in C major yields C G Am F", () => {
const out = buildProgression({
tonic: tonicMidi("C", 4),
scaleName: "major",
degrees: PRESETS.pop.degrees,
});
assert.deepEqual(out.map((c) => c.name), ["C", "G", "Am", "F"]);
});
audio.js は theory.js の 出力(MIDI 番号の配列) だけに依存する。逆向きの依存はない。「ロジックの正しさ」と「音の鳴らし方」をテストの粒度として分離できるので、和声規則を後から追加しても回帰が起きにくい。
app.js は両者の薄いグルー — UI イベントで state を書き換え → theory.buildProgression() で再構築 → 必要なら audio.play()。state は単純なオブジェクトで、React や Vue のような大袈裟な仕組みは入れない。
まとめ
- 音楽理論で「コード進行」と呼ばれるものの 実装に必要な知識は驚くほど狭い(7 音 × 度数 × ローマ数字)
- コード名はテーブルではなく 音程関係からの逆算 で出すと拡張に強い
- Web Audio API は triangle 波 + 最小 ADSR + 自動 autoplay 対応の 3 点を押さえれば動く
- 音楽理論ロジックと Audio スケジューリングを分離すると、Node でロジックだけテストできる
リポジトリ: https://github.com/sen-ltd/chord-progression-gen
このツールは弊社の OSS ポートフォリオ #240 として作成しました。SEN 合同会社(東京)では小さくて切れ味のあるツール群を継続的に公開しています: https://sen.ltd/portfolio/
