Web Audio APIでブラウザ楽器を作った — Piano・Guitar・Kalimbaの実装
このトピックを深掘りする
Web Audio・サウンドプログラミング
Web Audio APIによる音響合成・楽器実装・物理シミュレーション参考文献
全3本中 2 本目。
次に読む記事
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
AudioContextの初期化
Web Audio APIはAudioContextを起点にすべてのノードが接続される。ブラウザのAutoplay Policyにより、ユーザー操作なしでは音が鳴らない:
let audioCtx: AudioContext | null = null
function getAudioContext(): AudioContext {
if (!audioCtx) {
audioCtx = new AudioContext()
}
if (audioCtx.state === 'suspended') {
audioCtx.resume()
}
return audioCtx
}suspended 状態のチェックは必須。iOSのSafariでは初回タップで resume() を呼ばないと音が出ない。
12平均律の周波数計算
Pianoの各キーの周波数は、A4 = 440Hzを基準に12平均律で計算する:
function noteToFrequency(note: number): number {
// note: MIDIノート番号 (A4 = 69)
return 440 * Math.pow(2, (note - 69) / 12)
}この1行で88鍵すべての周波数が決まる。12で割る=1オクターブ12音の等比分割。数学的にはシンプルだが、この発想に至った人類の音楽理論の歴史は深い。
ADSRエンベロープ
楽器らしい音を作るには、音量の時間変化(エンベロープ)が不可欠:
type ADSR = {
attack: number // 立ち上がり時間(秒)
decay: number // 減衰時間(秒)
sustain: number // 持続レベル(0-1)
release: number // 離鍵後の減衰時間(秒)
}
function applyEnvelope(
gainNode: GainNode,
adsr: ADSR,
startTime: number,
) {
const { attack, decay, sustain, release } = adsr
const g = gainNode.gain
g.setValueAtTime(0, startTime)
g.linearRampToValueAtTime(1, startTime + attack)
g.linearRampToValueAtTime(sustain, startTime + attack + decay)
}
function releaseEnvelope(
gainNode: GainNode,
adsr: ADSR,
releaseTime: number,
) {
const g = gainNode.gain
g.cancelScheduledValues(releaseTime)
g.setValueAtTime(g.value, releaseTime)
g.linearRampToValueAtTime(0, releaseTime + adsr.release)
}Pianoは attack=0.01, decay=0.3, sustain=0.3, release=0.5。Kalimbaは attack=0.005, decay=0.1, sustain=0.0, release=1.5。同じ仕組みでもパラメータで音色が激変する。
楽器ごとの波形選択
OscillatorNodeの type で基本波形を選ぶ:
| 楽器 | 波形 | 理由 |
|---|---|---|
| Piano | triangle | 柔らかいが芯がある |
| Guitar | sawtooth | 倍音が豊富で弦楽器的 |
| Kalimba | sine | 澄んだ金属音 |
| WindChimes | sine + detune | 揺らぎのある金属音 |
function playNote(freq: number, type: OscillatorType, adsr: ADSR) {
const ctx = getAudioContext()
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.type = type
osc.frequency.setValueAtTime(freq, ctx.currentTime)
osc.connect(gain)
gain.connect(ctx.destination)
applyEnvelope(gain, adsr, ctx.currentTime)
osc.start(ctx.currentTime)
return { osc, gain }
}Guitarのストラム表現
Guitarは6弦を同時に鳴らすのではなく、少しずつずらして鳴らす:
function strum(frequencies: number[], direction: 'down' | 'up') {
const ctx = getAudioContext()
const delay = 0.03 // 弦間30msのずれ
const ordered = direction === 'down' ? frequencies : [...frequencies].reverse()
ordered.forEach((freq, i) => {
const startTime = ctx.currentTime + i * delay
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.type = 'sawtooth'
osc.frequency.setValueAtTime(freq, startTime)
osc.connect(gain)
gain.connect(ctx.destination)
gain.gain.setValueAtTime(0, startTime)
gain.gain.linearRampToValueAtTime(0.3, startTime + 0.01)
gain.gain.exponentialRampToValueAtTime(0.001, startTime + 2)
osc.start(startTime)
osc.stop(startTime + 2)
})
}30msのずれが「じゃらーん」という質感を生む。この微小な遅延が人間の耳にはリアルに聞こえる。
WindChimesのランダム性
WindChimesはペンタトニックスケールからランダムに音を選び、ランダムな間隔で鳴らす:
const PENTATONIC = [0, 2, 4, 7, 9] // 半音単位のオフセット
function chime(baseNote: number) {
const offset = PENTATONIC[Math.floor(Math.random() * PENTATONIC.length)]
const octave = Math.floor(Math.random() * 2) + 5
const note = baseNote + offset + octave * 12
playNote(noteToFrequency(note), 'sine', {
attack: 0.005,
decay: 0.1,
sustain: 0,
release: 2 + Math.random(),
})
}ペンタトニックスケールはどの音を同時に鳴らしても不協和音にならない。ランダム生成でも「心地よい」と感じる理由がここにある。
なぜuse-soundと併用するのか
サイト全体のUI効果音(ポップ音、クリック音)にはuse-sound(Howler.js)を使い、楽器コンポーネントにはWeb Audio APIを直接使う。use-soundはWAVファイルの再生に最適化されていて、リアルタイムの周波数制御には向かない。適材適所。
まとめ:Web Audio楽器で使った技術と道具
Web Audio APIの全貌を掴むには道具棚の書籍が役立つ。とくにオシレーターの組み合わせによる音色設計は、書籍で体系的に学ぶほうが効率がいい。