1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コード進行ジェネレータをブラウザで実装 — 音楽理論を JS で書いて Web Audio API で鳴らす

1
Posted at

ポップス、ジャズ、パッヘルベルのカノン。よく聞くコード進行は ダイアトニックスケール上の度数の組み合わせ で説明できる。本ツールは 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

Screenshot

音楽理論の最小知識

「コード進行」と聞いて構えるかもしれないが、実装に必要な部分は驚くほど狭い。

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° 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)はあとから音程を測って判定する。

コード名の自動判定(テーブルを持たない設計)

AmBm7♭5G7 のような表記はハードコードしたくない。理由: 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.jstheory.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/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?