You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Node.js中分离ReSpeaker四麦阵列Buffer中的4个声道?

分离ReSpeaker四麦阵列的4声道PCM数据(Node.js实现)

我刚好做过类似的ReSpeaker四麦声道分离需求,给你整理了完整的可运行实现方案,直接可以用来验证声道分离效果:

完整代码实现

const { spawn } = require('child_process');
const stream = require("stream");
const fs = require('fs');

// 自定义Transform流,专门用于提取单个声道的PCM数据
class ChannelTransformer extends stream.Transform {
  constructor(channelIndex, totalChannels, sampleSize = 2) {
    super({ readableObjectMode: false, writableObjectMode: false });
    this.channelIndex = channelIndex; // 目标声道索引(0-3,对应4个麦克风)
    this.totalChannels = totalChannels; // 总声道数,这里固定为4
    this.sampleSize = sampleSize; // 16位PCM每个采样占2字节
    this.remainingBuffer = Buffer.alloc(0); // 缓存不完整的帧数据,避免截断采样
  }

  _transform(chunk, encoding, callback) {
    // 把上次剩余的不完整数据和当前chunk合并
    const fullBuffer = Buffer.concat([this.remainingBuffer, chunk]);
    // 计算单帧的总字节数:4声道×2字节/采样
    const frameByteSize = this.totalChannels * this.sampleSize;
    // 只处理能被完整帧整除的部分,剩下的留到下次处理
    const processableLength = Math.floor(fullBuffer.length / frameByteSize) * frameByteSize;
    
    // 准备输出Buffer,大小是可处理长度的1/4(因为只提取一个声道)
    const outputBuffer = Buffer.alloc(processableLength / this.totalChannels);
    let outputOffset = 0;

    // 循环提取目标声道的每个采样
    for (let i = this.channelIndex * this.sampleSize; i < processableLength; i += frameByteSize) {
      fullBuffer.copy(outputBuffer, outputOffset, i, i + this.sampleSize);
      outputOffset += this.sampleSize;
    }

    // 保存剩余的不完整数据
    this.remainingBuffer = fullBuffer.slice(processableLength);
    // 输出提取后的声道数据
    this.push(outputBuffer);
    callback();
  }

  _flush(callback) {
    // 剩余的不完整帧不足以构成一个完整采样组,直接丢弃即可
    callback();
  }
}

// 启动arecord进程,获取四麦的raw PCM数据
const arecordProcess = spawn('arecord', [
  '-r', '16000',    // 采样率16kHz
  '-f', 'S16_LE',   // 16位小端格式PCM
  '-t', 'raw',      // 输出无文件头的raw格式
  '-c', '4',        // 4声道
  '-D', 'ac108'     // ReSpeaker四麦阵列的设备名
]);

// 为每个声道创建对应的Transformer和文件写入流
const channelHandlers = [];
for (let channelIdx = 0; channelIdx < 4; channelIdx++) {
  const transformer = new ChannelTransformer(channelIdx, 4);
  const fileWriter = fs.createWriteStream(`channel_${channelIdx + 1}.raw`);
  transformer.pipe(fileWriter);
  channelHandlers.push(transformer);
}

// 将arecord输出的每一段数据分发给所有声道的Transformer
arecordProcess.stdout.on('data', (chunk) => {
  channelHandlers.forEach(handler => handler.write(chunk));
});

// 处理进程退出事件,关闭所有流
arecordProcess.on('close', (exitCode) => {
  console.log(`arecord进程已退出,退出码:${exitCode}`);
  channelHandlers.forEach(handler => handler.end());
});

// 捕获arecord的错误输出
arecordProcess.stderr.on('data', (errData) => {
  console.error(`arecord运行错误:${errData.toString()}`);
});

关键逻辑说明

  1. PCM数据存储格式:ReSpeaker四麦输出的raw PCM是交错存储的,顺序为:声道0采样1 → 声道1采样1 → 声道2采样1 → 声道3采样1 → 声道0采样2 → ...,每个采样占2字节(S16_LE格式)。
  2. Transform流的作用:自定义的ChannelTransformer负责处理流的分块特性(stream的chunk可能不是完整帧对齐的),缓存不完整数据,确保只处理完整的采样组,然后提取指定声道的所有采样。
  3. 多声道分发:把arecord输出的每一段数据同时传给4个Transformer,每个Transformer只提取自己负责的声道数据,最终写入独立文件。

验证方法

生成的channel_1.raw到channel_4.raw是单声道的raw PCM文件,可以用aplay命令播放验证:

# 播放第一个声道的文件
aplay -r16000 -fS16_LE -traw -c1 channel_1.raw

如果需要转换成带文件头的WAV格式,可以用sox工具:

sox -r16000 -b16 -e signed-integer -c1 channel_1.raw channel_1.wav

注意事项

  • 确保系统已安装alsa-utils包(包含arecord/aplay),且ReSpeaker的驱动已正确配置,设备名ac108可用(可以用arecord -l命令查看系统中的音频设备列表)。
  • raw格式没有文件头,播放或转换时必须指定与录制时一致的参数(采样率、位深、声道数)。

内容的提问来源于stack exchange,提问作者jincio92

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:05:46