AI Dev Lab
PitchFlip

PitchFlip ができるまで — soundtouchjs でブラウザ完結のボイスチェンジャーを作る

soundtouchjs (SoundTouch / SimpleFilter / WebAudioBufferSource) を CDN side-load して、 音声のピッチ (±24半音) とテンポ (0.5〜2.0倍) を独立に変更し WAV エンコードして書き出すボイスチェンジャーの設計記録。

·decision改善·stage公開中

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 をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — 文章embeddingsで意味検索をブラウザ内に

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →