音のプログラミングで参考にしたもの — Web Audio APIと音楽理論の接点
このトピックを深掘りする
Web Audio・サウンドプログラミング
Web Audio APIによる音響合成・楽器実装・物理シミュレーション参考文献
全3本中 1 本目。
次に読む記事
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
はじめに
sakimytocomには現在約200個のインタラクティブコンポーネントがある。そのうち「音」を扱うものは11個だ。
Piano, Guitar, Kalimba, Theremin, Drum Machine, Wind Chimes, Music Box, Metronome, Color Organ, Music Visualizer, Sound Waves——これらは単にUIを作れば終わりという類のコンポーネントではない。音をリアルタイムに生成し、周波数を計算し、エンベロープで音色を制御し、ブラウザのAutoplay Policyと戦う。HTMLとCSSでは到達できない領域がそこにある。
この記事では、11個の音楽コンポーネントを実装する過程で学んだことと、その学びの源泉となった書籍・リソースを紹介する。書評や技術解説というより、「何を読んで、何を作り、何にハマったか」の実装者目線の記録だ。
音のプログラミングは、最初は難しそうに見える。しかし振り返ると、基本的な概念は驚くほど少ない。AudioContext、OscillatorNode、GainNode、エンベロープ——この4つを理解すれば、ブラウザで楽器を作れるようになる。問題は、この4つに到達するまでにかなりの回り道をしたことだ。
Web Audio APIの基本概念
Web Audio APIの設計は、一言でいえば「ノードを繋ぐ」モデルだ。音声信号がグラフ上を流れていく。
AudioContext → OscillatorNode → GainNode → destination(スピーカー)この3段構成がすべての基本になる。OscillatorNodeが音を生成し、GainNodeが音量を制御し、destinationがスピーカーに出力する。シンプルだが、この「グラフベースのシグナルフロー」という考え方が、あらゆるエフェクト処理の土台になっている。
実装上の最も重要な原則は「AudioContextは1つだけ作って使い回す」ことだ。
let audioCtx: AudioContext | null = null
function getAudioContext(): AudioContext {
if (!audioCtx) {
audioCtx = new AudioContext()
}
if (audioCtx.state === 'suspended') {
audioCtx.resume()
}
return audioCtx
}AudioContextの生成はコストが高い。複数のコンポーネントがそれぞれ独自のAudioContextを作ると、ブラウザのリソースを圧迫する。さらに、ブラウザによっては同時に存在できるAudioContextの数に制限がある。
もう一つの鉄則は、初期化を必ず遅延させることだ。コンポーネントのマウント時にAudioContextを作るのではなく、ユーザーの最初のインタラクション(クリックやタップ)で初期化する。これはiOS Safariの制限に起因する——ユーザージェスチャーのコールスタック内でなければ、AudioContextはsuspended状態のまま固定される。
この教訓は高くついた。18個のコンポーネントでAudioContext関連のバグを一括修正したことがある。最も多かったパターンは「コンポーネントのuseEffect内でAudioContextを初期化し、iOSでは音が出ない」というものだった。修正方針はシンプルで、初期化をイベントハンドラ内に移動し、try-catchで囲むだけ。しかし18ファイル分の修正は、最初から正しいパターンを知っていれば不要だった。
楽器ごとの実装アプローチ
11個の音楽コンポーネントは、大きく3つの実装パターンに分類できる。
- 音程のある楽器: Piano, Guitar, Kalimba, Theremin, Music Box, Wind Chimes
- リズム楽器: Drum Machine, Metronome
- 音の可視化: Sound Waves, Music Visualizer, Color Organ
それぞれ、Web Audio APIの使い方がまったく異なる。
Piano: 12平均律とエンベロープ
Pianoの実装で最初に必要になるのは、鍵盤と周波数の対応関係だ。これは12平均律という数学的に美しいルールで決まる。
function noteToFrequency(note: number): number {
return 440 * Math.pow(2, (note - 69) / 12)
}A4(MIDIノート番号69)が440Hz。そこから半音上がるごとに周波数が2^(1/12)倍になる。1オクターブ(12半音)上がるとちょうど2倍。この数式自体は中学数学で理解できる。
しかし、この数式だけでは「ピアノっぽい音」は出ない。OscillatorNodeの生の矩形波は、電子音そのものだ。「ピアノっぽさ」を生むのはADSRエンベロープ——鍵盤を押してから離すまでの音量変化のカーブだ。
Attack (0.01s) → 急速に最大音量へ
Decay (0.1s) → 少し音量が下がる
Sustain (0.3) → 押している間の音量レベル
Release (0.3s) → 離した後の減衰Attackを0.01秒にするか0.1秒にするかで、まったく異なる楽器の印象になる。ピアノのように打鍵する楽器はAttackが極端に短い。ストリングスのように弓で弾く楽器はAttackが長い。たった4つのパラメータで、楽器の「触感」を表現できるのがADSRの面白さだ。
実装上のポイントは、GainNodeのlinearRampToValueAtTime()やexponentialRampToValueAtTime()を使うこと。setIntervalでゲインを手動で変えるのではなく、Web Audio APIのスケジューリング機能に委ねる。音声処理はメインスレッドとは別のオーディオスレッドで走るため、JavaScriptのタイマーに頼ると音のタイミングがずれる。
Theremin: 連続的な周波数制御
Thereminは、Web Audio APIの「リアルタイム性」が最も活きるコンポーネントだ。マウスの動きがそのまま音になる。
X軸がピッチ(周波数)、Y軸がボリューム。ポインターの位置を周波数にマッピングするとき、リニアスケールではなく指数スケールを使う。
const minFreq = 65 // C2
const maxFreq = 2093 // C7
const frequency = minFreq * Math.pow(maxFreq / minFreq, normalizedX)人間の耳は周波数を対数的に知覚する。200Hzと400Hzの差(1オクターブ)は、200Hzと600Hzの差(1オクターブ+完全五度)よりも「音程の差」としては小さく感じるが、Hz数としての差は同じ200Hzだ。指数マッピングにすることで、画面上の等距離移動が等しい音程変化として知覚される。
Thereminで学んだもう一つの重要な技術は、OscillatorNodeのfrequency.setTargetAtTime()だ。マウスが動くたびに即座に周波数を切り替えると、不快なクリック音(ポップノイズ)が発生する。setTargetAtTimeで少しだけスムージングをかけることで、滑らかな周波数遷移が実現する。これは本物のThereminの動作にも近い。
Kalimba: 減衰音と倍音
Kalimbaの金属的な音色を再現するには、倍音構成を模倣する必要がある。
function playKalimbaNote(frequency: number) {
const ctx = getAudioContext()
const harmonics = [
{ ratio: 1, gain: 1.0 }, // 基音
{ ratio: 2, gain: 0.6 }, // 第2倍音
{ ratio: 3, gain: 0.2 }, // 第3倍音
{ ratio: 5.4, gain: 0.08 }, // 非整数倍音(金属感)
]
// 各倍音をOscillatorNodeとして生成し、個別のGainNodeで音量調整
}実際の楽器の振動モードを完全に再現することは難しいが、基音に対して整数倍・非整数倍の周波数成分を重ね合わせることで、「それっぽい音色」は作れる。特に非整数倍音を少し加えると、金属的なきらめきが生まれる。これは木琴とカリンバの音色の違いを説明する要素でもある。
Kalimbaのもう一つの特徴は、減衰が比較的速いこと。弾いた瞬間に最大音量になり、1〜2秒で消える。エンベロープでいえば、Attackが極短、Sustainがゼロ、Releaseが支配的。ピアノとは異なるカーブだ。
参考書: Web Audio API(Boris Smus)
この分野で最初に読むべき本は、Boris Smusの『Web Audio API』(O'Reilly)だ。
この本は薄い——120ページほどしかない。それでも、Web Audio APIの設計哲学と全体像を掴むには最適だ。著者はGoogleのエンジニアで、Web Audio APIの仕様策定にも関わった人物。APIの「なぜこうなっているのか」が腹落ちする説明が随所にある。
特に有用だったのは以下の章だ。
第2章: Perfect Timing and Latency — Web Audio APIのスケジューリングモデルが説明されている。currentTimeベースの絶対時間指定が、なぜsetTimeoutよりも正確なのか。Drum MachineとMetronomeの実装では、この章の知識がないとリズムが微妙にずれる。
第3章: Volume and Loudness — GainNodeの使い方だけでなく、人間の聴覚特性(ウェーバー・フェヒナーの法則)に基づいた音量制御の方法が書かれている。リニアなゲイン値ではなく、デシベルスケールで考えるべき理由がわかる。
第5章: Analysis and Visualization — AnalyserNodeを使った周波数解析の方法。Music VisualizerとColor Organはこの章が直接のベースになった。getByteFrequencyData()で取得したスペクトルデータをCanvasに描画するパターンは、ここで学んだ。
グラフベースのシグナルフローという設計思想は、ハードウェアのモジュラーシンセサイザーに由来している。「ノードを繋ぐ」モデルが直感的に感じられるのは、物理的なケーブルを差し替える行為のメタファーだからだ。この発想を知ってからは、新しいエフェクトを追加するときも「どこにノードを挟むか」で考えられるようになった。
音楽理論との接点
11個のコンポーネントを作る中で、音楽理論の基礎知識が繰り返し必要になった。特に重要だったのは「12平均律」「倍音」「協和と不協和」だ。
12平均律 vs 純正律
Pianoの実装で使った12平均律は、すべての半音間隔を均等にする調律法だ。転調が自由にでき、現代の鍵盤楽器のほとんどがこの方式を採用している。
一方、純正律は整数比で音程を定義する。完全五度が3:2、長三度が5:4。数学的にきれいで、和音の響きは12平均律よりも美しい。ただし転調すると破綻する。
この違いを実感したのは、Pianoで和音を鳴らしたときだ。12平均律のCメジャーコード(C-E-G)は、理論上は「わずかにうなりが生じる」。純正律なら完全に調和する。ところが実際にOscillatorNodeで試すと、その差は素人耳にはほとんどわからなかった。この「理論と知覚のギャップ」が面白い。実装上は12平均律で何の問題もない。
倍音構成が音色を決める
同じ440Hzでも、ピアノとフルートとクラリネットではまったく音色が違う。その違いは倍音構成——基音に対してどの周波数成分がどの強さで含まれているか——で説明できる。
- フルート: ほぼ正弦波。倍音がほとんどない
- クラリネット: 奇数倍音が強い(3倍、5倍、7倍…)
- ヴァイオリン: 多数の倍音がバランスよく含まれる
この知識は、OscillatorNodeの波形選択に直結する。sineはフルートに近く、squareはクラリネットに近い。sawtoothはストリングスに近い。もちろん実際の楽器はこれほど単純ではないが、出発点としては十分だ。
FourierSeriesコンポーネントを実装したとき、この関係を可視化した。矩形波をフーリエ級数で分解すると、奇数倍音だけが現れる。数式でsin(x) + sin(3x)/3 + sin(5x)/5 + ...と書いても直感的にはわからないが、アニメーションで重ね合わせの過程を見ると、「矩形波の正体は無限の正弦波の和だ」ということが腑に落ちる。音色の正体が倍音であることの、これ以上ない可視化だった。
The Nature of Code + 数学ガール
音のプログラミングに直接関係する本ではないが、間接的に大きな影響を受けた2冊がある。
The Nature of Code(Daniel Shiffman)
この本の第3章「Oscillation(振動)」は、音の物理的な基盤を理解するのに役立った。正弦波の振幅・周波数・位相という3パラメータが、Web Audio APIのOscillatorNodeのプロパティにそのまま対応する。
Sound Wavesコンポーネントは、この章の延長線上にある。複数の正弦波を重ね合わせて波形を可視化する。音としても再生できる。「見えている波形がそのまま音として聞こえる」という体験は、プログラミングと物理の接点として強力だ。
Music Visualizerもこの本の影響が大きい。音声信号を時間領域と周波数領域の両方で可視化する——AnalyserNodeのgetByteTimeDomainData()が波形、getByteFrequencyData()がスペクトル。Shiffmanの本で学んだ「振動は時間と空間の関数」という感覚が、ここで活きた。
数学ガール(結城浩)
数学ガールのフーリエ変換の章は、「なぜ周波数解析ができるのか」の直感を与えてくれた。
時間領域の信号と周波数領域の信号は、同じ情報の異なる表現である——この二重性の概念は、Music VisualizerやColor Organの設計思想の根幹にある。曲をかけると波形が見え、同時にスペクトルも見える。同じ音楽が2つの異なる形で表示される。それは情報が「失われている」のではなく「変換されている」だけだという理解が、数学ガールによって得られた。
結城浩の文章は、数学の抽象概念を「物語」として追体験させてくれる。フーリエ変換を微分方程式と内積の関係から導出するくだりは、教科書で読むより遥かにわかりやすかった。実装者にとって数学は道具だが、その道具がどういう思想で作られたかを知っていると、使い方の幅が広がる。
AudioContextのハマりポイント
18コンポーネントのAudioContext関連バグを一括修正した経験から、頻出パターンを列挙する。
iOS Safari: ユーザージェスチャー必須 — 最も多かったバグ。useEffectでAudioContextを初期化すると、iOSではsuspended状態のまま固定される。修正は、初期化をクリックハンドラやタッチハンドラに移動すること。
Chrome: Autoplay Policy — Chromeは2018年以降、ユーザー操作なしでの音声再生をブロックしている。AudioContext.state === 'suspended'のチェックとresume()の呼び出しは、すべてのコンポーネントで必須。
OscillatorNodeは使い捨て — stop()を呼んだOscillatorNodeは再利用できない。新しい音を鳴らすたびに新しいOscillatorNodeを生成する必要がある。最初はこれを知らずにstart()を2回呼んでエラーになった。
GainNode.gain.value の直接代入はポップノイズの原因 — gain.value = 0と直接代入すると、急激な音量変化によりクリック音が発生する。gain.linearRampToValueAtTime(0, ctx.currentTime + 0.01)のように短いフェードを入れるべき。0.01秒で十分。
メモリリーク: disconnect忘れ — コンポーネントのアンマウント時にノードをdisconnect()しないと、ガベージコレクションされない。特にAnalyserNodeをrequestAnimationFrameで読み続けるMusic Visualizerのようなコンポーネントでは、クリーンアップ処理が必須。
これらのバグの共通点は、「ブラウザ間の挙動差」と「Web Audio APIの設計上の制約」に起因していること。APIドキュメントを読んでいればわかることもあるが、実際にバグとして踏まないと身につかないものも多い。
use-sound(Howler.js)との使い分け
sakimytocomでは、音の再生に2つの異なるアプローチを使い分けている。
use-sound(Howler.js) — 事前に録音されたWAVファイルを再生する。BubbleWrapのポップ音、ボタンのクリック音など、短い効果音に使う。ファイルサイズは小さく(数KB)、再生の遅延もほとんどない。実装はuseSound('/sounds/pop.wav')の1行で終わる。
Web Audio API(OscillatorNode) — リアルタイムに音を合成する。PianoやThereminなど、ユーザーの操作に応じて動的に音を生成する必要がある楽器に使う。WAVファイルでは対応できない——Pianoの88鍵すべての音をWAVで用意するのは非現実的だし、Thereminのような連続的な周波数変化はファイル再生では実現できない。
この使い分けの境界線は明確だ。「再生する音が事前に確定しているか、リアルタイムに生成する必要があるか」。録音音源の再生と、リアルタイム合成は、根本的に異なるプログラミングモデルだ。
Howler.jsの利点は、ブラウザ間の互換性をライブラリが吸収してくれること。Web Audio APIの低レベルな制御は楽器には必須だが、クリック音を鳴らすためだけにOscillatorNodeを生成するのは過剰だ。適材適所。
まとめ
Web Audio APIは「音のCanvas」だ。Canvas APIが「ピクセルを自由に操作する」能力を与えてくれるように、Web Audio APIは「音を自由に生成・加工する」能力を与えてくれる。
そして、音の世界では数学が音色を決める。周波数の比率が音程を、倍音の構成が音色を、エンベロープのカーブが楽器の「触感」を定義する。440 * Math.pow(2, (note - 69) / 12)という一行の数式の背後には、ピタゴラスから始まる音楽理論の歴史がある。
11個の音楽コンポーネントと、18コンポーネントのAudioContextバグ修正を経て、音のプログラミングの基礎は身についたと思う。使った主なリソースは以下の3つだ。
- Web Audio API(Boris Smus): APIの設計思想と全体像
- The Nature of Code(Daniel Shiffman): 振動と波の物理的な直感
- 数学ガール(結城浩): フーリエ変換と周波数の二重性
書籍1冊とリファレンス2冊、そして実装11個。音のプログラミングへの入口としては、これで十分だった。あとは作りたい楽器を1つ選んで、OscillatorNodeを繋いでみるだけだ。最初は電子音しか出ないが、エンベロープを調整し、倍音を重ねていくうちに、「それっぽい音」が生まれる瞬間がある。その瞬間が、音のプログラミングの最大の報酬だ。