HN 日本語サマリー

← 一覧へ戻る
プログラミング

プログラマーのための音楽理論

Music Theory for Programmers (runjs.app)

350 pointsby birdculture220 コメント

要約

この記事は、プログラマーが音楽理論を理解するための入門ガイドです。音の物理的な性質から始め、Web Audio APIを使ったコーディングを通じて、周波数、エンベロープ、倍音、そして音階やコードといった音楽の基本概念を、直感的に理解できるように解説しています。楽器の演奏経験がなくても、コードを書きながら音楽の仕組みを解き明かしていきます。

全文翻訳

プログラマーのための音楽理論

私は楽器を弾けません。何度か試しましたが、その都度、なぜそれが正しい音なのかを理解することなく、おおよそ正しい音を出すことができる程度で終わってしまいました。

問題は練習不足ではありませんでした。音楽理論の説明が、物事がどのように、そしてなぜそのようになっているのかという根本的な理由をすべて省略しているように思えたのです。ここに五線譜があります。ここに音符があります。これは長音階です、パターンを覚えてください。なぜあの音符なのですか? なぜあのパターンなのですか? それは慣習だからです。

物理学と算術から本質的に出てくるシステムを教えるには、奇妙な方法です。特定の音が一緒に良く聞こえる理由があります。音階、インターバル、コードの背後には数学的なパターンがあります。そして、それらのパターンを数値に変換すれば、計算できます。

そこで、最初からやり直して音楽を第一原理から学びたいと思い、コーディングを始めることでその旅を始めました。

この記事はその結果です。単一の数値が時間とともに変化することから始まり、それに従えば、12の音符を導き出し、配列から音階とコードを構築し、実際の音楽のように聞こえるコード進行を作成できます。楽器は不要で、何も信じる必要はありません。記譜法は最後に登場しますが、それは記譜法の対象となるものができてから、ようやくです。

音とは時間とともに変化する数値である

音は単なる空気圧の揺らぎです。スピーカーはコーンを前後に動かすことで音を出し、コンピューターがオーディオで行うことはすべて、毎秒4万4千回、そのコーンがどこにあるべきかを示す数値のリストに集約されます。

オーディオファイルはそのリストを書き出したものです。シンセサイザーはリストをその場で作成し、ブラウザは形状を指定すればその部分を処理してくれます。最も単純な形状はサイン波なので、ここでは毎秒440回繰り返されるサイン波を示します。

440Hzのサイン波

const osc = ctx.createOscillator();
osc.frequency.value = 440;
osc.connect(out);
osc.start();
osc.stop(ctx.currentTime + 1);

ctxとoutはブラウザのものではなく私のものです。それ以外はすべて、そのままのWeb Audio APIです。このスニペットをどこかで実行するには、次のように始めます。

const ctx = new AudioContext();
const out = ctx.destination;

そこにある唯一音楽的な意味を持つ数値は440ですが、それすらも任意です。「A」と呼ぶことに誰かが合意した周波数であり、システム全体がそれに固定されているチューニングフォークです。これを300に変えてもう一度実行してみてください。それでも機能しますが、単に異なるピッチで再生されるだけです。オシレーターは音符が何であるかを知りません。毎秒何回スイングするかだけを知っています。

周波数はピッチです:数値が高いほど、音は高くなります。音楽の他のことは、これほど単純ではありません。

なぜその音がクリックしたのか

最後に少しクリック音が聞こえたかもしれません。ブラウザは指示されたとおりに正確に実行しました。クリック音は物理現象です。

オシレーターは停止したときに波の途中にあったため、スピーカーコーンはその移動範囲の端のどこかにあり、その後すぐに元に戻りました。圧力の瞬間的なジャンプがクリック音です。

修正するには、ピッチではなく音量を制御する2番目の数値が必要です。ミュージシャンはそれをエンベロープと呼びます。

エンベロープ付きの同じ音

const osc = ctx.createOscillator();
osc.frequency.value = 440;

const env = ctx.createGain();
const t = ctx.currentTime;

env.gain.setValueAtTime(0, t);
env.gain.linearRampToValueAtTime(0.3, t + 0.01);
env.gain.exponentialRampToValueAtTime(0.001, t + 1);

osc.connect(env).connect(out);
osc.start(t);
osc.stop(t + 1);

エンベロープは単一の音符の音量カーブで、無音から無音に戻るまでのものです。前面の上昇はアタック、その後の下降はディケイです。

フェードインまで10ミリ秒、その後ゆっくりとほぼ無音まで減衰します。それがテストトーンと、もう一度聞きたくなるようなものの違いです。

アタック 10ms ディケイ 1.00s

アタックを半秒近くまでドラッグすると、音は鳴り始めるのではなく膨らみ始めます。それはもはや打たれたものではなく、弓で弾かれたようなもので、ピッチは1ヘルツも動いていません。音符のキャラクターとして聞こえるもののほとんどは、そのエンベロープから来ています。

ただし、これは単純化されたエンベロープです。完全版はADSR:アタック、ディケイ、サステイン、リリースで、サステインはキーが押されている間に音符が保持するレベル、リリースはキーを離したときにどのようにフェードするかです。

次の例で使われるヘルパー関数は以下の通りです。

function note(freq, start = 0, length = 0.5, type = "sine") {
  const t = ctx.currentTime + start;
  const osc = ctx.createOscillator();
  const env = ctx.createGain();
  osc.type = type;
  osc.frequency.value = freq;

  env.gain.setValueAtTime(0, t);
  env.gain.linearRampToValueAtTime(0.3, t + 0.01);
  env.gain.exponentialRampToValueAtTime(0.001, t + length);

  osc.connect(env).connect(out);
  osc.start(t);
  osc.stop(t + length);
}

