楽器を練習するとき、最低限ほしい道具が 2 つあります。メトロノーム(一定のテンポを刻む)とチューナー(音の高さを合わせる)。どちらもアプリはありますが、広告だらけだったり、片方ずつ別アプリだったり。
MetroTune は、この 2 つを 1 ページにまとめたツールです。しかも面白いことに、両方とも「音の時間を正確に扱う」という点で技術的に地続きでした。メトロノームは正確な間隔で音を鳴らし、チューナーは音の周期を正確に測る。裏表の関係です。
#🥁 メトロノーム:JS タイマーでは刻めない
まず知っておくべき事実:setInterval でメトロノームを作ると、必ずガタつきます。
JavaScript のタイマーは、他の処理に割り込まれて数十ミリ秒ずれます。人間の耳はテンポのゆらぎに敏感なので、このズレは「もたつき」としてはっきり聞こえてしまう。練習の役に立ちません。
解決策は look-ahead スケジューリングです。JS で「いつ鳴らすか」を予約だけして、実際の発音は Web Audio の正確なオーディオクロックに任せます。
export function playClick(
ctx: AudioContext, dest: AudioNode, when: number,
kind: "accent" | "beat" | "sub",
): void {
const osc = ctx.createOscillator
const gain = ctx.createGain
const freq = kind === "accent" ? 1760 : kind === "beat" ? 1320 : 880
const amp = kind === "accent" ? 0.6 : kind === "beat" ? 0.42 : 0.2
osc.type = "square"
osc.frequency.value = freq
gain.gain.setValueAtTime(0.0001, when)
gain.gain.exponentialRampToValueAtTime(amp, when + 0.001)
gain.gain.exponentialRampToValueAtTime(0.0001, when + 0.05)
osc.connect(gain); gain.connect(dest)
osc.start(when); osc.stop(when + 0.06)
}引数の when が肝です。これは「オーディオクロック上の、鳴らすべき正確な時刻」。JS 側は少し先の拍までをまとめて when 付きで予約しておき、発音のタイミングはオーディオエンジンがサンプル精度で守る。これで、何分回してもずれないクリックになります。
クリック音は録音ではなくその場で合成しています。正方波を短く鳴らし、exponentialRampToValueAtTime で一瞬で立ち上げてすぐ減衰させる——これが「カチッ」という硬い音になります。強拍・弱拍・裏拍で周波数と音量を変えて(1760 / 1320 / 880 Hz)、拍の重みを聴き分けられるようにしました。
#🎸 チューナー:音の「周期」を測る
チューナーはメトロノームの逆です。鳴っている音を録って、その基本周波数(音の高さ)を測る。これには**自己相関(autocorrelation)**という手法を使いました。
音は波です。同じ高さの音は、同じ波形が一定周期で繰り返されている。自己相関は「波形を少しずつずらして自分自身と重ね、一番よく一致するズレ幅」を探すことで、その繰り返しの周期を見つけます。
export function autoCorrelate(buf: Float32Array, sampleRate: number): number {
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 c = new Array(n).fill(0)
for (let lag = 0; lag < n; lag++) {
for (let i = 0; i < n - lag; i++) c[lag] += b[i] * b[i + lag] // ずらして重ねる
}
// 最初の谷を越えたあとの、一致が最大になるズレ = 1 周期
// ...
return sampleRate / T0 // 周期 → 周波数
}c[lag] が「lag だけずらして重ねたときの一致度」。これが最大になる lag(= T0)が 1 周期のサンプル数で、サンプルレート ÷ T0 が周波数になります。
工夫が 2 つあります。ひとつは無音の棄却。RMS(音の大きさ)が小さすぎるときは -1 を返して、無音の中からノイズを拾わないようにする。もうひとつは放物線補間です。
const a = (x1 + x3 - 2 * x2) / 2
const bb = (x3 - x1) / 2
if (a) T0 = T0 - bb / (2 * a)自己相関のピークはサンプル単位(飛び飛び)でしか出ませんが、ピーク周辺の 3 点に放物線を当てて頂点を求めると、サンプルの間の周期まで推定できます。これがないと、特に高い音でチューニングがガタつきます。
#🎵 周波数を音名とセントに変換する
周波数(例: 442 Hz)が出たら、それを人間に分かる「音名(A)と、どれだけずれているか(セント)」に翻訳します。
export function freqToNote(freq: number, a4 = 440): PitchReading | null {
if (freq <= 0 || !Number.isFinite(freq)) return null
const midi = 69 + 12 * Math.log2(freq / a4)
const nearest = Math.round(midi)
const cents = Math.round((midi - nearest) * 100)
const note = NOTE_NAMES[((nearest % 12) + 12) % 12]
const octave = Math.floor(nearest / 12) - 1
return { freq, note, octave, cents }
}音の高さは対数で並んでいます(1 オクターブ上がると周波数は 2 倍)。だから log2(freq / a4) で「A4 から何オクターブ分か」を測り、12 を掛けて半音単位の番号(MIDI ノート番号)にします。一番近い整数が音名、そこからのズレがセント(半音の 1/100)。0 セントならジャスト、±なら少し高い/低い。
a4 = 440 を引数にしているのは、基準ピッチを 442 Hz などに変えられるようにするため。オーケストラや古楽では基準が違うことがあるので、ここは可変にしました。
#😓 苦労したところ
#マイク許可と無音
チューナーはマイク入力(getUserMedia)が必要で、ユーザーの許可が要ります。許可されるまでは動かせないし、許可後も「音が鳴っていない間」は周波数が定まらない。無音時に無理やり数値を出すと、画面がチラチラして落ち着きません。RMS で無音を弾き、有効な音が入っているときだけ表示を更新するようにしました。
#倍音に釣られる
弦楽器やリード楽器は、基本周波数の上に倍音(2 倍・3 倍…の周波数)が乗ります。素朴なピーク検出だと、この倍音を基本周波数と誤認して「1 オクターブ上」と表示してしまう。自己相関で「最初の谷を越えたあとの最大ピーク」を取るようにしているのは、この倍音の誤検出を避けるための定石です。
#メトロノームとチューナーの同居
両方が同じ AudioContext を使うので、メトロノームのクリックがチューナーのマイクに回り込むと、自分のクリック音をチューニングし始めてしまう。使う場面が違う(テンポ練習と音合わせ)ので、基本は片方ずつ使う想定にしていますが、こうした「音の道具を 1 ページに同居させる」ときの相互干渉は、作って初めて気づく類の問題でした。
#🔭 今後の拡張
- チューナーの楽器プリセット — ギター/ベース/ウクレレの各弦を目標音として表示
- メトロノームのアクセントパターン編集 — 拍ごとの強弱を自由に組む
- テンポの段階増加 — 練習で少しずつ BPM を上げる自動モード
- 視覚メトロノーム — 音を出せない環境向けの点滅・振り子表示
#💡 このサービスから言えること
「時間を正確に扱う」という一点で、鳴らす側と測る側はつながっている。 メトロノームは正確な時刻に音を出し、チューナーは音の周期を正確に測る。方向は逆でも、必要な精度の源は同じ Web Audio のクロックでした。一見別々の機能が、根っこで同じ技術を使っている——だから 1 つのツールにまとめる意味がありました。
技術的な核心は、やはり look-ahead スケジューリングと自己相関+放物線補間という、音の分野の定石です。どちらも「JS の粒度では足りない精度を、別の仕組みで補う」という発想でできています。ブラウザで音を扱うなら避けて通れない考え方で、環境音の合成でも同じ手を使っています。目に見えないぶん、音の道具は「正確さ」がそのまま使い物になるかを決める、という実感が残りました。
[ ./next_action ]
読んだら、 MetroTune を実際に動かす。
この開発ログは MetroTune をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。