← Blog
Web Audio APIMusicBooksSound Design

音のプログラミングで参考にしたもの — Web Audio APIと音楽理論の接点

更新 2026年5月27日16 分で読める

このトピックを深掘りする

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つの実装パターンに分類できる。

  1. 音程のある楽器: Piano, Guitar, Kalimba, Theremin, Music Box, Wind Chimes
  2. リズム楽器: Drum Machine, Metronome
  3. 音の可視化: 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を繋いでみるだけだ。最初は電子音しか出ないが、エンベロープを調整し、倍音を重ねていくうちに、「それっぽい音」が生まれる瞬間がある。その瞬間が、音のプログラミングの最大の報酬だ。