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 回だけ呼びます。

「インターリーブ」とは、複数チャンネルを 1 つの配列に交互に並べたもの(L, R, L, R…)で、「モノラル」は 1 チャンネルです。前者には processInterleaved(...)、後者には processMono(...) を使います。

ライブ処理と一括レンダーを混同しない

RealtimeVoiceChanger は「短いブロックを順番に処理し、ゲートやコンプの状態を保つ」ためのクラスです。Python / CLI の例は同じプリセットチェーンをファイル全体へ適用する入口で、ブラウザの AudioWorklet コールバック内でそのまま使うコードではありません。

信号チェーン

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

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

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

音程
0 st

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

プリセットの信号チェーン
入力段ピッチ・フォルマント出力段ハイパスゲートリチューンフォルマントEQコンプレッサーディエッサーリバーブリミッター
各ブロックは上から下へこの順で流れます。入力段(ハイパス、ゲート)と出力段(EQ〜リミッター)はサンプル単位で処理し、その間のリチューンとフォルマントはブロック全体に対して処理します。ドライ/ウェットのミックス後には True Peak リミッター(既定で有効)が続きます。
役割
ハイパス低域のランブルや DC を最初に取り除くクリーンアップ用プリフィルター(eq.highpassHz
ゲートフレーズの合間の小さな部屋ノイズやマイクノイズを抑える
リチューンプリセットが指定する固定の半音値だけ、グレインのオーバーラップアドで信号全体のピッチを上下させる(例: 声を高く/低く)。チェーンの中のピッチシフト段。ピッチ検出も音階へのスナップも行わないため、それが必要な場合は編集 DSP のオフラインのピッチ補正を使う
フォルマント音高とは独立に、声の大きさやキャラクターの印象を変える
EQボディ・プレゼンス・エアの音色調整(eq.bodyDbpresenceDbairDb
コンプレッサーブロックをまたいでレベルを安定させる
ディエッサー刺さりやすい歯擦音を抑える
リバーブ空間を足す、または整える
リミッターピークを捕まえる。ドライ/ウェットのミックス後に 4 倍オーバーサンプリングの True Peak(トゥルーピーク)リミッターが続く(既定で有効)

TIP

eq ブロックはチェーンの両端を設定します。highpassHz は先頭のクリーンアップ用ハイパスで、bodyDbpresenceDbairDb はフォルマント処理の後に置かれる音色シェルフです。

ボイスチェーンの用語をまとめて把握

  • ハイパス — 信号の最初で低域のランブルや DC を取り除くクリーンアップフィルターです。
  • ゲート — 信号がしきい値を下回ると無音にし、フレーズの合間の小さなマイク/部屋ノイズを取り除きます。
  • リチューン — 信号全体を一定の音程だけ高く/低く移調します。音を動かさずに声のキャラクターを変えるフォルマントとは別物です。あくまで移調であってオートチューンではなく、歌った音を聴き取る仕組みは入っていません。
  • フォルマント — 声を大きく/小さく、男性的/女性的に聞かせる共鳴です。これをずらすと、音程を変えずに声のキャラクターが変わります。
  • コンプレッサー — 大きい部分と小さい部分を自動でならし、レベルを安定させます。
  • ディエッサー — 耳に刺さる「s」「sh」の音(歯擦音)を抑えます。
  • リミッター — チェーン末尾でピークがクリップしないよう止める安全装置です。ここで使うのは True Peak リミッターで、再生時に波形を復元したときサンプルとサンプルの間に現れるピーク(サンプル間ピーク、ISP)まで見て抑えます。

標準プリセット ID には neutral-monitorbright-idolsoft-whisperdeep-narratorrobot-mascotdark-villain があります。これらは出発点であり、ジャンルや話者属性のラベルとして固定的に扱うものではありません。

どの言語でも同じ流れ

どのバインディングも同じ流れで動きます。changer を生成し、サンプルレートとブロックサイズで準備し、ブロックごとに 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

後始末はバインディングごとに違う

生成と処理の流れは共通ですが、ネイティブハンドルの解放方法は言語ごとに異なります。解放は必ず 1 回だけ行ってください。

  • ブラウザ / WASMdelete() を呼びます(finally 内、コンポーネントの unmount 時、Worklet 停止時など)。
  • Node ネイティブdestroy() を呼びます。using(Node 22 以降)を使えば自動で解放できます。
  • Pythonwith ブロックを抜けるとハンドルを解放します。with を使わない場合は close() を呼びます。

AudioWorklet 形式のループでは、ブラウザ / WASM で説明している WASM ヒープ上のリアルタイムバッファを使います。ブラウザ例の素の processMono(...) と違い、レンダークォンタムごとに新しい出力配列を確保せずに済みます。

CLI のモード

sonare voice-change には 2 つのモードがあります。

モードオプション
単純なピッチ/フォルマント変換--pitch-semitones--formant-factor
リアルタイムプリセットチェーンでのレンダリング--preset--preset-json--preset-pack--set PATH=VALUE

リアルタイムプリセット系のオプションを渡した場合、コマンドはプリセットチェーンを使います。単純なピッチ/フォルマント指定との併用は無効パラメータとして拒否されます。--preset-pack FILE にはエントリ選択用の --preset ID が必須で、先頭エントリへのフォールバックはありません。selector の規則とコマンド表は 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 は throw します。realtimeVoiceChangerPresetConfig(...) は無効な序数で throw します。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 つの文書が持てるのはどちらか一方だけで、dspmacros の両方を書いた文書は 不正なパラメータとして拒否されます。したがって両者の優先順位という概念はありません。

0〜1 のマクロは、そのままの値が書き込まれるのではなく対象の有効範囲へマッピングされます。 macros.space1.0 にするとリバーブミックスの上限 0.45 に到達します。範囲外の 1.0 がパラメータに書き込まれるわけではありません。

実用上の注意

リアルタイム音声処理は状態を持ちます。同じ changer をブロック間で再利用し、ブロックサイズは prepare(...) した最大値以内に保ち、コンポーネントやストリーム停止時にはハンドルを解放してください。

大きなピッチ、フォルマント、空間処理の変更はサウンドデザインには有効ですが、自然さは下がります。自然なモニタリングではプリセット編集を控えめにし、latencySamples() でレイテンシを一度確認しておいてください。

ここでいう「レイテンシ」とは

レイテンシは、音が入ってから加工後の音が出てくるまでの遅れで、チェーンが行う解析によって生じます。latencySamples() はこれをサンプル数で報告するので、サンプルレートで割れば秒になります。

prepare 済みのチェーンでは値は固定です。リチューン経路とチェーン全体のドライ経路が オーバーラップアド分のレイテンシに揃えられているため、ウェットやリチューンのミックスを 動かしても報告値は変わりません。したがってコントロールを動かすたびに読み直す必要はなく、 prepare(...) の後に一度読んで補正すれば済みます。支配的な項はリチューン段がピッチシフトの ために解析する窓の大きさで、グレインが大きいほど 1 ステップで解析する音が増え遅延も増えます (StreamingRetunegrainSize フィールドを参照)。 True Peak(サンプル間ピーク、ISP)リミッターが有効な場合は、その遅延が加わります。

ライブのコントロールはすべてサンプル単位でスムージングされるため、setConfig(...) で 新しい設定スナップショットを適用してもブロック境界で段差になりません。フォルマント量に 比例して変化するのは周波数の変位だけなので、量が 0 でもボディ・ブライトネス・ナザルは効きます。

関連ページ