Skip to content

リアルタイムボイスチェンジャー ​

RealtimeVoiceChanger は、libsonare のプリセット式ライブ音声チェーンです。マイク入力、リアルタイムモニタリング、または音声を繰り返しブロックで処理し、呼び出し間で DSP 状態を保ちたい場面で使います。

半音値とフォルマント係数で 1 回だけオフライン変換するなら voiceChange(...) を使います。ライブ処理やチャンク処理のプリセットチェーンが必要なら、このページを読んでください。

ブロック・サンプルレート・DSP 状態

リアルタイム音声は、小さなブロック(サンプルのまとまり)を次々に処理します。ブラウザでは AudioWorklet が 128 サンプルの固定レンダークォンタムを 1 回ずつ渡します。サンプルレート(例: 48000)は 1 秒が何サンプルでできているかです。DSP 状態は、エフェクトがブロック間で持ち越すメモリ(リバーブの残響、コンプレッサーの現在のゲインなど)です。だからこそボイスチェンジャーは 1 回限りの関数ではなく、再利用するオブジェクトになっています。

なぜ関数ではなくクラスなのか

ライブ音声処理には「前のブロックから続く状態」があります。ゲート、コンプレッサー、リバーブ、滑らかなピッチ/フォルマント変化は、前後のブロックをまたいで動く必要があります。RealtimeVoiceChanger はその状態を保持するため、ブロックごとに作り直すと音が不自然になり、準備コストも増えます。

このページで身につくこと ​

このページを読むと、次のことを判断・実装できるようになります。

  • 単純なオフライン voiceChange(...) ではなく RealtimeVoiceChanger を選ぶべき場面を判断できる。
  • ブロック処理のためにクラスを準備し、WASM ハンドルを正しく解放できる。
  • 組み込みプリセット JSON を確認し、ユーザー作成プリセットを受け入れる前に検証できる。
  • ブラウザ、Python、Node ネイティブ、CLI のどの入口を使うべきか選べる。

いつ使うか ​

作りたいもの使う入口入力出力
ブラウザでマイク音声をライブ加工するRealtimeVoiceChangerAudioWorklet などから届く短い 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 は内部状態を持つため、ブロックごとに作り直さず、ストリームが続く間は同じインスタンスを再利用します。

実装で決めること目安
sampleRateAudioContext.sampleRate や入力ファイルの実サンプルレートを使います。推測で 44100 / 48000 を固定しないでください。
maxBlockSizeprocessMono(...) に渡す最大ブロック長です。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 コールバック内でそのまま使うコードではありません。

信号チェーン ​

リアルタイムチェーンは単なるピッチシフターではありません。組み込みプリセットは次の段を組み合わせます。

PARAM SWEEP · PITCH SHIFTIDLE
ピッチシフト — 倍音の櫛ごと動かす

ピッチシフトは長さを変えずに音程を移します。セミトーンをドラッグするとスペクトルが動き、すべての倍音がまとめてスケールし、基音マーカーが新しい音程を追います。このシフトはフォルマント保存ではないため、フォルマントの山も一緒に動きます — いわゆる「チップマンク」効果です。再生で確認できます。

試聴対象
再生中に切り替えると、選んだ音を先頭から再生します。処理後
音程
0 st

上のデモはリチューン段(ピッチシフト)だけを取り出して、その効果を単体で聞けるようにしたものです。実際のプリセットでは、ピッチシフトの前にクリーンアップ用ハイパスとノイズゲートが動き、その後に EQ・ダイナミクス・空間処理が続きます。全体の順序は下の図のとおりです。

プリセットの信号チェーン
入力段ピッチ・フォルマント出力段ハイパスゲートリチューンフォルマントEQコンプレッサーディエッサーリバーブリミッター
各ブロックは上から下へこの順で流れます。ドライ/ウェットのミックス後には True Peak リミッター(既定で有効)が続きます。
段役割
ハイパス低域のランブルや 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() を読むだけです。違うのはコンストラクタの形と後始末の呼び出しだけで、それを下のタブに並べています。

typescript
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 ハンドルの解放
}
python
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")
typescript
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())
}
bash
# 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 は、音声チェーン設定を確認・保存・検証したいときに使います。

typescript
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 パッケージに同梱されているので、ホスト側はオフラインで文書を検証できます。

typescript
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リチューン(半音)
formant0.55 〜 1.65フォルマントのスケール係数
brightness0 〜 1フォルマントの明るさ、EQ のプレゼンスとエア
space0 〜 1リバーブミックス(チェーンの上限 0.45 まで)
intensity0 〜 1コンプレッサーのレシオ
noiseControl0 〜 1ゲートとノイズ処理
sibilance0 〜 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 フォルマントの位置を示しています。

リチューンとフォルマント段が動かすもの
F1F2入力0 dB−60F0 150 Hz リサンプリングで +5 半音:櫛も包絡も動く'relative'、formantFactor 10 dB−60F0 200 Hz +5 半音でフォルマントを保持:櫛だけが動く'absolute'、formantFactor 10 dB−60F0 200 Hz フォルマント ×1.33、ピッチ据え置き:包絡だけが動くピッチシフトなし、formantFactor 1.330 dB−60F0 150 Hz 01234周波数(kHz)
  • 倍音(櫛)
  • スペクトル包絡
  • 入力のフォルマント位置
F0 = 150 Hz の「ア」を 5 半音(比率 1.33)上げた場合です。相対モードで formantFactor 1 のときはスペクトル全体がリサンプルされるので、倍音の櫛の間隔が広がり、フォルマントの包絡も一緒に上がります。絶対モードで formantFactor 1 のときは、櫛の間隔だけが広がります。リチューンなしでフォルマント係数だけを変えると、包絡だけが動きます。

プリセット JSON では dsp.formant.mode にあたります。

PARAM SWEEP · FORMANTIDLE
フォルマントシフト — 音程はそのまま、声の質感を変える

フォルマントシフトはピッチシフトと対になる処理で、音程はそのままに、スペクトル包絡 — 声を小さく明るく、あるいは大きく暗く感じさせるフォルマントの山 — を動かします。係数をドラッグすると、倍音の櫛と基音マーカーは位置を保ったまま、包絡だけがスライドします。1.0 より上では声が明るく小さく、1.0 より下では暗く大きくなりますが、音程は変わりません。再生すると、音程を変えずに声の質感だけが動くのが聴けます。

試聴対象
再生中に切り替えると、選んだ音を先頭から再生します。処理後
フォルマント
1 ×
'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] で制限されます)。オートメーションも範囲外の値をキー名つきで拒否し、直前の設定がそのまま残ります。

typescript
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)は効きます。

関連ページ ​