はじめに
音声エージェントでは、マイクに入る音の品質が思っている以上に効きます。雑音やスピーカーからの回り込みは、音声認識の精度だけでなく、発話区間の検出(VAD)、割り込みの判定、発話終端(EOU)の判定まで狂わせます。エージェントが自分の声を「ユーザーの発話」と誤認して、自分で自分の話を遮る、というのが典型的な失敗です。
Voice Live には、入力音声を整える仕組みが3段階あります。
- ノイズ抑制(
azure_deep_noise_suppression) - サーバ側のエコー除去(
server_echo_cancellation) - Live-Reference AEC(エコー除去の参照に、クライアントが実際に再生した音を使う)
本記事では「どこまで入れればよいか」の判断の流れと、一番手間のかかる 3 の実装の勘所を整理します。
本記事は Voice Live 機能解説シリーズの第2回です。
シリーズの記事一覧(SDK 1.2.0 / API 2026-07-15)
-
Voice Live SDK 1.2.0の変更点とAPI 2026-07-15 GA
SDKに型が無い機能の分類 -
Voice Live APIで会話の「間」を扱う
先に話す・言いよどみを待つ・割り込む・待ち時間に話す - Voice Live の音声入力の品質を上げる(本記事)
ノイズ抑制・エコー除去・Live-Reference AEC の選び方 -
Voice LiveでMCPサーバのツールを使う
function callingとの違いと並列ツール呼び出し -
Voice LiveでFoundryのエージェントをツールとして呼ぶ
chat-supervisorパターンの使い方と前段モデルの選び方 -
30秒の音声で「自分の声」を作って Microsoft Foundry エージェントを喋らせる
Personal Voice の申請から実装まで -
Voice Live APIのアバターを使い分ける
標準・カスタム写真・カスタムビデオの設定と実装 -
Voice Liveのアバター映像をWebSocketで受ける
WebRTCとの違いと映像の形式
番外編:Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ
SDPを1行ずつ突き合わせた記録
検証に使ったコードはこちらです。
本記事は 2026年10月時点の情報です。対象は Voice Live API 2026-07-15(GA)、Live-Reference AEC の実機検証は 2026-06-01-preview で行っています(理由は後述)。
3段階の全体像
| 段階 | 設定 | 何を取り除くか | 実装の手間 |
|---|---|---|---|
| 1. ノイズ抑制 | input_audio_noise_reduction |
周囲の雑音 | 設定のみ |
| 2. サーバ側エコー除去 |
input_audio_echo_cancellation(参照はサーバ) |
エージェント自身の声の回り込み | 設定のみ |
| 3. Live-Reference AEC | 同上(参照はクライアント) | 実際にスピーカーから出た音の回り込み | 大きい(ステレオ参照の生成) |
1 と 2 は session.update に1行ずつ足すだけです。
{
"type": "session.update",
"session": {
"input_audio_sampling_rate": 24000,
"input_audio_noise_reduction": { "type": "azure_deep_noise_suppression" },
"input_audio_echo_cancellation": { "type": "server_echo_cancellation" }
}
}
サンプルでは 1 と 2 を常に有効にして使っていますが、有効・無効で認識精度などを比較する検証はしていません。本記事で実測を伴うのは 3 です。
ノイズ抑制は3つのモードから選ぶ
ノイズ抑制は当初 azure_deep_noise_suppression の1種類だけでしたが、現在は3つのモードから選べます。「どの声を残したいか」で決まります。
type |
挙動 | 使う場面 |
|---|---|---|
azure_deep_noise_suppression |
背景の雑音を抑え、マイクに一番近い話者に最適化する | 従来の汎用的なノイズ抑制 |
azure_deep_noise_suppression_headset |
近くの話者の声を保ちつつ、背景の「声」とノイズを抑える | 周囲に人の話し声がある環境で、目の前の話者だけを通したい |
azure_personal_noise_suppression |
対象話者の情報を使って、その人の声だけを残す(手前にいる他の話者の声も除去する) | 前後の分離だけでは足りず、話者単位で絞りたい |
違いは「背景か手前か」で切るのか、「誰の声か」で切るのかです。カフェで隣の席の会話を消したいなら headset、複数人が同じ場所にいて特定の1人だけを拾いたいなら personal、という使い分けになります。
headset と personal には、さらに detection_input という指定があります。
detection_input |
想定する環境 |
|---|---|
raw_ec(既定) |
電話など一般的な環境 |
mas_dns |
雑音が多く、音響的に厳しい環境 |
{
"type": "session.update",
"session": {
"input_audio_noise_reduction": {
"type": "azure_deep_noise_suppression_headset"
}
}
}
{
"type": "session.update",
"session": {
"input_audio_noise_reduction": {
"type": "azure_personal_noise_suppression",
"detection_input": "mas_dns"
}
}
}
使う前に押さえておきたい点が3つあります。
-
ノイズ抑制のモードを選ぶことは、エコー除去の有効化・無効化とは独立です。
input_audio_echo_cancellationは別に設定します -
session.updatedは「要求した設定」を返すだけで、パーソナライズが実際に効いているかは示しません(この「受理されたのに効いているか分からない」は Voice Live のあちこちで出てくるパターンです) - 話者ターゲティングは本人確認の仕組みではありません。 また、ノイズ抑制は他の話者が文字起こしに出ないことを保証しません
なお、筆者はまだ3モードの比較検証をしていないため、本記事で実測を伴うのは引き続き Live-Reference AEC です。
もう1つの選択:文字起こしモデル
入力音声の品質という観点では、どの文字起こしモデルを使うかも効いてきます。こちらも選択肢が増えました。input_audio_transcription.model で指定します。
| 文字起こしモデル | 組み合わせられるチャットモデル |
|---|---|
azure-speech |
すべての非マルチモーダルモデルとエージェント(既定で有効) |
mai-transcribe / mai-transcribe-2(preview) |
gpt-realtime、gpt-realtime-mini、すべての非マルチモーダルモデル、エージェント |
whisper-1 / gpt-4o-transcribe / gpt-4o-mini-transcribe / gpt-4o-transcribe-diarize
|
gpt-realtime と gpt-realtime-mini のみ |
組み合わせを間違えるとセッションが拒否されます。 OpenAI 系の文字起こしモデルは realtime 系のモデルとしか組み合わせられず、逆に gpt-4o のようなカスケードのモデルに指定すると弾かれます。筆者は以前これで「このリージョンでは realtime モデルが使えない」と誤判定しました。
mai-transcribe 系は phrase_list(認識させたい語を並べて精度を上げる)に対応しています。より低遅延な mai-transcribe-2-streaming(preview)もありますが、使えるリージョンが mai-transcribe-2 とは異なるので、選ぶ前にリージョンの表を確認してください。
どこまで入れるかの判断
まずサーバ側のエコー除去で足りるか
サーバ側のエコー除去は、サーバが送った TTS の音声を参照信号にして、マイク入力からエージェント自身の声を引き算します。つまり「サーバが送った音 = スピーカーから実際に鳴った音」という前提に立っています。
公式ドキュメントには、この前提の限界も書かれています。
The service assumes the client plays response audio as soon as it receives it. If playback is delayed for more than two seconds, echo cancellation quality is impacted.
― How to use the Voice Live API
逆に言えば、この前提が成り立つ環境ならサーバ側で十分です。
| サーバ側で十分な環境 | 理由 |
|---|---|
| ヘッドセット・イヤホン | そもそも回り込みが小さい |
| 受け取った TTS をそのまま鳴らすだけ | 参照と実際の再生音が一致する |
| クライアントを単純に保ちたい | 3 の実装コストに見合わない |
Live-Reference AEC が要る環境
前提が崩れるのは、スピーカーから出る音がサーバの TTS と一致しない場合です。
| 環境 | サーバ側の参照では足りない理由 |
|---|---|
| 音量調整・EQ・ノーマライズ・デバイスの DSP(ラウドネス補正、空間オーディオなど) | 振幅や周波数特性が変わると引き算が成立しない |
| Bluetooth などでの再エンコード・リサンプリング | 波形が元の TTS と一致しない |
| 効果音・BGM・通知音、動画やゲームの音を同時に鳴らす | サーバの参照にアプリ独自の音が入っていない |
| 複数のエージェントやアバターが同時に話す | スピーカーから出るのはミックス後の音 |
| 再生の遅延・ジッタ・途中停止(バージインなど) | 「送った通りに鳴った」が崩れる |
| スピーカーとマイクが近い、大音量(キオスク、車載、ロボット、スマートスピーカー、XR ヘッドセット) | 回り込みが大きく、参照の正確さが結果に直結する |
判断の目安は「スピーカーの実際の出力がサーバの TTS と違う環境ほど、3 の価値が上がる」です。ただし後述のとおり、参照の時間合わせを正しく作れないと逆効果になります。作り込めるなら 3、無理なら 2 が現実的な線引きです。
Live-Reference AEC
仕組み
マイクの音声を送る代わりに、マイク(チャンネル0)と実際の再生音(チャンネル1)を交互に並べたステレオ PCM16 を送ります。
[mic₀, ref₀, mic₁, ref₁, mic₂, ref₂, ...]
サーバは内部の TTS ループバックの代わりに、チャンネル1をエコー除去の参照に使います。エコー除去の処理自体はサーバで行われ、クライアントは参照を渡すだけです。
設定
{
"type": "session.update",
"session": {
"input_audio_format": "pcm16",
"input_audio_echo_cancellation": {
"type": "server_echo_cancellation",
"reference_source": "client",
"channels": 2
}
}
}
reference_source と channels の組み合わせは2通りしか許されません。
reference_source |
channels |
結果 |
|---|---|---|
server(既定) |
1(既定) |
OK。従来のサーバ側エコー除去 |
server |
2 |
エラー invalid_ec_channels_requires_client
|
client |
1 |
エラー invalid_ec_reference_channels
|
client |
2 |
OK。Live-Reference AEC |
また、reference_source、channels、input_audio_format、input_audio_sampling_rate はセッションの途中で変更できません。この機能は API 2026-06-01-preview で追加され、2026-07-15(GA)でも使えます。
帯域はステレオになるぶんほぼ倍になります。公式の目安では 24 kHz PCM16 で生データ約 94 KB/s、base64 後に約 125 KB/s です。参照チャンネルは課金の計測前に取り除かれるので、音声の課金は増えません。
SDK 1.2.0 では AudioEchoCancellation に ReferenceSource と Channels が入ったので、設定は型で書けます。
options.InputAudioFormat = InputAudioFormat.Pcm16;
options.InputAudioEchoCancellation = new AudioEchoCancellation
{
ReferenceSource = EchoCancellationReferenceSource.Client,
Channels = 2
};
feature flag を付けて接続する
reference_source と channels を使うには、WebSocket の URL に &features=client_ec_reference:true を付ける必要があります(API リファレンスに記載があります)。
本記事の実機検証は 2026-06-01-preview + このフラグで行いました。フラグなしで通るかは確認していないので、まずフラグ付きで試すのが確実です。
wss://<resource>.cognitiveservices.azure.com/voice-live/realtime?api-version=2026-06-01-preview&model=gpt-4o&features=client_ec_reference:true
公式にもブラウザ向けのサンプル(Live-Reference AEC sample)が用意されています。Web アプリで試すならこちらが近道です。
https://github.com/microsoft-foundry/voicelive-samples/tree/main/javascript/live-reference-aec
実装の勘所:参照の時間合わせ
設定は数行ですが、動かすのは難しいです。サーバは渡された参照をそのまま引き算に使うので、参照が実際にスピーカーから出ている音とずれていると、エコーは消えません。サンプルで最初に作った素朴な実装(再生した音を FIFO に積み、マイクのフレームごとに取り出す)は、しばらくするとずれて戻らなくなりました。
最終的に安定させるまでに効いたのは次の4点です。
1. 再生バッファで音声を捨てない
応答の音声は実時間より速くまとめて届きます。サンプルの再生バッファ(NAudio の BufferedWaveProvider)は当初 10秒で、溢れた音声を捨てる設定(DiscardOnBufferOverflow)でした。これだと長い応答で音声が黙って捨てられ、参照に積んだ音と実際に鳴った音が一致しなくなります。
120秒に広げて、捨てない状態にしました。
これはエコー除去に限らず全モードに効く不具合でした。10秒を超える応答が途中でぷつぷつ切れる場合は、再生バッファの溢れを疑ってください。
2. 参照は「最新 − 再生待ち」の位置を読むだけにする
いまマイクが拾っているのは、いまスピーカーから出ている音です。それは「クライアントが再生キューに積んだ最新の音」から「まだ再生バッファに残っている分(再生待ち)」だけ前の位置にあります。
再生キューに積んだ音 : ────────────────────────▶ 最新
▲
いま鳴っている位置 = 最新 − 再生待ち
ポイントは、この位置をマイクのフレームごとに毎回計算し直して、読むだけにすることです。取り出して消費すると、読み位置がマイクの速度で進みます。再生が遅れている局面ではその読み位置が実際の再生より先行し、一度先行すると戻れません。毎回計算し直せば、ずれても次のフレームで自然に戻ります。
public byte[] BuildStereoFrame(byte[] micMonoPcm, int playbackBacklogBytes)
{
int sampleCount = micMonoPcm.Length / 2;
var stereo = new byte[sampleCount * 4];
lock (gate)
{
// いま鳴っている位置 = 最新 − 再生待ち。毎フレーム計算し直し、読むだけ(消費しない)
int readOffset = count - playbackBacklogBytes;
if (readOffset < 0) readOffset = 0; // 履歴が足りない(参照が再生より先行している)
for (int i = 0; i < sampleCount; i++)
{
// ch0: マイク
stereo[i * 4] = micMonoPcm[i * 2];
stereo[i * 4 + 1] = micMonoPcm[i * 2 + 1];
// ch1: 実際に鳴っている音(無ければ無音)
int pos = readOffset + i * 2;
if (pos + 1 < count)
{
stereo[i * 4 + 2] = reference[(head + pos) % reference.Length];
stereo[i * 4 + 3] = reference[(head + pos + 1) % reference.Length];
}
}
// このフレームの分は再生済みになったので、古い側から捨ててバッファを一定に保つ
int retire = Math.Min(readOffset, micMonoPcm.Length);
head = (head + retire) % reference.Length;
count -= retire;
}
return stereo;
}
「再生待ち」は再生バッファの残量(BufferedWaveProvider.BufferedBytes)から取ります。参照用の履歴はこの再生待ちより長く持つ必要があるので、サンプルでは30秒分を確保しています。
3. マイクのコールバックで再生バッファをロックしない
再生待ちの量を正確に読もうとして、マイクのコールバックの中で再生バッファをロックすると、再生が途切れます。残量の読み取りはロックせずに行い、多少の誤差は 2 の「毎回計算し直す」で吸収させます。
4. バージインで参照もリセットする
ユーザーが割り込んで再生バッファを捨てたときは、参照の履歴も一緒に捨てます。捨てないと、もう鳴らない音が参照に残り、以降の位置計算がずれます。
動いているかの確かめ方
エコー除去の効果は耳では判断しにくいので、サンプルでは5秒ごとに統計を出しています。
[EC ref] sent <送信秒数>s stereo | ref audible <参照に音があった割合>% / silence <無音の割合>% | playback backlog <再生待ち>ms | ref buffered <参照の履歴>ms | skipped <読み飛ばした秒数>s | aligned
見るのは次の2点です。
playback backlog(再生待ち)とref buffered(参照の履歴)が ±100ms 程度で追随している- 末尾が
alignedのまま(履歴が足りず参照が再生より先行したフレームが無い)
実機では120秒連続で aligned を維持し、参照が再生待ちに ±100ms で追随しました。エージェントの声をマイクが拾って自分の発話を遮る自己トリガーは起きなくなりました。
この実装は、再生バッファより後ろ(デバイスやドライバの遅延)はモデル化していません。完全な音響的整合ではなく、ワイヤ上の仕組みを確かめるための best effort です。それでも自己トリガーを消すには十分でした。
まとめ
| 状況 | 選ぶもの |
|---|---|
| 周囲に人の話し声がある | ノイズ抑制を headset モードに |
| 複数人の中から特定の1人だけ拾いたい | ノイズ抑制を personal モードに |
| ヘッドセット、TTS をそのまま鳴らすだけ | ノイズ抑制 + サーバ側エコー除去 |
| スピーカーの音が TTS と一致しない(DSP、BGM、ミックス、Bluetooth、大音量) | Live-Reference AEC |
| Live-Reference AEC の時間合わせを作り込めない | サーバ側エコー除去のまま |
- ノイズ抑制とサーバ側エコー除去は設定だけで入る。ノイズ抑制は3モードから「どの声を残すか」で選ぶ
- 文字起こしモデルの組み合わせに制約がある。OpenAI 系は realtime 系のモデルとだけ
- Live-Reference AEC は設定は数行、実装の大半は参照の時間合わせ
- 再生バッファで音声を捨てない
- 参照は「最新 − 再生待ち」を毎回計算し直して読むだけ
- マイクのコールバックで再生バッファをロックしない
- バージインで参照もリセットする
- 接続 URL に
&features=client_ec_reference:trueを付ける
次回は MCP サーバのツールと並列ツール呼び出し を扱います。