
Twilio Voice のブラウザ通話に RNNoise のノイズキャンセリングを組み込んで聞き比べた
はじめに
ブラウザから発着信するコールセンターや業務アプリでは、利用者の周りの雑音がそのまま相手に届きます。たとえばエアコンの音やキーボードの打鍵音、店内の音楽、周囲の会話などは、通話の聞き取りを妨げる代表的な雑音です。
Twilio は 2026 年 8 月に、Twilio Voice の JavaScript SDK にノイズキャンセリングを組み込むリファレンス実装を公開しました。本記事では、そのうち RNNoise 版を実際の通話に組み込み、雑音の種類ごとにノイズキャンセリングの有無による違いを聞き比べました。
先に結論を述べると、空調やキーボードの音はほぼ消え、声が聞き取りやすくなりました。音楽やカフェの環境音も大きく減りました。一方、周囲の人の話し声はほとんど残りました。
Twilio Voice と Noise Cancellation Reference Components
Twilio Voice は、電話やブラウザでの音声通話をアプリケーションに組み込める Twilio のサービスです。ブラウザ向けには Voice JavaScript SDK を提供しています。
この SDK は、バージョン 2.9.0 で AudioProcessor API を導入しました。マイクの音声を Twilio へ送る前に、任意の処理を差し込める仕組みです。2.17.0 では、受信した音声への適用にも対応しました。
Noise Cancellation Reference Components は、この API を使ったノイズキャンセリングのリファレンス実装です。オープンソースの RNNoise を使う版と、商用の Krisp SDK を使う版があります。
検証環境
- macOS 26.7.1 (Apple M4、MacBook Pro)
- Google Chrome 154
- Node.js 24.15.0
- twilio-voice-js-reference-components (commit 56b24c1、@twilio/voice-sdk 2.18.3、@sapphi-red/web-noise-suppressor 0.3.5)
対象読者
- Twilio Voice でブラウザから発着信するアプリケーションを開発・運用している方
- 通話相手の周りの雑音に悩んでいる方
参照
- Voice JavaScript SDK: Reference Components (Twilio 公式)
- AudioProcessor (Twilio 公式)
- twilio/twilio-voice-js-reference-components (GitHub)
背景
ブラウザの通話では、マイクに入った音をすべて相手に送ります。Chrome にも標準のノイズ抑制がありますが、あらゆる雑音を消せるわけではありません。
Twilio の公式ドキュメントでは、ノイズキャンセリングの組み込み方が説明されていますが、一方で、雑音の種類ごとの効果は示されていません。導入を検討する側にとって最も知りたいのは、組み込んだときの通話の音の変化でしょう。
そこで、雑音の種類を変えながら実際に通話し、ノイズキャンセリングの有無による違いを聞き比べました。
検証方法
リファレンス実装のデモを手元で起動し、2 つの Chrome から通話します。送話側 (alice) には、声と雑音を重ねた音声ファイルをマイク入力として与えます。受話側 (bob) で受信音を録音し、送話側のノイズキャンセリングを無効にした場合と有効にした場合を比べます。
通話環境の用意
リファレンスのデモは、TwiML App の Webhook として /twiml を公開する必要があります。今回は、この処理だけ Twilio Functions でデプロイしました。
/twiml は、発信者と着信者を 1 対 1 の Conference でつなぐ TwiML を返します。
Twilio Functions に移した /twiml
const crypto = require('crypto');
const isPhoneNumber = (value) => /^[\d+\-() ]+$/.test(value || '');
exports.handler = async (context, event, callback) => {
const twiml = new Twilio.twiml.VoiceResponse();
const roomName = `conference-${crypto.randomUUID()}`;
let recipient = event.recipient || 'bob';
recipient = isPhoneNumber(recipient) ? recipient : `client:${recipient}`;
twiml.dial().conference(
{
beep: 'false',
endConferenceOnExit: true,
maxParticipants: 2,
participantLabel: 'client-caller',
startConferenceOnEnter: true,
waitUrl: '',
},
roomName
);
// Functions は callback で実行が終わるため、TwiML を返す前に着信者を追加する
try {
await context.getTwilioClient().conferences(roomName).participants.create({
beep: 'false',
endConferenceOnExit: true,
from: event.From,
label: 'client-callee',
to: recipient,
});
} catch (error) {
console.error(`Failed to add callee to conference ${roomName}:`, error.message, error.code);
}
return callback(null, twiml);
};
聞き比べ用の音声の用意
声は、筆者が MacBook Pro の内蔵マイクで読み上げた約 20 秒の業務連絡です。雑音は次の 5 種類を用意しました。
| 雑音 | 作り方 |
|---|---|
| エアコン・ファン | ブラウンノイズに 100 Hz と 200 Hz のうなりを加えて合成 |
| キーボード | 内蔵マイクで打鍵音を録音 |
| 音楽 | ジャズの器楽演奏 (Wikimedia Commons の CC0 音源: Jazz at the park ) |
| カフェ | 人の話し声を含む店内音 (Wikimedia Commons の CC0 音源: Cafe ambiance ) |
| 周囲の人の声 | macOS の say コマンドで 3 人分の日本語音声を合成し、ずらして重ねたもの |
声の前に 3 秒、後ろに 2 秒の雑音だけの区間を置き、雑音を声より 5 dB 小さい音量で重ねました。かなり騒がしい環境を想定した条件です。
話者は筆者 1 人、マイクは MacBook Pro の内蔵マイク、ブラウザは macOS 版 Chrome だけです。
雑音は音声データとして声に重ねたもので、実際の部屋の反響は含みません。周囲の人の声も合成音声です。
スペクトログラムの観察結果と、耳で聞いた印象を評価しました。
ノイズキャンセリングの切り替えと録音
Chrome は、起動オプションで音声ファイルをマイク入力の代わりに使えます。2 つの Chrome を起動し、alice にだけ雑音入りの音声ファイルを与えました。
const context = await chromium.launchPersistentContext(userDataDir, {
channel: 'chrome',
headless: false,
args: [
'--use-fake-device-for-media-stream',
'--use-fake-ui-for-media-stream',
'--autoplay-policy=no-user-gesture-required',
'--mute-audio',
`--use-file-for-fake-audio-capture=${wavPath}`,
],
});
通話がつながったら、bob の受信音 (call.getRemoteStream()) を録音します。続いて、alice の画面で Denoise microphone のチェックボックスを入れ、同じ長さを録音します。チェックボックスを入れると、リファレンス実装は次の処理を実行します。
await loadRnnoiseOnce();
// Disable the browser's own NS/AGC only while denoising the mic, so they
// don't run in series with RNNoise. Reverted when denoise is turned off.
await this.#device.audio.setAudioConstraints({ noiseSuppression: false, autoGainControl: false });
await this.#device.audio.addProcessor(this.#localProcessor, false);
RNNoise を有効にすると、Chrome 標準のノイズ抑制と自動音量調整は無効になります。 つまり今回の比較は、何も処理しない音と RNNoise の比較ではありません。Chrome 標準のノイズ抑制と RNNoise の比較です。
録音した音声からは、入力との相互相関で 1 回分の再生区間を切り出しました。
通話と録音のスクリプト
import fs from 'node:fs';
import path from 'node:path';
import { HARNESS_DIR, connectCall, launch, sleep, writeWav } from './lib.mjs';
const NOISES = process.argv.slice(2).length ? process.argv.slice(2) : ['fan', 'keyboard', 'music', 'cafe', 'babble'];
const listenDir = path.join(HARNESS_DIR, 'audio', 'listen');
const outDir = path.join(HARNESS_DIR, 'out', 'listen');
fs.mkdirSync(outDir, { recursive: true });
const silence = path.join(HARNESS_DIR, 'audio', 'silence.wav');
const wavSeconds = (file) => (fs.statSync(file).size - 44) / 2 / 48000;
for (const noise of NOISES) {
const input = path.join(listenDir, `${noise}.wav`);
const recordMs = Math.ceil(wavSeconds(input) * 2 * 1000) + 1000;
const bob = await launch('bob', silence, outDir);
const alice = await launch('alice', input, outDir);
await connectCall(alice, bob);
const record = async (label) => {
await bob.page.evaluate(() => window.__startRec('rec', window.__call.getRemoteStream()));
await sleep(recordMs);
const { b64 } = await bob.page.evaluate(() => window.__stopRec('rec'));
writeWav(path.join(outDir, `${noise}-${label}-raw.wav`), b64);
};
await record('off');
await alice.page.locator('#denoise-local-checkbox').click();
await sleep(500);
if (!(await alice.page.locator('#denoise-local-checkbox').isChecked())) throw new Error('RNNoise を有効にできなかった');
await record('on');
await alice.page.locator('#hangup').click();
await sleep(1000);
await alice.context.close();
await bob.context.close();
}
window.__call と window.__startRec は、ページの読み込み前に注入したスクリプトで用意しています。前者はデモの通話オブジェクトへの参照で、後者は MediaStream を 48 kHz の PCM で録音する関数です。
検証結果
雑音の種類ごとに、無効時と有効時のスペクトログラムを並べます。
表の音量は、雑音だけの区間の RMS です。各条件の録音は 1 回ずつのため、数値は目安としてご覧ください。
| 雑音 | 無効 | 有効 | 差 |
|---|---|---|---|
| エアコン・ファン | -48.7 dBFS | 無音 | 消えた |
| キーボード | -51.1 dBFS | 無音 | 消えた |
| 音楽 | -36.8 dBFS | -56.5 dBFS | 20 dB 低下 |
| カフェ | -39.6 dBFS | -50.6 dBFS | 11 dB 低下 |
| 周囲の人の声 | -25.7 dBFS | -32.0 dBFS | 6 dB 低下 |
エアコン・ファン

