【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 播放结束');
    });
  });在这里插入图片描述

}, []);

六、噪音原因与解决总结

流程数据格式说明

七、噪音的本质原因复盘

原因描述

八、最终效果总结

优化点效果

相关推荐
atwednesday3 小时前
日志处理
javascript
拉不动的猪4 小时前
图文引用打包时的常见情景解析
前端·javascript·后端
浩男孩4 小时前
🍀继分页器组件后,封装了个抽屉组件
前端
Dolphin_海豚4 小时前
@vue/reactivity
前端·vue.js·面试
该用户已不存在4 小时前
程序员的噩梦,祖传代码该怎么下手?
前端·后端
namehu4 小时前
前端性能优化之:图片缩放 🚀
前端·性能优化·微信小程序
rit84324994 小时前
ES6 箭头函数:告别 `this` 的困扰
开发语言·javascript·es6
摸鱼的春哥4 小时前
【编程】是什么编程思想,让老板对小伙怒飙英文?Are you OK?
前端·javascript·后端
尘世中一位迷途小书童4 小时前
版本管理实战:Changeset 工作流完全指南(含中英文对照)
前端·面试·架构