音色とは、要求しなかった周波数である

サイン波は単一の周波数であり、それ以外は何もありません。そのため、聴力検査のような音に聞こえ、どの楽器とも似ていません。440Hzにチューニングされたギター弦を弾くと、毎秒440回繰り返される波が得られますが、弦は同時に半分、3分の1、4分の1でも振動しています。これらは、要求した周波数の上に乗っている追加の周波数です(880、1320、1760など)。

そのスタックは倍音列と呼ばれます。要求した音は基音であり、倍音列は周波数を1、2、3、4、5倍したものです。

1x 220Hz

2x 440Hz

3x 660Hz

4x 880Hz

5x 1100Hz

6x 1320Hz

7x 1540Hz

8x 1760Hz

220Hzの倍音列。バーをクリックすると、その倍音を単独で聞くことができます。

バーをクリックしてください。単独ではかなり退屈です。重要なのは、それらがパッケージとして到着すること、そしてそれらの相対的な音量のレシピが、バイオリンをバイオリンらしく、トランペットらしくしないものであることです。ミュージシャンはそれを音色と呼び、どちらの場合も同じ音です。

ブラウザには、すぐに使える4つのレシピが用意されています。

同じピッチの4つの波形

["sine", "triangle", "square", "sawtooth"].forEach((type, i) => {
  note(220, i * 0.7, 0.6, type);
});

同じ220Hz、4つの非常に異なるキャラクター。矩形波は奇数倍音のみを含み、そのため中空でわずかにエレクトロニックに聞こえます。のこぎり波はすべてを含み、耳障りでバズのような音に聞こえます。上のスコープは、再生中の各波形の形状を示しており、その形状が倍音のレシピです。

倍音列を覚えておいてください。それは、以降のほとんどすべてを説明します。演奏するすべての音符は、静かな追加の音符のスタックを引きずり、どの音符がそれらであるかは私たち次第ではありません。それは算術であり、振動する弦と空気の柱の物理学によって固定されており、それらを基盤とするどの楽器でも同じように出てきます。

周波数を2倍にすると同じ音になる

ここに5つの音符があります。それぞれが前の音符の周波数の2倍です。

1つの音符、5回

[110, 220, 440, 880, 1760].forEach((freq, i) =>
  note(freq, i * 0.45, 0.4),
);

それらは異なるピッチですが、同じ音のように聞こえます。単に似ているのではなく、同じです。接触のないほとんどすべての音楽文化が、これを独立して発見しました:周波数を2倍にすると、同じ名前を持つに値するほど似たものが得られます。西洋記譜法では、これらの周波数は上記の例ではすべてAと呼ばれ、それらの間の距離はオクターブです。

なぜ2倍にすると同じ音に聞こえるのでしょうか? 倍音列がそれを説明します。440のすべての倍音は、220の倍音列にすでに存在しています。なぜなら、220の倍音列は220、440、660、880、1100であり、440の倍音列は440、880、1320、1760だからです。高い方の音は、低い方がすでに生成していなかった周波数を追加しないため、耳は異なる明るさで同じ色合いを聞きます。

低い方の音符の各サイクルで繰り返される220Hzと440Hz

その観察結果から、この記事の残りの部分で利用する2つのルールが得られます。

ピッチは加算的ではなく乗算的です。オクターブを上がるということは、何かを加えるのではなく2倍にすることです。110から220までのギャップは110Hzであり、880から1760までのギャップは880Hzであり、それらはまったく同じ距離に聞こえます。周波数空間は対数的であり、音楽のすべてのインターバルは比率です。

私たちは1つのオクターブだけを解決する必要があります。2倍にすると同じ音に戻るため、「どのピッチが存在すべきか」という問題は、「周波数とその2倍の周波数の間の空間をどのように分割するか」という問題に還元されます。それを一度解決すれば、答えは可聴範囲全体に繰り返されます。

では、オクターブをどのように分割しますか?

単純な比率は良く聞こえ、その理由はここにある

単純な答えは、均等に分割して家に帰ることです。しかし、私たちの耳は他の考えを持っています。すべての周波数ペアを同じように経験するわけではありません:いくつかの組み合わせは落ち着いて協和して聞こえ、他の組み合わせは緊張して、ざらざらして、あるいは単に間違っているように聞こえます。その違いはすぐに聞くことができます。

同じ音に対する6つの比率

const ratios = [
  ["2/1   octave", 2],
  ["3/2   fifth", 3 / 2],
  ["4/3   fourth", 4 / 3],
  ["5/4   major third", 5 / 4],
  ["16/15 semitone", 16 / 15],
  ["√2    the awkward one", Math.SQRT2],
];

ratios.forEach(([label, ratio], i) => {
  note(220, i * 1.4, 1.2);
  note(220 * ratio, i * 1.4, 1.2);
  console.log(label, ">", (220 * ratio).toFixed(2) + "Hz");
});

最初の4つはコードのように聞こえます。16/15は2つの音が議論しているように聞こえます。最後のものは車の警報のように聞こえます。そして、パターンは、それを見たときに微妙ではありません:比率が単純であるほど、協和して聞こえる傾向があります。 2/1オクターブ、次に3/2の5度、次に4/3の4度、そして16/15になると完全に崩壊します。

「心地よい」という主観的なものに対して、これは疑わしいほど算術的な結果であり、それには2つの物理的な理由があります。

