【electron6】Web Audio + AudioWorklet PCM 实时采集噪音和模拟调试
连这条博客(【electron6】浏览器实时播放PCM数据)
一、背景与目标
在语音识别或实时通话类项目中,我们通常需要通过 AudioWorkletNode 从麦克风采集音频数据,并将其转换为标准 16bit PCM(线性脉冲编码调制)格式,以便传输或播放。为了调试 Worklet 数据处理流程,本笔记中使用一个本地 PCM 文件 (.ptt) 来模拟 AudioWorkletNode.process() 的输出数据,从而快速验证 PCM ↔ Float32 转换链路是否正确、是否干净无噪音。
二、音频采集基础结构
显式指定 sampleRate = 16000,保证采样率一致性:
this.audioCtx = new AudioContext({sampleRate: 16000
});
await this.audioCtx.audioWorklet.addModule('./voice.js');this.randomNoiseNode = new AudioWorkletNode(this.audioCtx,"voices",{channelCount: 1,processorOptions: {recording: this.recording,targetSampleRate: 16000,frameSize: 320, // 每帧 20ms},parameterData: {customGain: 1.0}}
);
三、AudioWorkletProcessor 核心实现
Worklet 内部负责实时从输入流读取音频、缓存、打包并发送至主线程:
class VoicesProcessor extends AudioWorkletProcessor {constructor(options) {super();this.buffer = [];this.recording = options.processorOptions.recording;this.frameSize = options.processorOptions.frameSize || 320;}encodePCM(float32Array) {const buffer = new ArrayBuffer(float32Array.length * 2);const view = new DataView(buffer);let offset = 0;for (let i = 0; i < float32Array.length; i++, offset += 2) {let s = Math.max(-1, Math.min(1, float32Array[i]));view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);}return view;}process(inputs, outputs) {const input = inputs[0][0];if (!input) return true;// 将每次 process 的 128 采样块累计缓存this.buffer.push(...input);// 达到一帧长度(320 samples = 20ms@16kHz)时发送if (this.buffer.length >= 320) {const frame = this.buffer.slice(0, 320);this.buffer = this.buffer.slice(320);const bytes = this.encodePCM(new Float32Array(frame));this.port.postMessage({ type: 'result', data: bytes });}return this.recording;}
}registerProcessor('voices', VoicesProcessor);
四、encodePCM 函数(Float32 → PCM16)
const encodePCM = (float32Array: Float32Array) => {const buffer = new ArrayBuffer(float32Array.length * 2);const view = new DataView(buffer);let offset = 0;for (let i = 0; i < float32Array.length; i++, offset += 2) {let s = Math.max(-1, Math.min(1, float32Array[i]));view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);}return view;
};
五、文件模拟 Worklet 数据调试
使用本地 .ptt(其他的.pcm文件也可) PCM 文件模拟 AudioWorkletNode 输出数据:
import axios from 'axios';
const onePcm = require('./ceshi.ptt');const encodePCM = (float32Array: Float32Array) => {const buffer = new ArrayBuffer(float32Array.length * 2);const view = new DataView(buffer);let offset = 0;for (let i = 0; i < float32Array.length; i++, offset += 2) {let s = Math.max(-1, Math.min(1, float32Array[i]));view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);}return view;
}
useEffect(() => {axios({url: onePcm,method: 'get',responseType: 'arraybuffer'}).then(res => {const int16Array = new Int16Array(res.data);const float32Array = new Float32Array(int16Array.length);// Int16 → Float32 标准化for (let i = 0; i < int16Array.length; i++) {let s = Math.max(-1, Math.min(1, int16Array[i]));float32Array[i] = s < 0 ? int16Array[i] / 0x8000 : int16Array[i] / 0x7FFF;}// 模拟 Worklet 内 encodePCM 再还原const decodeView = encodePCM(float32Array);// 使用自定义播放器播放let voice = new ProcessPCM();voice.playback(decodeView, () => {console.log('PCM 播放结束');});});在这里插入图片描述}, []);
六、噪音原因与解决总结
流程数据格式说明
七、噪音的本质原因复盘
原因描述
八、最终效果总结
优化点效果