ChatTTS噪声问题深度解析:从原理到高效降噪实践
做实时语音合成最怕什么?不是模型跑不动,而是好不容易生成的语音被“滋啦滋啦”的噪声盖过去。线上用户最常吐槽的两句话:
“这声音像在老式收音机里”,以及“我一开麦就全是电流麦”。
ChatTTS 在浏览器端跑流式合成时,噪声来源大致三条:
- 笔记本电源电磁干扰,3 kHz~7 kHz 高频毛刺;
- 办公室空调、键盘、空调低频稳态噪声;
- 移动端扬声器→麦克风回声,延迟 80 ms~200 ms 的循环啸叫。
噪声一旦混入 TTS 输出,后端 ASR 识别率直接掉 30%,用户体验负分。本文把最近踩坑总结的一套“实时降噪”方案拆开聊:从频谱图看差异、到 WebAudio 管线、再到 WASM 加速与移动端兼容,全部给代码,能直接搬进生产。
1. 噪声从哪来?先抓频谱“指纹”
把带噪语音送进 AnalyserNode,用 2048 点 FFT 画线,一眼就能看到 4 kHz 附近两根“刺”。
Mel 频谱图更直观:把 0-8 kHz 压成 40 维滤波器组,噪声能量块呈横向连续条带,语音则是纵向断续斑点。
对比图如下(本地采样 48 kHz,矩形窗):
- 纯语音 Mel 图:斑点集中在 100-2000 ms,频率 200-4000 Hz。
- 含键盘噪声:100-4000 Hz 全时段出现横向亮带,能量均匀。
目的明确——把横向亮带压低,同时不碰纵向斑点。
2. 技术方案选型:单麦 vs 双麦、RNN vs 经典信号
2.1 双麦克风(DMIC)(笔记本常见)
- 主麦 + 参考麦,硬件做差分谱减法,信噪比(SNR)↑12 dB。
- 缺点:Web 拿不到第二路独立流,Chrome 仅暴露“echoCancellation”开关,数据被浏览器混音,算法不可控。
2.2 单麦克风(SMIC)纯软方案
浏览器可控,本文重点。
两条路线:
- 传统 IIR/谱减:延迟 <10 ms,CPU 占用低,对稳态噪声好。
- AI 降噪(RNNoise、TCN):非稳态键盘声也吃,但模型 2-3 MB,纯 JS 跑 44 kHz 单声道要 30% CPU(M1 Mac)。
折中做法:IIR 预滤波 + 轻量 TCN(128 隐藏单元,INT8),延迟 30 ms,CPU 降到 8%。
3. WebAudio 实时管线:代码直接搬
核心思路:MediaStream→AudioWorklet→ 自定义降噪处理器 →ScriptProcessor(回退)→ 扬声器/RTC。
3.1 主线程注册
// main.ts await audioCtx.audioWorklet .addModule('/workers/denoiser-worklet.js'); const denoiserNode = new AudioWorkletNode(audioCtx, 'denoiser-processor'); denoiserNode.connect(audioCtx.destination);3.2 AudioWorklet 处理器(TypeScript)
// denoiser-processor.ts // 编译后生成 denoiser-worklet.js declare const sampleRate: number; class DenoiserProcessor extends AudioWorkletProcessor { private iirCoeffs: Float32Array; private xBuf: Float32Array; // 输入延迟线 private yBuf: Float32Array; // 输出延迟线 constructor() { super(); // 4 阶 Butterworth 高通 80 Hz,滤掉空调嗡嗡 this.iirCoeffs = new Float32Array([ 0.9722, -1.9444, 0.9722, // b0,b1,b2 1.0, -1.9444, 0.9444 // a0,a1,a2 ]); this.xBuf = new Float32Array(3); this.yBuf = new FloatArray(3); } process(inputs: Float32Array[][], outputs: Float32Array[][]) { const input = inputs[0][0]; const output = outputs[0][0]; if (!input) return true; for (let i = 0; i < input.length; i++) { const x0 = input[i]; const y0 = this.iirCoeffs[0] * x0 + this.iirCoeffs[1] * this.xBuf[1] + this.iirCoeffs[2] * this.xBuf[0] - this.iirCoeffs[4] * this.yBuf[1] - this.iirCoeffs[5] * this.yBuf[0]; output[i] = y0; // 更新延迟线 this.xBuf[0] = this.xBuf[1]; this.xBuf[1] = x0; this.yBuf[0] = this.yBuf[1]; this.yBuf[1] = y0; } return true; } } registerProcessor('denoiser-processor', DenoiserProcessor);3.3 Web Worker 并行谱减(可选)
若还要做谱减,把 PCM 片段 postMessage 给 Worker,FFT 用 kissfft-wasm,算完再回传,避免阻塞 Worklet。
4. 性能:WASM 让 CPU 占用腰斩
MacBook Air M1 / Chrome 119 / 48 kHz 单声道:
- 纯 JS 谱减(2048 点 FFT):28%
- WASM (clang -O3) 同参数:12%
- WASM + SIMD(-msimd128):7%
采样率降到 24 kHz,三者依次 15% / 7% / 4%。
结论:
- 44 kHz 以上必须上 WASM;
- 移动端 24 kHz 够用,省 50% 运算。
5. 避坑指南:Safari 延迟 & 安卓回声
5.1 Safari 延迟
- iOS Safari 17 之前,AudioWorklet 输出到扬声器固定 256 帧缓冲,额外 80 ms。
- 解决:回退到 ScriptProcessor,bufferSize 设 512,延迟压到 23 ms,但 CPU 高 15%。
5.2 安卓回声
- 部分小米/华为机型打开
echoCancellation:true后,系统自带 AEC 把 TTS 语音当回声消掉。 - 解决:先
getUserMedia时关 AEC,用 WebAudio 自写声学回声消除(AEC),参考 Speex 的 NLMS 算法,步长 0.05,运算量低。
6. 生产参数调优清单
- 稳态噪声:IIR 高通 80 Hz + 低通 7 kHz,CPU<2%,SNR↑6 dB。
- 键盘突发:谱减 α=4,β=0.002,辅以 Voice Activity Detection(VAD),无人声时直接 mute。
- 回声:延迟估计 80-140 ms,步长 0.05,ERLE(回声回损)↑20 dB。
- 延迟预算:TTS 合成 30 ms + 降噪 20 ms + 缓冲 10 ms < 60 ms,会议场景可接受。
7. 留给下一步:端侧 AI 与数据闭环
- WebNN API 已出 Origin Trial,用
ml.createContext('cpu')跑 TFLite 降噪模型,可把 128 单元 TCN 再提速 30%,无需 WASM glue。 - 噪声特征库:把线上 10 s 片段上传,按 Mel 频带能量聚类,自动打标签“键盘/空调/风扇”,每月重训,模型越用越聪明。
踩完坑回头看,ChatTTS 的“电流麦”不是无解,只要抓住“频谱指纹→轻量算法→WASM 加速→端侧闭环”四步,就能把信噪比拉上来,同时把延迟压下去。
下一版打算把 WebNN 的降噪模型直接塞进 Worklet,彻底扔掉 WASM 文件,届时再来分享。祝你也能早日让用户忘记“噪声”这个词。