1つ目は再び倍音列です。220と330(3:2の比率)を一緒に鳴らしてください。最初の音は220、440、660、880、1100、1320を生成します。2番目の音は330、660、990、1320を生成するため、660と1320を正確に共有します。5度離れた2つの音は、実際には2つの別々の音ではなく、互いを強化する、大きく重なり合った2つのスタックです。次に220と311(√2に近い)を試してください。どの倍音も一致しません。互いに関連のない周波数の2つの完全なスタックが得られます。

2番目の理由は、ラフネス(ざらつき)です。2つの周波数が近くても同一でない場合、それらは位相がずれて波打ち、音量のパルスを聞きます。それがビートであり、チューニングがずれた音がチューニングがずれているように聞こえる原因です。

広い範囲から同一まで、ビート

[220, 226, 223, 221, 220.5, 220].forEach((freq, i) => {
  note(220, i * 1.3, 1.2);
  note(freq, i * 1.3, 1.2);
});

2つの周波数が収束するにつれてワブルは遅くなり、一致すると消えます。そのレートはそれらの間の差と正確に一致します。6ヘルツ離れていると、毎秒6パルス。分数に複雑な場合、倍音の2つのスタックは数ヘルツ離れたペアで散らばっており、それらの各ペアは他のペアとビートしています。それが不協和音です。

低い方の音符の各サイクルで繰り返される220Hzと440Hz

上記の比率を切り替えて、緑色の線(2つの波が加算されたもの、まさに鼓膜が加算するもの)を見てください。2:1と3:2の場合、結合された形状はほとんどすぐに繰り返されるパターンに落ち着きます。16:15の場合、元に戻るのに15サイクルかかります。√2の場合、√2は無理数なので、耳がロックできるパターンがまったくないため、決して元に戻りません。

協和音とは、周波数間の単純な関係を耳が認識することです。 関係が単純であるほど、音は協和して聞こえる傾向があります。それがすべてではありませんが、驚くほど多くのことを説明するには十分です。

5度を積み重ね、修正できないバグ

これで実際に何かを構築できます。単純な比率が良いものであることはわかっています。オクターブ自体を除けば、物理学で最もクリーンな比率は3/2、つまり5度です。

では、オクターブと5度だけを使って音楽のアルファベットを構築しようとするとどうなるでしょうか?

明白なことをしてください:5度ずつ上がり、オクターブを離れるたびに半分にします。これはピタゴラスが行ったこととほぼ同じで、しばらくの間はうまく機能します。

1つのオクターブ内の5度の積み重ね

let freq = 220;
const notes = [220];

for (let i = 0; i < 12; i++) {
  freq = (freq * 3) / 2;
  while (freq >= 440) freq = freq / 2;
  notes.push(freq);
  console.log(`fifth ${i + 1}: ${freq.toFixed(3)}Hz`);
}

notes.sort((a, b) => a - b).forEach((f, i) => note(f, i * 0.22, 0.3));

最初の音符と最後の音符を見てください。220から始まり、12回の5度を適用し、223に着地しました。220ではありません。同じ音になろうと audible に聞こえるほど近いですが、使用できないほど離れています。

誤差は丸めではなく構造的なものであり、以下のようにするとより簡単にわかります。

閉じられないギャップ

const twelveFifths = (3 / 2) ** 12;
const sevenOctaves = 2 ** 7;

console.log("twelve fifths:", twelveFifths.toFixed(6));
console.log("seven octaves:", sevenOctaves.toFixed(6));
console.log("ratio:", (twelveFifths / sevenOctaves).toFixed(6));
console.log(
  "in cents:",
  (1200 * Math.log2(twelveFifths / sevenOctaves)).toFixed(2),
);

note(220, 0, 1.5);
note((220 * twelveFifths) / sevenOctaves, 0, 1.5);

12回の完全な5度は、7回の完全なオクターブを1.0136倍上回ります。そのギャップはピタゴラス音律のコンマと呼ばれます。セントはピッチの距離を測定する方法であり、オクターブには1200セントがあり、コードのその数値はそこから来ています。したがって、23セントは約隣接するピアノキー間のギャップの4分の1であり、最後の2つの音符のペアでゆっくりとした醜いビートとして聞くことができます。

そして、プログラマーが認識する理由で、それを修正することはできません。5度を積み重ねるということは3/2を掛けることを意味するため、n回の5度の後、3^n / 2^nになります。オクターブを積み重ねるということは2^mを意味します。両者が一致するには、3^n = 2^(n+m)が必要であり、3のべき乗が2のべき乗に等しくなる必要があります。3と2は両方とも素数です。nが何であっても、決して起こりません。

オクターブが純粋で、5度が純粋で、すべてが整然としたループに閉じられるシステムは存在せず、かつて存在したこともありません。すべての調律システムは、ある方法または別の方法で、競合するインターバル関係の異なる妥協です。

平均律は妥協である

現代の答えは、純粋な比率を完全に諦めることです。オクターブを取り、それを12の均等な乗算ステップに分割し、オクターブ以外は何一つ正確に正しくならないことを受け入れます。

1ステップは2の12乗根です。

2の12乗根

const semitone = 2 ** (1 / 12);
console.log("one semitone =", semitone);

let freq = 220;
for (let i = 0; i < 13; i++) {
  note(freq, i * 0.2, 0.28);
  freq = freq * semitone;
}

console.log(
  "twelve steps later:",
  (220 * semitone ** 12).toFixed(10),
);

12ステップは正確に440に着地します。なぜなら、それが根の働き方だからです。したがって、オクターブは構築によって完璧であり、他のすべては近似されます。

