リアルタイムボイスチェンジャー
RealtimeVoiceChanger は、libsonare のプリセット式ライブ音声チェーンです。マイク入力、リアルタイムモニタリング、または音声を繰り返しブロックで処理し、呼び出し間で DSP 状態を保ちたい場面で使います。
半音値とフォルマント係数で 1 回だけオフライン変換するなら voiceChange(...) を使います。ライブ処理やチャンク処理のプリセットチェーンが必要なら、このページを読んでください。
ブロック・サンプルレート・DSP 状態
リアルタイム音声は、小さなブロック(サンプルのまとまり)を次々に処理します。ブラウザでは AudioWorklet が 128 サンプルの固定レンダークォンタムを 1 回ずつ渡します。サンプルレート(例: 48000)は 1 秒が何サンプルでできているかです。DSP 状態は、エフェクトがブロック間で持ち越すメモリ(リバーブの残響、コンプレッサーの現在のゲインなど)です。だからこそボイスチェンジャーは 1 回限りの関数ではなく、再利用するオブジェクトになっています。
なぜ関数ではなくクラスなのか
ライブ音声処理には「前のブロックから続く状態」があります。ゲート、コンプレッサー、リバーブ、滑らかなピッチ/フォルマント変化は、前後のブロックをまたいで動く必要があります。RealtimeVoiceChanger はその状態を保持するため、ブロックごとに作り直すと音が不自然になり、準備コストも増えます。
このページで身につくこと
このページを読むと、次のことを判断・実装できるようになります。
- 単純なオフライン
voiceChange(...)ではなくRealtimeVoiceChangerを選ぶべき場面を判断できる。 - ブロック処理のためにクラスを準備し、WASM ハンドルを正しく解放できる。
- 組み込みプリセット JSON を確認し、ユーザー作成プリセットを受け入れる前に検証できる。
- ブラウザ、Python、Node ネイティブ、CLI のどの入口を使うべきか選べる。
いつ使うか
| 作りたいもの | 使う入口 | 入力 | 出力 |
|---|---|---|---|
| ブラウザでマイク音声をライブ加工する | RealtimeVoiceChanger | AudioWorklet などから届く短い Float32Array ブロック | 同じ長さの加工済みブロック |
| ブラウザで 1 つの録音クリップを半音値とフォルマント値だけで変換する | voiceChange(...) | デコード済みモノラル Float32Array 全体 | 加工済み Float32Array |
| Python でファイルや配列全体をプリセットチェーンに通す | voice_change_realtime(...) | モノラル配列と sample_rate | 加工済み配列 |
| Node ネイティブでサーバー/デスクトップ側の一括処理を行う | voiceChangeRealtime(...) | モノラル配列とサンプルレート | 加工済み配列 |
| ターミナルで WAV/MP3 をプリセット加工して書き出す | sonare voice-change --preset ... | 音声ファイル | 書き出しファイル |
| プリセット設定を UI で表示・保存・検証する | realtimeVoiceChangerPresetJson(...) と検証 API | プリセット ID または JSON | 設定 JSON と検証結果 |
実装の全体像
ブラウザのライブ処理では、最初に init() と prepare(sampleRate, maxBlockSize, channels) を済ませ、その後は届いた音声ブロックごとに processMono(...) または processMonoInto(...) を呼びます。RealtimeVoiceChanger は内部状態を持つため、ブロックごとに作り直さず、ストリームが続く間は同じインスタンスを再利用します。
| 実装で決めること | 目安 |
|---|---|
sampleRate | AudioContext.sampleRate や入力ファイルの実サンプルレートを使います。推測で 44100 / 48000 を固定しないでください。 |
maxBlockSize | processMono(...) に渡す最大ブロック長です。AudioWorklet の標準レンダークォンタムなら 128 が出発点です。 |
channels | このページの例はモノラルなので 1 です。インターリーブ入力なら processInterleaved(...) を使います。 |
| 出力バッファ | 単純な実装は processMono(...)、AudioWorklet のホットパスでは事前確保した出力へ processMonoInto(...) または WASM ヒープバッファ経路を使います。 |
| 後始末 | Vue / React コンポーネントの unmount、worklet 停止、録音停止時に delete() を呼びます。ハンドルは一度解放され、繰り返しても何も起きません。 |
「インターリーブ」とは、複数チャンネルを 1 つの配列に交互に並べたもの(L, R, L, R…)で、「モノラル」は 1 チャンネルです。前者には processInterleaved(...)、後者には processMono(...) を使います。
ライブ処理と一括レンダーを混同しない
RealtimeVoiceChanger は「短いブロックを順番に処理し、ゲートやコンプの状態を保つ」ためのクラスです。Python / CLI の例は同じプリセットチェーンをファイル全体へ適用する入口で、ブラウザの AudioWorklet コールバック内でそのまま使うコードではありません。
信号チェーン
リアルタイムチェーンは単なるピッチシフターではありません。組み込みプリセットは次の段を組み合わせます。
上のデモはリチューン段(ピッチシフト)だけを取り出して、その効果を単体で聞けるようにしたものです。実際のプリセットでは、ピッチシフトの前にクリーンアップ用ハイパスとノイズゲートが動き、その後に EQ・ダイナミクス・空間処理が続きます。全体の順序は下の図のとおりです。
| 段 | 役割 |
|---|---|
| ハイパス | 低域のランブルや DC を最初に取り除くクリーンアップ用プリフィルター(eq.highpassHz) |
| ゲート | フレーズの合間の小さな部屋ノイズやマイクノイズを抑える |
| リチューン | プリセットが指定する固定の半音値だけ、グレインのオーバーラップアドで信号全体のピッチを上下させる(例: 声を高く/低く)。チェーンの中のピッチシフト段で、シフト量はちょうど 2^(semitones / 12) になる。ピッチ検出も音階へのスナップも行わないため、それが必要な場合は編集 DSP のオフラインのピッチ補正を使う |
| フォルマント | 音高とは独立に、声の大きさやキャラクターの印象を変える |
| EQ | ボディ・プレゼンス・エアの音色調整(eq.bodyDb/presenceDb/airDb) |
| コンプレッサー | ブロックをまたいでレベルを安定させる |
| ディエッサー | 刺さりやすい歯擦音を抑える |
| リバーブ | 空間を足す、または整える |
| リミッター | ピークを捕まえる。ドライ/ウェットのミックス後に 4 倍オーバーサンプリングの True Peak リミッターが続く(既定で有効) |
TIP
eq ブロックはチェーンの両端を設定します。highpassHz は先頭のクリーンアップ用ハイパスで、bodyDb/presenceDb/airDb はフォルマント処理の後に置かれる音色シェルフです。
ボイスチェーンの用語をまとめて把握
- ハイパス: 信号の最初で低域のランブルや DC を取り除くクリーンアップフィルターです。
- ゲート: 信号がしきい値を下回ると無音にし、フレーズの合間の小さなマイク/部屋ノイズを取り除きます。
- リチューン: 信号全体を一定の音程だけ高く/低くピッチシフトします。音程を変えずに声のキャラクターを変えるフォルマントとは別物です。あくまで一定量のピッチシフトであり、自動ピッチ補正ではないので、歌った音を聴き取る仕組みは入っていません。
- フォルマント: 声を大きく/小さく、男性的/女性的に聞かせる共鳴です。これをずらすと、音程を変えずに声のキャラクターが変わります。詳しくは声とフォルマントを参照してください。
- コンプレッサー: 大きい部分と小さい部分を自動でならし、レベルを安定させます。
- ディエッサー: 耳に刺さる「s」「sh」の音(歯擦音)を抑えます。
- リミッター: チェーン末尾でピークがクリップしないよう止める安全装置です。ここで使うのは True Peak リミッターで、再生時に波形を復元したときサンプルとサンプルの間に現れるピーク(サンプル間ピーク、ISP)まで見て抑えます。
標準プリセット ID には neutral-monitor、bright-idol、soft-whisper、deep-narrator、robot-mascot、dark-villain があります。これらは出発点であり、ジャンルや話者属性のラベルとして固定的に扱うものではありません。
どの言語でも同じ流れ
どのバインディングも同じ流れで動きます。チェンジャーを生成し、サンプルレートとブロックサイズで準備し、ブロックごとに processMono(...) を呼び、setConfig(...) でプリセットをライブ差し替えし、latencySamples() を読むだけです。違うのはコンストラクタの形と後始末の呼び出しだけで、それを下のタブに並べています。
import {
init,
RealtimeVoiceChanger,
realtimeVoiceChangerPresetNames,
} from '@libraz/libsonare';
await init();
const changer = new RealtimeVoiceChanger('bright-idol');
changer.prepare(48000, 128, 1);
try {
const out = changer.processMono(inputBlock);
changer.setConfig('soft-whisper');
console.log(realtimeVoiceChangerPresetNames(), changer.latencySamples(), out);
} finally {
changer.delete(); // WASM ハンドルの解放
}import libsonare as sonare
print(sonare.voice_character_preset_id(1))
preset_config = sonare.realtime_voice_changer_preset_config("bright-idol")
with sonare.RealtimeVoiceChanger(48000, preset="bright-idol", max_block_size=128) as changer:
out = changer.process_mono(input_block)
changer.set_config("soft-whisper")
print(sonare.realtime_voice_changer_preset_names(), preset_config, changer.latency_samples())
# 同じプリセットチェーンで配列全体をレンダーする:
processed = sonare.voice_change_realtime(vocal, sample_rate=48000, preset="soft-whisper")import {
RealtimeVoiceChanger,
realtimeVoiceChangerPresetNames,
voiceChangeRealtime,
} from '@libraz/libsonare-native';
const changer = new RealtimeVoiceChanger({
sampleRate: 48000,
maxBlockSize: 128,
channels: 1,
preset: 'bright-idol',
});
try {
const blockOut = changer.processMono(inputBlock);
changer.setConfig('soft-whisper');
// 同じプリセットチェーンで配列全体をレンダーする:
const rendered = voiceChangeRealtime(vocal, 48000, 'soft-whisper');
console.log(realtimeVoiceChangerPresetNames(), changer.latencySamples(), blockOut, rendered);
} finally {
changer.destroy(); // ネイティブハンドルの解放(WASM は delete())
}# sonare voice-change はファイル全体をプリセットチェーンに通してレンダーします。
# ブロック単位のリアルタイムループではありません。
sonare voice-presets --json
sonare voice-change vocal.wav --preset soft-whisper -o rendered.wav後始末はバインディングごとに違う
生成と処理の流れは共通ですが、ネイティブハンドルの解放方法は言語ごとに異なります。
- ブラウザ / WASM:
delete()(別名destroy())を呼ぶか(finally内、コンポーネントの unmount 時、worklet 停止時など)、usingで宣言します。繰り返し呼んでも問題ありません。 - Node ネイティブ:
destroy()を呼びます。using(Node 22 以降)を使えば自動で解放できます。 - Python:
withブロックを抜けるとハンドルを解放します。withを使わない場合はclose()を呼びます。
AudioWorklet 形式のループでは、ブラウザ / WASM で説明している WASM ヒープ上のリアルタイムバッファを使います。ブラウザ例の素の processMono(...) と違い、レンダークォンタムごとに新しい出力配列を確保せずに済みます。パッケージには出来合いのプロセッサ(@libraz/libsonare/worklet の registerSonareRealtimeVoiceChangerWorkletProcessor)も同梱されています。AudioWorklet 内で使うを参照してください。
CLI のモード
sonare voice-change には 2 つのモードがあります。
| モード | オプション |
|---|---|
| 単純なピッチ/フォルマント変換 | --pitch-semitones、--formant-factor |
| リアルタイムプリセットチェーンでのレンダリング | --preset、--preset-json、--preset-pack、--set PATH=VALUE |
リアルタイムプリセット系のオプションを渡した場合、コマンドはプリセットチェーンを使います。単純なピッチ/フォルマント指定との併用は無効パラメータとして拒否されます。--preset-pack FILE にはエントリ選択用の --preset ID が必須で、先頭エントリへのフォールバックはありません。セレクターの規則とコマンド表は CLI リファレンス を参照してください。
プリセット JSON
プリセット JSON は、音声チェーン設定を確認・保存・検証したいときに使います。
import {
realtimeVoiceChangerPresetJson,
validateRealtimeVoiceChangerPresetJson,
} from '@libraz/libsonare';
const json = realtimeVoiceChangerPresetJson('bright-idol');
const validation = validateRealtimeVoiceChangerPresetJson(json);
if (!validation.ok) {
throw new Error(validation.error);
}現在の組み込みプリセット JSON はスキーマバージョン 1 を使います。C のエントリーポイントが受け取るフラットな構造体(POD=plain old data)のネイティブ設定 ABI は、これとは別に管理されています。FFI(外部関数インターフェース)やネイティブ境界をまたぐ場合は voiceChangerAbiVersion() で確認してください。2 つの JSON Schema は npm パッケージに同梱されているので、ホスト側はオフラインで文書を検証できます。
import schema from '@libraz/libsonare/schemas/realtime-voice-changer-preset.schema.json';正規のプリセット ID や解決済みのフラットなネイティブ設定だけが必要なら、JSON を往復せず voiceCharacterPresetId(...) と realtimeVoiceChangerPresetConfig(...) を使います。WASM の voiceCharacterPresetId(...) は正規 ID または整数の序数を受け取り、未知の数値序数は null を返し、未知の文字列 ID は例外を投げます。realtimeVoiceChangerPresetConfig(...) は無効な序数で例外を投げます。Python では同じネイティブ設定取得経路を realtime_voice_changer_preset_config(...) として公開しています。
プリセット文書は完全である必要があります
有効な文書には必須のスキーマメタデータと、完全な dsp または macros のどちらか 1 つが必要です。部分的な文書は、無関係な既定値で黙って埋められるのではなく拒否され、deesser.ratio も必須です。プリセットを手で書く場合は、変更したいフィールドだけを書くのではなく realtimeVoiceChangerPresetJson('neutral-monitor') を出発点にして編集してください。configJson() で設定を読み出し、編集して setConfig(...) で書き戻す往復は安全です。トップレベルや各セクション内の未知のキーも拒否されます。
macros: 省略記法のセクション
「この声をもっと明るく」というだけのために dsp セクション全体を手で書くのは大がかりです。プリセットは代わりに macros オブジェクトを持てます。7 つの値が通常の DSP 設定にマッピングされます。
| マクロ | 範囲 | 動かす対象 |
|---|---|---|
pitch | −24 〜 24 | リチューン(半音) |
formant | 0.55 〜 1.65 | フォルマントのスケール係数 |
brightness | 0 〜 1 | フォルマントの明るさ、EQ のプレゼンスとエア |
space | 0 〜 1 | リバーブミックス(チェーンの上限 0.45 まで) |
intensity | 0 〜 1 | コンプレッサーのレシオ |
noiseControl | 0 〜 1 | ゲートとノイズ処理 |
sibilance | 0 〜 1 | ディエッサーの量 |
マクロは入力専用です。共有パーサーがコントロールスレッド上で通常の DSP 設定へ展開するため、正規化された出力には現れません。設定を読み戻すと、書いたマクロではなく展開後の dsp セクションが返ります。1 つの文書が持てるのはどちらか一方だけで、dsp と macros の両方を書いた文書は不正なパラメータとして拒否されます。したがって両者の優先順位という概念はありません。
0〜1 のマクロは、そのままの値が書き込まれるのではなく対象の有効範囲へマッピングされます。macros.space を 1.0 にするとリバーブミックスの上限 0.45 に到達します。範囲外の 1.0 がパラメータに書き込まれるわけではありません。
フォルマントモード
リチューンはグレインをリサンプルしてピッチを動かすため、すべてのフォルマントも同じ比率で動きます。+12 半音なら、フォルマント周波数も 2 倍になります(いわゆる「チップマンク」効果)。背景は声とフォルマントを参照してください。
- 既定の相対モード(
formantMode: 'relative')では、フォルマント段はリチューンの後に動くため、formantFactorはピッチシフトがすでに起こした移動量に掛け算されます。 - 絶対モード(
formantMode: 'absolute')では、まずフォルマントをピッチ比の逆数で先にワープしておくので、シフト量にかかわらずformantFactor: 1なら元の声の大きさのままです。ワープはリチューンのウェット分(retuneMix)にだけ追従します。
下の図は、1 つの声でこの 3 つの場合を示したものです。どの段にも入力の第 1・第 2 フォルマントの位置を示しています。
- 倍音(櫛)
- スペクトル包絡
- 入力のフォルマント位置
プリセット JSON では dsp.formant.mode にあたります。
'relative'(既定) | 'absolute' | |
|---|---|---|
formantFactor の意味 | リチューンの後に掛かるワープ | 入力に対するフォルマントの位置 |
formantAmount | フォルマント移動量を調整する | 無視される |
| レイテンシ | latencySamples() の報告値 | 解析 1 フレーム分増える(48 kHz で 1024 サンプル、実測) |
| 届く範囲 | formantFactor のパラメータ範囲内 | formantFactor / 2^(retuneSemitones / 12) が [0.55, 1.65] 内(+12 半音では事前ワープだけで 0.5 になるため、formantFactor は 1.1 以上が必要) |
モードはチェンジャーの作成・prepare 時に固定されます。JSON ではキーを省略でき、省略時は relative です(フラット POD では必須)。別のモードを持つ設定は拒否され(SonareError、"formant mode is fixed at prepare()")、届かない範囲の絶対指定も拒否されます。後者のエラーには、そのピッチシフトで届く formantFactor の範囲と、その範囲を絞り込んでいる formantFactor 自体の境界が示されます(+12 半音では、届く範囲は [1.1, 1.65]、ワープ自体は [1.1, 3.3] に届きますが、formantFactor の [0.55, 1.65] で制限されます)。オートメーションも範囲外の値をキー名つきで拒否し、直前の設定がそのまま残ります。
import { init, RealtimeVoiceChanger, realtimeVoiceChangerPresetConfig } from '@libraz/libsonare';
await init();
const cfg = realtimeVoiceChangerPresetConfig('neutral-monitor');
const changer = new RealtimeVoiceChanger({
...cfg,
formantMode: 'absolute',
retuneSemitones: 5,
formantFactor: 1, // +5 半音、フォルマントは動かさない
});
try {
changer.prepare(48000, 128, 1);
console.log(changer.latencySamples()); // relative モードより解析 1 フレーム分大きい
} finally {
changer.delete();
}realtimeVoiceChangerPresetConfig(...) が返し、コンストラクタ・setConfig(...)・setPodConfig(...) が受け取るフラットな POD 設定は、formantMode を含むすべてのフィールドが必須で、省略すると TypeError になります。C では sonare_realtime_voice_changer_config_default() から始めてください。struct_version は 0 と 1 のどちらでも現在の 152 バイトのレイアウトになります。SONARE_VOICE_CHANGER_ABI_VERSION は 3 なので、C の呼び出し側は現在のヘッダーで再ビルドしてください。
ミキサーインサートとして
同じチェーンは、モノラル/ステレオのストリップ上でリアルタイムインサート voice.changer(カタログのカテゴリは voice)としても使えます。プリセットのフラットキー(retuneSemitones、formantFactor、compressorThresholdDb など)で 37 個のパラメータを公開し、うち 33 個はオートメーションできます。formantMode、retuneGrainSize、limiterEnableIspLimiter、reverbSeed は構築時のみ指定できます。範囲外の値は、構築時もオートメーションでもキー名つきで拒否され、直前の設定が残ります。ほかのインサートと同様にレイテンシ、リバーブテール、ゲインリダクション(コンプレッサーのメイクアップは含まない)、非有限値の破棄数を報告し、masteringProcess でも使えます。ディスクリプターのフィールドはエフェクトインサートを参照してください。
実用上の注意
リアルタイム音声処理は状態を持ちます。同じチェンジャーをブロック間で再利用し、ブロックサイズは prepare(...) した最大値以内に保ち、コンポーネントやストリーム停止時にはハンドルを解放してください。
大きなピッチ、フォルマント、空間処理の変更はサウンドデザインには有効ですが、自然さは下がります。自然なモニタリングではプリセット編集を控えめにし、latencySamples() でレイテンシを一度確認しておいてください。
ここでいう「レイテンシ」とは
レイテンシは、音が入ってから加工後の音が出てくるまでの遅れで、チェーンが行う解析によって生じます。latencySamples() はこれをサンプル数で報告するので、サンプルレートで割れば秒になります。
遅延は 2 つの部分からなります。
| 部分 | 含まれるもの | 変わる条件 |
|---|---|---|
latencySamples()(報告値) | ドライ経路とチェーン全体の位置合わせ。リチューン段の解析窓と、True Peak(サンプル間ピーク、ISP)リミッターが有効な場合はその遅延 | prepare(...) とグレインサイズだけ。ウェットやリチューンのミックスを動かしても変わらない |
| ウェット経路の追加遅延 | グレインを速く(上方シフト)または遅く(下方シフト)再生すると、読み出し位置が書き込み位置に対してどれだけ遅れるかが変わります。その差が上限に達するとシフターが同期をやり直すため、ウェット経路は周期のどこにいるかに応じて 0〜2 グレイン遅れます | シフト量。シフトなしでは 0、それ以外では最大 2 グレイン |
報告値は prepare(...) の後に一度読んで補正すれば済み、コントロールを動かすたびに読み直す必要はありません。グレインが大きいほど 1 ステップで解析する音が増え、どちらの遅延も増えます(StreamingRetune の grainSize フィールドを参照)。既定のグレイン(約 46 ms)で 48 kHz の場合、neutral-monitor の報告値は 2263 サンプル(47 ms)で、ピッチをシフトした声はそこからさらに最大で約 93 ms 遅れることがあります。そのため、ピッチを動かすプリセットは遅れに厳しいライブモニタリングには向きません。自動で選ばれるリチューンのグレインは 8192 サンプルが上限なので、保存した設定は 176.4 kHz や 192 kHz でもグレインとレイテンシがそのまま往復します。
ライブのコントロールはすべてサンプル単位でスムージングされるため、setConfig(...) で新しい設定スナップショットを適用してもブロック境界で段差になりません。フォルマント量に比例して変化するのは周波数の変位だけなので、量が 0 でもボディ・ブライトネス・鼻にかかった響き(nasal)は効きます。