PitchFlip ができるまで — soundtouchjs でブラウザ完結のボイスチェンジャーを作る
PitchFlip は、 声のピッチとテンポを アップロード不要・サインアップ不要・無料・ブラウザだけで 変えられる Webサービスです。 soundtouchjs を CDN side-load して WebAudio API で動かす構成。 メディア処理ラボ 13本柱に続く 14本目、 新カテゴリ 「音声加工」 を追加しました。
なぜこの形にしたか
「ボイスチェンジャー ブラウザ」「声 加工 無料」 の検索需要は SNS / YouTuber / 言語学習で日常的に強い。 既存サービスは ほぼ全部 スマホアプリ DL 必須 / サーバーアップロード前提。 soundtouchjs は SoundTouch (C++) の純 JS ポートで、 ピッチとテンポを独立に制御できる。 ブラウザ完結で出す素地が揃っています。
visual direction
シンセウェーブ・ネオン (deep purple + hot magenta + cyan)。 既存 13本柱と完全別軸の neon dark warm。
実装の見どころ
1. CDN side-load + Turbopack バイパス
soundtouchjs は ESM 構文だが dist/soundtouch.js を main にしている。 Next.js Turbopack の chunk loader を介すと過去踏んだのと同じクラッシュを発火させ得るので、 最初から CDN ESM の dynamic import で side-load:
const mod = await import(
"https://cdn.jsdelivr.net/npm/soundtouchjs@0.3.0/dist/soundtouch.js"
)
const { SoundTouch, SimpleFilter, WebAudioBufferSource } = mod
2. ピッチ + テンポを独立に変える
audio.playbackRate = 1.5 系のナイーブな実装はピッチとテンポを同時に変えてしまう。 SoundTouch は WSOLA 系のアルゴリズムで、 ピッチを変えずにテンポだけ または テンポを変えずにピッチだけ が変えられる:
const st = new SoundTouch()
st.pitch = Math.pow(2, semitones / 12) // 1.0 = 原音, 2.0 = 1オクターブ上
st.tempo = tempoFactor // 1.0 = 原速, 1.5 = 50%速い
3. Offline レンダリング + WAV エンコード
ファイル全体を offline 処理して WAV で書き出す経路を v1 では採用。 リアルタイムプレビューより 「変えた音声を SNS / 動画編集に貼りたい」 動線優先:
const blockSize = 4096
const sampleBuffer = new Float32Array(blockSize * 2)
const outSamples: number[] = []
while (true) {
const extracted = filter.extract(sampleBuffer, blockSize)
if (extracted === 0) break
for (let i = 0; i < extracted; i++) outSamples.push(sampleBuffer[i * 2])
if (outSamples.length % (blockSize * 16) === 0) {
await new Promise((r) => setTimeout(r, 0)) // UI yield
}
}
WAV は 44byte の RIFF/WAVE ヘッダ + 16-bit PCM mono で素朴に組みます。
4. プリセット 6本
SNS で需要の高い 6パターンをプリセット化:
- そのまま / チップ (+7st) / ダーク (-6st) / モンスター (-12st, 0.85x) / lofi (-2st, 0.8x) / 高速 (1.5x, ピッチ維持)
5. マイク録音
getUserMedia + MediaRecorder でブラウザ内録音 → 即加工 → DL。 メタデータも残らない。
6. SEO §7.1
- 主用語: 「ボイスチェンジャー」「声 加工 ブラウザ」「ピッチ変更 オンライン」
- 補助: 「soundtouchjs」「WebAudio」「アップロード不要」「無料」
7. §8.5.2 invariants
result panel に data-output-bytes / data-mime / data-output-sec / data-input-sec を出し、 e2e で mime === audio/wav / outputSec > 0 / outputBytes > 4096 を assert。
苦労したところ
- soundtouchjs の TS 型なし — 必要なクラスのシグネチャだけ手書きで定義
- mono 強制 — ステレオはタイミングが微妙にズレ得るので v1 は down-mix mono に
- 長尺ファイル — 1分以上の音声でも UI が固まらないようブロック毎に
setTimeout(0)yield
今後の拡張
- ステレオ対応 / mp3 / opus 出力
- 共有 URL (
?pitch=&tempo=) - リアルタイム preview
- voice-scribe との Process Chain (加工 → 文字起こし)
- リバーブ / EQ プリセット
ラボの 14本柱
voice-scribe / clip-cast / bg-snap / text-pluck / pdf-anvil / pixel-lift / pic-flip / mind-cell / beam-drop / word-warp / exif-peel / ascii-bake / py-pad / pitch-flip
カテゴリ 8つ目 「音声加工」 を追加。
[ ./next_action ]
読んだら、 PitchFlip を実際に動かす。
この開発ログは PitchFlip をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。