0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

効果音ボタンに「早送り」「ピッチ変更」を付ける:playbackRate・preservesPitch・Web Audio APIのdetuneの使い分け

0
Posted at

memetap.png

はじめに

サウンドボードや効果音ボタンを作っていると、「ちょっと速くしたい」「声を高く(チップマンク風に)したい」「逆に低くして重々しくしたい」という要望がよく出ます。ミーム系の音声は、速度やピッチを変えるだけで別の面白さが生まれるからです。

ブラウザでこれを実現する方法は大きく2つあります。

  1. <audio>(HTMLMediaElement)のplaybackRateとpreservesPitch
  2. 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を作っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?