明白な質問は、なぜ12なのかということです。それは伝統ではなく、約15行で見つけることができます。可能な限りのnに対してオクターブをn個の均等なステップに分割し、利用可能な最良のステップが実際の3/2の5度にどれだけ近いかを確認します。

12を総当たりで検索

function bestFifth(n) {
  let error = Infinity;
  let step = 0;
  for (let candidate = 1; candidate < n; candidate++) {
    const off = Math.abs(2 ** (candidate / n) - 1.5);
    if (off < error) {
      error = off;
      step = candidate;
    }
  }
  return { step, error };
}

for (let n = 5; n <= 25; n++) {
  const { step, error } = bestFifth(n);
  const pct = (error / 1.5) * 100;
  console.log(
    `${n} steps: best fifth is step ${step}, off by ${pct.toFixed(3)}%`,
  );
}

// And here is what those errors actually sound like.
[5, 7, 12, 19].forEach((n, i) => {
  const { step } = bestFifth(n);
  note(220, i * 1.7, 1.5);
  note(220 * 2 ** (step / n), i * 1.7, 1.5);
});

最後の4行は、5、7、12、19分割で最良の5度を再生します。それぞれが同じ220Hzに対して再生されます。最初の2つは波打ち、3番目はクリーンで、4番目はその中間に位置します。あなたはエラー列を聞いています。

12は、5度を約0.1パーセントの精度で正しく取得する最初の分割であり、それ以下のどの数値よりも8倍以上優れています。24は同点ですが、それは24ステップが12ステップに余分な音符が間に挟まっているだけなので、実際には競合相手ではありません。5度が改善されるまで、29と41まで行かなければなりませんが、それらのシステムは西洋音楽ではるかに一般的ではありません。説得力のある5度を購入するのに最も安価な音符の数であり、5度が近くなると、4度と3度も一緒に付いてきます。

どれだけ近いかを示します。

実際の5度と妥協案の比較

const pure = (220 * 3) / 2;
const tempered = 220 * 2 ** (7 / 12);

console.log("pure fifth:     ", pure.toFixed(4) + "Hz");
console.log("tempered fifth: ", tempered.toFixed(4) + "Hz");
console.log(
  "difference:     ",
  (1200 * Math.log2(tempered / pure)).toFixed(2),
  "cents",
);

note(220, 0, 1.5);
note(pure, 0, 1.5);
note(220, 2, 1.5);
note(tempered, 2, 1.5);

2セントフラット。2番目のペアには、注意深く聞けばゆっくりとしたビートがあり、そのビートは地球上のすべてのピアノで演奏されるすべての5度に存在します。私たちは、どこでも少し間違っている方が、1つのキーで完璧でも他のキーで使えないよりは良いと判断しました。

その見返りは、音符が整数になったことです。任意の音符を番号0として選択すると、他のすべての音符はそれからの半音の整数倍になります。慣習はMIDI番号付けであり、69は私たちの440HzのAであり、変換は1行です。

音符は整数になりました

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

console.log("60 (middle C):", midiToFreq(60).toFixed(2));
console.log("69 (the A above it):", midiToFreq(69).toFixed(2));
console.log("81 (an octave up):", midiToFreq(81).toFixed(2));

[60, 62, 64, 65, 67, 69, 71, 72].forEach((n, i) =>
  note(midiToFreq(n), i * 0.25, 0.4),
);

最後の行は長音階であり、音階が何であるかをまだ定義していません。それは単なる整数の配列です。ここからこの記事のすべては配列で行われ、周波数について考える必要はなくなりました。

キーをクリック

オクターブごとに12の音符があり、無限に繰り返されます。黒鍵は特別ではなく、単に名前が付かなかったものです。

そのキーボードは一瞬見る価値があります。なぜなら、そのレイアウトは歴史的な偶然だからです。オクターブごとに12個の均等に配置された音符があります。7つには文字と大きな白い鍵盤が割り当てられ、5つにはシャープ記号と小さな黒い鍵盤が割り当てられ、白い鍵盤を持つ7つは、あなたが演奏したばかりの音階と正確に一致します。その下の物理学は完全に均一であり、キーボードはそうではありません。

なぜ12音符で、すべてではないのか

12音符があるからといって、12音符を使うわけではありません。それらを順番にすべて演奏すると、驚くほど音楽的ではありません。

すべて12音符、順番に

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

for (let n = 60; n <= 72; n++) {
  note(midiToFreq(n), (n - 60) * 0.18, 0.25);
}

効果音のように聞こえます。すべてのステップが均一なので、何も際立たず、どこにも「家」のように聞こえず、自分がどこにいるのかを知る方法もありません。

音楽はサブセットを選びます。ほとんどの場合、12個の中から7つを選び、それらの間のギャップが不均一になるように選択します。これにより、耳で音符を区別することが可能になります。サブセットは音階であり、音階は音符ではなく、それらの間のステップとして記述するのが最善です。

音階はギャップのリストです

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const major = [2, 2, 1, 2, 2, 2, 1];

function buildScale(root, pattern) {
  return pattern.reduce(
    (notes, step) => [...notes, notes.at(-1) + step],
    [root],
  );
}

const cMajor = buildScale(60, major);
console.log("C major:", cMajor);

cMajor.forEach((n, i) => note(midiToFreq(n), i * 0.25, 0.4));

ステップは2 2 1 2 2 2 1で、合計12になり、パターンはオクターブを正確に閉じます。それが長音階、西洋音楽で最も馴染み深い音であり、7要素の配列です。

