Chunk Buffering
Stretcher はソース音声全体を一括で time-stretch するのではなく、チャンクに分割して逐次変換・再生します。
チャンク分割の利点
一括変換には 3 つの問題があります。
- 初期遅延: 10 分の音声を丸ごと変換すると、再生開始まで数秒〜十数秒かかる。チャンク分割なら最初のチャンクだけ変換すれば再生を開始できる
- メモリ使用量: 変換後の音声は元の音声とは別にメモリを消費する。長い音声を一括変換するとメモリが倍増する。チャンク単位なら必要な範囲だけ保持できる
- テンポ変更への対応: テンポが変わると変換結果はすべて無効になる。一括変換では最初からやり直しだが、チャンク分割なら再生位置周辺のチャンクだけ再変換すればよい
Overlap と Crossfade
隣接するチャンクは端が少し重なるように分割されます。
入力バッファ: ████████████████████████████████████████████ ├─chunk 0─┤ ├──┤ overlap ├─chunk 1──┤ ├──┤ overlap ├─chunk 2──┤この重複区間があることで、チャンクの接続部で crossfade が可能になります。
Equal-Power Crossfade
チャンク接続には equal-power crossfade を使用します。
fadeIn(t) = sin(t × π/2) // 0 → 1fadeOut(t) = cos(t × π/2) // 1 → 0単純な linear crossfade(直線的な fade)では、中間点で両方の信号が半分の振幅になり、合成後の音圧が約 -6dB 下がります。人間の聴覚はこれを「音量の谷」として知覚します。
Equal-power crossfade は三角関数の特性(sin² + cos² = 1)を利用して、どの時点でもエネルギーの合計が一定になるようにします。これにより、チャンク境界でのクリックノイズや音量変動を防ぎます。
優先度スケジューリング
チャンクの変換は再生位置からの距離に基づいて優先度付けされます。
- 前方チャンク(未再生) は優先度が高い — 再生に直結するため
- 後方チャンク(再生済み) は優先度が低いが、ゼロではない — ユーザーが後方へ seek する可能性に備える
前方重視だが後方も備える設計により、通常の順方向再生では途切れず、seek 時にもバッファリングの発生を最小限に抑えられます。
ダブルバッファリング再生
チャンク間をシームレスに接続するために、再生エンジンは 2 つの AudioBufferSourceNode を同時に管理します。
時間軸 →current source: ████████████████████▓▓next source: ▓▓████████████████████ ^^ crossfade 区間現在再生中のチャンク(current)の終了が近づくと、次のチャンク(next)を事前にスケジュールし、重複区間で crossfade します。切り替えが完了すると next が current に昇格します。
1 つのソースだけでは、チャンク終了と次チャンク開始の間にギャップが生じます。2 つ同時に管理することで、再生が途切れない連続的な音声出力を実現しています。
Lookahead には setInterval を使用しています。requestAnimationFrame ではなく setInterval を選んだ理由は、バックグラウンドタブでも動作する必要があるためです。
メモリウィンドウ管理
長い音声(数十分〜数時間)では、全チャンクの変換結果をメモリに保持するとメモリを消費しすぎます。
再生位置を基準にしたスライディングウィンドウで、前方と後方に一定数のチャンクだけを保持し、ウィンドウ外のチャンクは変換結果を解放します。
evicted evicted kept kept CURRENT kept kept evicted × × ←──→ ←─→ ▶ ←─→ ←──→ × 後方保持 前方保持- 前方を多めに保持し、再生の先読みに十分な余裕を持たせる
- 後方も少数保持し、近い位置への seek で再変換を避ける
- 解放されたチャンクが再び必要になった場合は、再変換される