ファン・ノイズキャンセリングなし

ファン・ノイズキャンセリングあり
無効時は、声の後ろでサーッという音が鳴り続けます。有効にすると、雑音だけの区間は完全な無音になりました。雑音が消えただけでなく、声が際立って聞き取りやすくなりました。
キーボード

キーボード・ノイズキャンセリングなし

キーボード・ノイズキャンセリングあり
打鍵音もほとんど聞こえなくなりました。雑音だけの区間は、ファンと同じく無音です。突発的に鳴る音でも、声と重ならない部分はきれいに消えています。
音楽

音楽・ノイズキャンセリングなし

音楽・ノイズキャンセリングあり
フルートとサックスの演奏は、20 dB ほど小さくなりました。わずかに残りますが、聞いていて違和感はありません。
カフェ

カフェ・ノイズキャンセリングなし

カフェ・ノイズキャンセリングあり
食器の音や店内のざわめきは 11 dB ほど小さくなりました。音楽と同様に少し残りますが、気になるほどではありませんでした。
周囲の人の声

声・ノイズキャンセリングなし

声・ノイズキャンセリングあり
周囲の人の話し声は、ほとんど消えませんでした。 音量は 6 dB 下がりましたが、聞いた印象では有効時も話し声がはっきり残っています。
声の音量・帯域
どの雑音でも、ノイズキャンセリングを有効にすると声の部分の音量が 5〜8 dB 小さくなりました。また、受信音の周波数成分を見ると、ノイズキャンセリングの有無にかかわらず、約 3.4 kHz より上がほぼ失われていました。
考察
雑音が声に似ているほど残る傾向が観察されました。RNNoise は、音声らしい成分を残してそれ以外を抑えるように学習されたモデルのため、この方式の性質だと考えています。
導入を勧めやすいのは、空調・キーボード・店内の音楽のような機械的な雑音が多い環境です。在宅勤務の利用者や、オフィスで PC から発信するオペレーターには効果が大きいと思います。一方、隣の席の会話が入るコールセンターのような環境では、RNNoise だけでは不十分でしょう。周囲の話し声まで抑えたい場合は、話者を区別できる別の方式を検討することになると思います。
まとめ
Twilio Voice のブラウザ通話に RNNoise 版のリファレンス実装を組み込み、5 種類の雑音で効果を聞き比べました。空調やキーボードの音はほぼ消えて声が聞き取りやすくなり、音楽やカフェの環境音も気にならない程度まで小さくなりました。一方、周囲の人の話し声はほとんど残りました。ブラウザ通話へのノイズキャンセリングの導入を検討する際の判断材料になれば幸いです。