数字を1つ変更すると、まったく異なるムードになります。

幸せと悲しみの違いは1つの数字

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, pattern) =>
  pattern.reduce(
    (notes, step) => [...notes, notes.at(-1) + step],
    [root],
  );

const patterns = {
  major: [2, 2, 1, 2, 2, 2, 1],
  naturalMinor: [2, 1, 2, 2, 1, 2, 2],
  majorPenta: [2, 2, 3, 2, 3],
  minorPenta: [3, 2, 2, 3, 2],
  blues: [3, 2, 1, 1, 3, 2],
};

let when = 0;
Object.entries(patterns).forEach(([name, pattern]) => {
  const scale = build(60, pattern);
  console.log(name.padEnd(13), scale.join(" "));
  scale.forEach((n, i) => note(midiToFreq(n), when + i * 0.22, 0.35));
  when += scale.length * 0.22 + 0.5;
});

長調と自然短調は、ギャップをシャッフルした同じ7音符のアイデアです。ペンタトニック音階は2つの音符を省略するため、間違った音符を演奏するのが非常に難しく、すべての初心者ギターレッスンがそこから始まる理由です。ブルース音階は意図的に不快な音符を1つ追加します。

モードは、私が常に記憶すべき7つのエキゾチックなギリシャ名のリストとして提示されていましたが、配列を回転させたものであることが判明しました。先頭から最初のステップを取り、末尾に置くと、次のものが得られます。

モードは配列の回転です

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, pattern) =>
  pattern.reduce(
    (notes, step) => [...notes, notes.at(-1) + step],
    [root],
  );

const major = [2, 2, 1, 2, 2, 2, 1];

const names = [
  "Ionian",
  "Dorian",
  "Phrygian",
  "Lydian",
  "Mixolydian",
  "Aeolian",
  "Locrian",
];

const rotate = (arr, by) =>
  arr.map((_, i) => arr[(i + by) % arr.length]);

names.forEach((name, i) => {
  const pattern = rotate(major, i);
  console.log(name.padEnd(11), pattern.join(" "));
  build(60, pattern).forEach((n, j)
    => note(midiToFreq(n), i * 2 + j * 0.2, 0.3),
  );
});

7つのモード、1つの配列、7つの回転。イオニア旋法は長音階であり、エオリア旋法は自然短調です。これは、「長調」と「短調」が2つのシステムではなく、同じものの回転0と回転5であることを意味します。リディア旋法は夢のように聞こえ、フリギア旋法はスペイン語のように聞こえ、ロクリア旋法は壊れているように聞こえます。そして、それらすべては、開始音符に対するギャップがどこにあるかを移動することから来ています。

ここで、音楽理論が恣意的であるという感覚を失いました。

コードは3度で積み重ねられた音符である

コードとは、同時に鳴る複数の音符のことです。これはあまり定義になりませんが、ほとんどの組み合わせはひどく聞こえます。有用な質問は、どれがそうでないかということです。

比率からすでに答えを知っています:倍音が重なる音符です。音階では、その説明に当てはまる音符は、スケール度で2つ離れた、ミュージシャンが3度と呼ぶギャップです。そこで、音階を取り、開始度を選択し、他の音符をすべて取得します。

トライアドは [0, 2, 4]

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, p) =>
  p.reduce((n, s) => [...n, n.at(-1) + s], [root]);

const cMajor = build(60, [2, 2, 1, 2, 2, 2, 1]);

const triad = [0, 2, 4].map((i) => cMajor[i]);

console.log("scale:", cMajor.join(" "));
console.log("triad:", triad.join(" "));
console.log("gaps:", triad[1] - triad[0], "and", triad[2] - triad[1]);

triad.forEach((n) => note(midiToFreq(n), 0, 2));

これはCメジャーコードです。3つの音符で、それが構築された音符(ミュージシャンがルートと呼ぶ)から0、4、7半音上です。周波数では1 : 1.26 : 1.50、ほぼ4 : 5 : 6です。倍音があちこちで重なり合う3つの単純な比率であり、そのため非常に落ち着いて聞こえます。

3度は4半音(長3度)または3半音(短3度)のいずれかであり、2つを積み重ねるとどちらの場合も7半音になります。長調は4 + 3、短調は3 + 4です。

ここで中央の音符を1半音下げてみましょう。

1つの半音、まったく異なる感覚

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const chord = (shape, root, at) =>
  shape.forEach((s) => note(midiToFreq(root + s), at, 1.6));

const shapes = {
  major: [0, 4, 7],
  minor: [0, 3, 7],
  diminished: [0, 3, 6],
  augmented: [0, 4, 8],
};

Object.entries(shapes).forEach(([name, shape], i) => {
  console.log(name.padEnd(11), shape.join(" "));
  chord(shape, 60, i * 2);
});

長調から短調への変化は、1つの音符が1半音移動することです。西洋音楽の2つの感情的な極を隔てるのはそれだけです。コードでは[0,4,7]対[0,3,7]です。減音程は両方のギャップを圧縮し、不安定で不安に聞こえます。増音程は両方を広げ、映画で何かが間違った方向に進みそうに聞こえます。

私はこれを、良い意味で、本当に迷惑だと感じました。長調と短調は深いカテゴリであるという考えを吸収していました。それらは単一の配列要素であり、1つだけ異なります。

さらに4番目の音符、つまりさらに3度上を追加すると、7度コードが得られ、ここで音楽ではなく実際の音楽のように聞こえ始めます。

