はじめに
サウンドボードや効果音ボタンを作っていると、「ちょっと速くしたい」「声を高く(チップマンク風に)したい」「逆に低くして重々しくしたい」という要望がよく出ます。ミーム系の音声は、速度やピッチを変えるだけで別の面白さが生まれるからです。
ブラウザでこれを実現する方法は大きく2つあります。
-
<audio>(HTMLMediaElement)のplaybackRateとpreservesPitch - Web Audio APIの
AudioBufferSourceNodeのplaybackRateとdetune
似ているようで挙動がかなり違うので、この記事では「何が変わるのか」「どちらを使うべきか」を整理し、最後にスライダー付きの効果音ボタンを実装します。
1. <audio>のplaybackRate:基本は「速度だけ」変わる
const audio = new Audio("/audio/clip.mp3");
audio.playbackRate = 1.5; // 1.5倍速
audio.play();
ここで多くの人が驚くのが、<audio>は速度を変えてもデフォルトでピッチを保つという点です。preservesPitchプロパティが既定でtrueになっていて、ブラウザが音程を補正します。動画の倍速視聴で声が高くならないのはこのためです。
チップマンク風に「速くして高く」したい場合は、明示的に切ります。
audio.preservesPitch = false;
// 古いブラウザ向けのベンダープレフィックス
audio.mozPreservesPitch = false;
audio.webkitPreservesPitch = false;
audio.playbackRate = 1.5;
注意点:
- 再生速度には上限・下限があります。 極端な値(たとえば0.05倍や20倍)は、ブラウザによって無音になったり例外になったりします。UIでは0.5〜2.0倍程度に制限しておくのが無難です。
-
逆再生(負の値)は基本的に使えません。
<audio>で負のplaybackRateをサポートしているブラウザはほぼないと考えてください。 -
ピッチだけを変える(速度はそのまま)ことはできません。
<audio>にはその機能がありません。
2. Web Audio API:速度とピッチが「一緒に」変わる
Web Audio APIでは、音声をAudioBufferにデコードしてからAudioBufferSourceNodeで鳴らします。
const ctx = new AudioContext();
async function loadBuffer(url) {
const res = await fetch(url);
const arrayBuffer = await res.arrayBuffer();
return ctx.decodeAudioData(arrayBuffer);
}
function play(buffer, { rate = 1, detune = 0 } = {}) {
const src = ctx.createBufferSource();
src.buffer = buffer;
src.playbackRate.value = rate; // 速度とピッチが両方変わる
src.detune.value = detune; // セント単位(100セント = 半音)
src.connect(ctx.destination);
src.start();
return src;
}
こちらにはpreservesPitchはありません。playbackRateを上げると、速度もピッチも一緒に上がります(レコードの回転数を上げるイメージ)。
detuneはセント単位で、100で半音、1200で1オクターブです。実はこれも「再生速度を変えてピッチを変える」仕組みなので、detune: 1200はplaybackRate: 2とほぼ同じ結果(2倍速・1オクターブ上)になります。両方指定すると掛け合わされます。
「半音いくつ上げる」というUIを作るならdetune、「何倍速」というUIならplaybackRate、と使い分けると直感的です。
3. 逆再生はバッファを反転して作る
AudioBufferSourceNodeも負のplaybackRateは実用的にサポートされていないため、逆再生はバッファのデータを反転したコピーを作ります。
function reversed(buffer) {
const copy = new AudioBuffer({
length: buffer.length,
numberOfChannels: buffer.numberOfChannels,
sampleRate: buffer.sampleRate,
});
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
const data = buffer.getChannelData(ch).slice(); // 元を壊さないようコピー
data.reverse();
copy.copyToChannel(data, ch);
}
return copy;
}
短い効果音ならコストはほぼゼロです。反転済みバッファもキャッシュしておきましょう。
4. どちらを使うべきか
| やりたいこと | <audio> |
Web Audio API |
|---|---|---|
| 倍速だけ(声の高さは保つ) | ◎ preservesPitch = true(既定) |
✕(標準では不可) |
| 速く+高く(チップマンク) | ○ preservesPitch = false
|
◎ playbackRate
|
| 半音単位でピッチ変更 | ✕ | ○ detune(速度も変わる) |
| 逆再生 | ✕ | ○ バッファ反転 |
| 同じ音の連打・重ね再生 | △ 要素を複数用意 | ◎ ノードを毎回作る |
| 実装の手軽さ | ◎ | ○ |
ミーム系の効果音ボタンなら、「速く・高く」「遅く・低く」「逆再生」ができて連打にも強いWeb Audio API版が向いています。長めのセリフを倍速で聞かせたいだけなら<audio>で十分です。
5. スライダー付き効果音ボタンの実装
Web Audio API版で、速度スライダーと逆再生トグルを付けます。
<button id="play">再生</button>
<label>速度 <input id="rate" type="range" min="0.5" max="2" step="0.05" value="1" /></label>
<output id="rateLabel">1.00x</output>
<label><input id="rev" type="checkbox" /> 逆再生</label>
const cache = new Map(); // url -> { normal, reversed }
let current = null;
async function getBuffers(url) {
if (!cache.has(url)) {
const normal = await loadBuffer(url);
cache.set(url, { normal, reversed: reversed(normal) });
}
return cache.get(url);
}
document.getElementById("rate").addEventListener("input", (e) => {
document.getElementById("rateLabel").textContent = `${Number(e.target.value).toFixed(2)}x`;
});
document.getElementById("play").addEventListener("click", async () => {
// iOSなどではユーザー操作の中でresumeが必要
if (ctx.state === "suspended") await ctx.resume();
const { normal, reversed: rev } = await getBuffers("/audio/clip.mp3");
const rate = Number(document.getElementById("rate").value);
const useRev = document.getElementById("rev").checked;
current?.stop(); // 連打時は前の音を止める(重ねたいなら消す)
current = play(useRev ? rev : normal, { rate });
});
ポイント:
-
デコードは1回だけ。 毎回
decodeAudioDataすると、タップから音が出るまでが遅くなります。 -
AudioContextはユーザー操作の中でresume()。 モバイルの自動再生制限対策です。 -
設定は
localStorageに保存しておくと、「いつも1.3倍で使う」ような人に親切です。 -
ダウンロード機能を付ける場合、速度変更後の音を書き出すには
OfflineAudioContextでレンダリングし直す必要があります。元ファイルをそのまま渡すと、変更前の音になる点に注意してください。
試すならこういう素材で
速度・ピッチ変更が一番映えるのは、短い笑い声や声ネタです。動作確認用の素材を探すなら、ブラウザでそのまま再生・ダウンロードできるMeme Tapのファニーサウンド集のような短いクリップがちょうどいいです。数秒の声ネタを0.7倍と1.6倍で聞き比べると、preservesPitchの有無の違いがすぐ分かります(上のコードは私が一般化したサンプルで、特定サイトの実装ではありません)。
まとめ
-
<audio>のplaybackRateは既定でピッチを保つ。チップマンク風にするならpreservesPitch = false - Web Audio APIの
playbackRateとdetuneは速度とピッチが一緒に変わる(detuneはセント単位) - 逆再生はバッファを反転したコピーで作る
- 効果音ボタンなら、デコード済みバッファをキャッシュしたWeb Audio API版が扱いやすい
数行の違いで、同じ効果音がまったく別のネタになります。ぜひ遊んでみてください。
Abid Ali:パキスタンのフルスタックWeb開発者。ブラウザで使える無料のミームサウンドボードMeme Tapを作っています。
