手軽屋
ツール一覧

Web Audio APIで音程検出する仕組み: AnalyserNode・自己相関・FFT・セント計算

MDN Web Docs仕様に基づき、ブラウザだけで動くチューナーの内部構造を実装観点で解説します。

音声処理の全体パイプライン

getUserMedia({audio: true})
  → MediaStreamSource (AudioContext内のノード)
  → AnalyserNode (FFTサイズ4096・時間軸バッファ取得)
  → getFloatTimeDomainData(Float32Array)
  → 自己相関アルゴリズム (周期検出)
  → 放物線補間 (山の頂点を高精度推定)
  → 周波数(Hz) → MIDIノート番号 → 音名 + セント差

AnalyserNodeの主要仕様

MDNのAnalyserNodeページに基づくと、主要プロパティは以下:

チューナー用途では時間軸データを使うためgetFloatTimeDomainDataが中心。FFTスペクトル(getFloatFrequencyData)は倍音と基本音の判別が苦手で、楽器の音程検出には向きません。

自己相関による周期検出

自己相関(autocorrelation)は、波形を時間方向にシフトした自分自身と掛け合わせて、ピークが出る位置を見つける手法です。基本周波数の周期分シフトしたとき相関値が最大になります。

function detectPitch(buf, sampleRate) {
  const SIZE = buf.length;
  let rms = 0;
  for (let i = 0; i < SIZE; i++) rms += buf[i] * buf[i];
  rms = Math.sqrt(rms / SIZE);
  if (rms < 0.01) return -1; // 無音判定

  const MAX_SAMPLES = Math.floor(SIZE / 2);
  let bestOffset = -1, bestCorrelation = 0;
  for (let offset = 8; offset < MAX_SAMPLES; offset++) {
    let correlation = 0;
    for (let i = 0; i < MAX_SAMPLES; i++) {
      correlation += buf[i] * buf[i + offset];
    }
    if (correlation > bestCorrelation) {
      bestCorrelation = correlation;
      bestOffset = offset;
    }
  }
  return sampleRate / bestOffset;
}

サンプリングレートが48000Hzなら、bestOffset=120のとき検出周波数は48000/120=400Hz、つまり概ねG4(392Hz)〜G♯4(415Hz)の間と判定できます。

放物線補間で精度を上げる

自己相関ピークはサンプル単位の解像度しか持たないため、48000Hz/120サンプル=400Hzのような離散値しか取れません。 隣接サンプルの相関値y0, y1, y2を放物線フィッティングし、頂点位置x = (y0 - y2) / (2 × (y0 - 2y1 + y2))を使って小数オフセットを求めると、精度が10倍以上向上しセント表示に必要な分解能が得られます。

MIDIノート番号とセント計算

検出した周波数 f を音名に変換する標準式:

const ref = 440; // A4基準
const midiFloat = 69 + 12 * Math.log2(f / ref);
const midi = Math.round(midiFloat);
const noteFreq = ref * Math.pow(2, (midi - 69) / 12);
const cents = Math.round(1200 * Math.log2(f / noteFreq));
const noteIndex = midi % 12; // 0=C, 1=C#, ..., 9=A, ..., 11=B
const octave = Math.floor(midi / 12) - 1;

MIDI 69はA4=440Hz。1セントは半音(100セント)の1/100で、1200セント=1オクターブ。これは等分平均律(12-TET)の標準式で、世界中のチューナーで使われています。

プライバシー: なぜサーバ送信が不要か

上記のパイプラインは全てブラウザ内のJavaScriptで完結します。MediaStreamSourceで取り込んだマイクデータはAudioContextのオーディオグラフ内でAnalyserNodeに流れ、波形データがJS側のFloat32Arrayにコピーされて自己相関処理されるだけ。 ネットワーク送信のためのfetchWebSocket呼び出しは一切ありません。

関連