7度

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const shapes = {
  "major 7th": [0, 4, 7, 11],
  "minor 7th": [0, 3, 7, 10],
  "dominant 7th": [0, 4, 7, 10],
};

Object.entries(shapes).forEach(([name, shape], i) => {
  console.log(name.padEnd(13), shape.join(" "));
  shape.forEach((s) => note(midiToFreq(60 + s), i * 2.2, 1.8));
});

メジャーセブンスはジャズカフェのコードです。マイナーセブンスは滑らかでわずかにメランコリックです。ドミナントセブンスは興味深いものです。ドミナントは音階の5番目の度数に対する伝統的な名前であり、このコードは多くの作業を行うことになります。

キーは7つのコードを提供する

これがコードチャートが最終的に私にとって意味をなすようになった部分です。

最初の度数から始めることに特別なことは何もありません。7つすべてから順番に実行し、オクターブをラップすると、音階の7つの音符のみから構築された7つのコードが得られます。

1つの音階から7つのコード

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, p) =>
  p.reduce((n, s) => [...n, n.at(-1) + s], [root]);

const scale = build(60, [2, 2, 1, 2, 2, 2, 1]).slice(0, 7);
const names = ["C", "D", "E", "F", "G", "A", "B"];

const chordOn = (degree) =>
  [0, 2, 4].map((step) => {
    const i = degree + step;
    return scale[i % 7] + Math.floor(i / 7) * 12;
  });

names.forEach((name, degree) => {
  const notes = chordOn(degree);
  const shape = notes.map((n) => n - notes[0]);
  const quality =
    shape[1] - shape[0] === 4
      ? "major"
      : shape[2] - shape[0] === 6
      ? "diminished"
      : "minor";
  console.log(`${name} ${quality.padEnd(11)} ${notes.join(" ")}`);
  notes.forEach((n) => note(midiToFreq(n), degree * 1.5, 1.3));
});

それらの品質を選んだ人はいません。3つは長調で、3つは短調で、最後の一つは減音程で出てきます。そのパターンは、音階の不均一なギャップによって強制されます。隣接する音符が4と3の間隔で配置されている度数から「他の音符」プロセスを開始すると、長調コードが得られます。3と4の間隔で開始すると、短調コードが得られます。

ミュージシャンはそれらの7つをローマ数字で書きます:大文字は長調、小文字は短調、小さな円は減音程です。

I ii iii IV V vi vii°

その表記は何か有用なことをしており、それが何をしているのかに気づくのに、私は恥ずかしいほど時間がかかりました。それはコードを名前ではなく、音階内の位置で記述しています。Vコードは、どのキーであっても「5番目の度数に構築されたコード」であり、相対アドレス指定です。ローマ数字で書かれたコードチャートはキーに依存しないソースであり、移調は定数を追加することです。

ローマ数字は相対アドレス指定です

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, p) =>
  p.reduce((n, s) => [...n, n.at(-1) + s], [root]);

function chordsInKey(root) {
  const scale = build(root, [2, 2, 1, 2, 2, 2, 1]).slice(0, 7);
  return (degree) =>
    [0, 2, 4].map((step) => {
      const i = degree - 1 + step;
      return scale[i % 7] + Math.floor(i / 7) * 12;
    });
}

// The same four numerals, played in two different keys.
const progression = [1, 5, 6, 4];

[60, 65].forEach((key, k) => {
  const chord = chordsInKey(key);
  progression.forEach((numeral, i) => {
    chord(numeral).forEach((n) =>
      note(midiToFreq(n), k * 7 + i * 1.6, 1.5),
    );
  });
});

同じ形状、2つの異なる開始音符、そして耳はそれを同じ音楽として認識します。だからこそ、その表記が存在するのです。

緊張と解決

1つのコードは単なる音です。音楽はそれらを順序付けるときに起こることであり、順序は重要です。いくつかのシーケンスは、到着したように感じられ、いくつかのシーケンスは質問のように感じられます。

システムにおける最も強い引きはVからIへのものであり、それには2つの具体的な理由があります。

VからIへの引き

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const play = (notes, at, len = 1.6) =>
  notes.forEach((n) => note(midiToFreq(n), at, len));

const C = [60, 64, 67]; // I
const G7 = [55, 59, 62, 65]; // V7

play(G7, 0, 1.8);
play(C, 2, 2.2);

console.log("B is", 59, "and C is", 60, "- one semitone apart");
console.log("F is", 65, "and E is", 64, "- one semitone apart");

まず、Bという音符はVコードにあり、Iのルートより半音下です。目的地にこれほど近い音符は、ドアに寄りかかっているように聞こえます。ミュージシャンはそれを導音と呼び、クリフハンガーのように機能します。

第二に、ドミナントセブンスコードにはBとFの両方が含まれており、それらは6半音離れています。6半音はトライトーン、オクターブのちょうど半分です:2 ** (6/12)は√2、以前聞いた正確な無理数比です。利用可能な最も不安定なインターバルがコード内にあり、その両方の音符はIに移動するときに反対方向に半音ずつ解決します。あなたが聞く緊張は、その無理数比が単純な比率に置き換えられることです。

緊張と解放の言語は、そのメカニズムに基づいています。ここでは、あなたが何度も聞いたことのある進行のいくつかと共に示します。

あなたがすでに知っている4つの進行

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, p) =>
  p.reduce((n, s) => [...n, n.at(-1) + s], [root]);

const scale = build(60, [2, 2, 1, 2, 2, 2, 1]).slice(0, 7);

const chord = (degree) =>
  [0, 2, 4].map((step) => {
    const i = degree - 1 + step;
    return scale[i % 7] + Math.floor(i / 7) * 12;
  });

