← Blog
Web Audio APITypeScriptSoundInteractive

Web Audio APIでブラウザ楽器を作った — Piano・Guitar・Kalimbaの実装

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

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

Web Audio・サウンドプログラミング

Web Audio APIによる音響合成・楽器実装・物理シミュレーション参考文献

全3本中 2 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

AudioContextの初期化

Web Audio APIAudioContextを起点にすべてのノードが接続される。ブラウザの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の全貌を掴むには道具棚の書籍が役立つ。とくにオシレーターの組み合わせによる音色設計は、書籍で体系的に学ぶほうが効率がいい。