const progressions = {
  "I  V  vi IV": [1, 5, 6, 4],
  "ii V  I": [2, 5, 1],
  "I  vi IV V": [1, 6, 4, 5],
  "vi IV I  V": [6, 4, 1, 5],
};

let when = 0;
Object.entries(progressions).forEach(([name, degrees]) => {
  console.log(
    name,
    "->",
    degrees.map((d) => chord(d).join("/")).join("  "),
  );
  degrees.forEach((d, i) => {
    chord(d).forEach((n) => note(midiToFreq(n), when + i * 1.1, 1));
  });
  when += degrees.length * 1.1 + 0.9;
});

I V vi IVは、驚くべき量のポップミュージックが構築されている4つのコードです。ii V Iはジャズのバックボーンです。vi IV I Vは最初のものと同じ4つのコードで、より悲しい場所から始まるように回転されています。

配列を編集してください。1から7までのほとんどすべての数値シーケンスはまとまります。なぜなら、すべてのコードは同じ7つの音符から構築されているからです。キーは、間違った答えに到達するのが難しい制約された空間を提供します。1で終わると完了したように聞こえ、5で終わると次の行が来るように聞こえ、7で終わると何かが間違ったように聞こえます。

記譜法はシリアライゼーションフォーマットである

これまでのどの部分も五線譜を必要としませんでした。上記のすべては整数の配列と、それらを周波数に変換する関数です。

しかし、記譜法は存在します。それは西洋音楽の文献が保存されている形式であり、それが何をエンコードしているのかをすでに知っていれば、それはかなり合理的なデザインであり、いくつかの非常に古い制約があることがわかります。それは、印刷が安価になる前に書かれ、手が忙しい間に人間がリアルタイムで読むために最適化され、インストールベースが大きすぎたため、一度も改訂されなかったシリアライゼーションフォーマットです。

ここにCメジャー音階、つまり同じ7つの整数があります。

MIDI 60 62 64 65 67 69 71 72

垂直軸はピッチですが、線形ではありません。各線と各スペースは音階を1ステップずつ上がります。そのため、連続する位置は、時には2半音、時には1半音離れています。軸はクロマチックではなくダイアトニックです。12音すべてを通過するのではなく、音階を通過します。つまり、ピッチとしてドレスアップされたスケール度を示しています。だからこそ、長音階はページをまっすぐに走る退屈なものに見え、世界で最も自然なシーケンスのように聞こえるのです。フォーマットは、期待されるケースに最適化されています。

クレフは原点を宣言します。五線譜は5本の線で、どの周波数にも固定されていません。そのため、先頭の記号は、あなたがどこにいるかを示します。ト音記号は様式化されたGであり、そのカールはGを意味する線を囲んでいます。ヘ音記号は様式化されたFで、F線を挟む2つの点があります。それは、原点が余白にマークされた座標系です。

同じ7つの整数、1オクターブ下、ヘ音記号で

音符は異なる場所に配置されます。なぜなら、ヘ音記号は原点をオクターブと6度移動させるからです。同じ整数、異なる座標系、そしてそれらの間で、人々が歌い演奏するもののほとんどをカバーしています。

付随記号は、ロスのあるエンコーディングをパッチします。オクターブごとに7つの垂直位置、表現する12の音符。シャープ、フラット、ナチュラル記号はエスケープハッチであり、それらのそれぞれは、そうでなければ位置が意味する音符をシフトする命令です。

12の半音のうち8つ、シャープを書き込んだもの

調号はDRYです。Dメジャーの曲の場合、その音階にはFシャープとCシャープが含まれており、曲中のすべてのFとCにシャープ記号を付ける必要があります。そのため、代わりに各行の先頭に一度宣言し、何かがそうでないことを示すまで適用されます。それはファイルの上部にホイストされた定数です。

Dメジャー:2つのシャープが一度宣言され、8回ではない

2つのシャープがまだ演奏されていることに注意してください。それらは単に各音符に書かれていないだけです。これも、楽譜が演奏する前に調を教えてくれる理由であり、ミュージシャンが曲が「〜調」であると話す理由です。調はヘッダーにあり、本文にはありません。

音価は2のべき乗です。全音符、二分音符、四分音符、八分音符、十六分音符。それぞれが前のものの半分であり、記譜法は視覚的に指数をエンコードします:空の音符頭、次に茎、次に塗りつぶされた音符頭、次に半分にするごとにフラグ。それは二項指数の一項エンコーディングであり、非常に中世的な数値の格納方法であり、一目で誤読することは不可能です。

全音符1つ、二分音符2つ、四分音符4つ、八分音符8つ - すべて同じ合計長

2のべき乗はかなりの範囲をカバーしますが、すべてではありません。そのため、もう1つの演算子があります:音符頭の後のドットは、その長さを1.5倍します。2つのドットは1.75倍します。それは句読点として書かれた二項分数です。

しかし、これらの音価のどれも時間ではありません。それらは拍であり、拍はテンポを固定した場合にのみ秒になります。

拍は、そう言うまで秒ではありません

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const bpm = 120;
const beat = 60 / bpm;

// [midi note, length in beats]
const melody = [
  [60, 1],
  [62, 1],
  [64, 2],
  [65, 0.5],
  [67, 0.5],
  [69, 3],
];

let when = 0;
melody.forEach(([n, beats]) => {
  note(midiToFreq(n), when, beats * beat * 0.95);
  when += beats * beat;
});

console.log("total:", when.toFixed(2), "seconds at", bpm, "bpm");

bpmを200に変更すると、同じ配列が同じ曲を、より速く再生します。その分割が、スコアをポータブルにするものです。相対的な音価を格納し、演奏者がクロックを供給します。

拍子記号は拍をグループ化します。4/4は1小節あたり4つの四分音符の拍を意味し、3/4は3つを意味します。縦線は、ページ上の場所を見つけやすくするためです。それは主に読みやすさのための機能ですが、実際の音楽的な主張も持っています。それは、各グループの最初の拍が強い拍であるということです。同じ6つの音符を3つずつグループ化した場合と2つずつグループ化した場合では、異なる曲になります。

それがほぼすべてです。ダイアトニック軸上のピッチ、原点とエスケープハッチ付き、2の負のべき乗としての音価、およびいくつかのヘッダーフィールド。楽譜上の他のすべては、その上に重ねられた演奏指示です:どれだけ大きく、どれだけ滑らかに、どの指で。

まとめ

ここにすべてを1か所にまとめました。キー、そのダイアトニックコード、進行、それらのコードを1つずつアルペジオに分解したもの、そして音階に留まるメロディー。約40行、ライブラリなし、そして私が音楽と呼ぶことができる最初のものです。デモンストレーションではなく。

キー、進行、そしてチューン

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

const build = (root, p) =>
  p.reduce((n, s) => [...n, n.at(-1) + s], [root]);

const key = 57; // A
const scale = build(key, [2, 1, 2, 2, 1, 2, 2]).slice(0, 7); // natural minor

const bpm = 104;
const beat = 60 / bpm;

const chord = (degree) =>
  [0, 2, 4].map((step) => {
    const i = degree - 1 + step;
    return scale[i % 7] + Math.floor(i / 7) * 12;
  });

const progression = [1, 6, 3, 7];
const melody = [0, 2, 4, 2, 3, 2, 1, 0, 4, 3, 2, 1, 0, 2, 1, 0];

progression.forEach((degree, bar) => {
  const at = bar * 4 * beat;
  const notes = chord(degree);

  // Bass note on the downbeat.
  note(midiToFreq(notes[0] - 12), at, beat * 3.6, "triangle");

  // Arpeggio: up, down, up, across the bar.
  [0, 1, 2, 1, 0, 1, 2, 1].forEach((which, i) => {
    note(midiToFreq(notes[which]), at + i * beat * 0.5, beat * 0.45);
  });

  // Melody, four notes per bar, always from the scale.
  melody.slice(bar * 4, bar * 4 + 4).forEach((step, i) => {
    note(
      midiToFreq(scale[step % 7] + 12),
      at + i * beat,
      beat * 0.9,
      "triangle",
    );
  });
});

ここにあるすべての数値は、私たちが導き出した意味を持っています。57は、2の12乗根とチューニングフォークのおかげでAです。[2,1,2,2,1,2,2]は、長音階を5つ回転させたものなので、短音階です。[0,2,4]はコードです。なぜなら、音符がスケール度で2つ離れていると倍音が重なるからです。[1,6,3,7]は、緊張がどこにあるかによって、どこかに移動するように聞こえます。

キーを60に変更すると移動します。スケールパターンを[2,2,1,2,2,2,1]に変更すると、同じチューンが陽気になります。メロディー配列を任意のものに変更しても、スケールをインデックス化しているため、常にフィットします。その制約が、上記のすべての理論の目的でした。

まだ理解していないこと

かなりたくさん。この記事はピッチとほとんどすべてをカバーしておらず、ピッチはおそらく簡単な半分です。

リズムについてはほとんど触れていません。読んだすべてが、それが見かけよりも深いことを示唆しています。ボイスリーディング、つまりコード間を最小限の距離で移動するのではなく、飛び回るビジネスは、書かれた音楽が単に正しいだけでなく、良く聞こえ始める場所であり、その理由を聞かなくてもルールを述べることができます。メロディーが着地したい場所にしたい理由のほとんどは、私にはまだ不透明です。そして、上記のすべては、ある伝統の答えです。多くの音楽はオクターブを異なる方法で分割したり、オクターブを単位として扱わなかったりしますが、それらのどれも間違っていません。

しかし、もはや雑学を暗記するように求められているとは感じていません。12音符は、2のべき乗と3のべき乗を調和させ、丸め誤差で妥協しようとしたときに得られるものです。音階は、どこにいるかがわかるほどギャップが不均一な、それらの音符のサブセットです。コードは、倍音がすでに重なり合っている音符のセットであり、キーは開始音符に対してコードを名前付けする方法であり、記譜法はそれらすべてを書き出すための古く、わずかにロスのある方法です。それらのそれぞれは、長い間、制約の下で行われたエンジニアリング上の決定であり、理由はその理由を探しに行けばまだそこにあります。

さらに進みたい場合は、この記事を書き始めるきっかけになった2つのことは、EeveeによるMusic Theory for Nerds(素晴らしい読み物です)と、この主題に関するインターネット上で最も美しいLightNoteです。API側では、MDNのWeb Audioドキュメントは異常に優れています。

このページにあるすべての例は、依存関係のないプレーンなJavaScriptなので、ブラウザエンジンがあればどこでも実行できます。さらに深く掘り下げたい場合は、RunJSは、Nodeとnpmと並んでWeb Audio APIを搭載したデスクトップJavaScriptプレイグラウンドなので、セットアップは何も必要ありません。上記のいずれかのスニペットをそこに貼り付け、先頭にconst ctx = new AudioContext()とconst out = ctx.destinationを追加して、そこから続行してください。