如何在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()}`); });
关键逻辑说明
- PCM数据存储格式:ReSpeaker四麦输出的raw PCM是交错存储的,顺序为:
声道0采样1 → 声道1采样1 → 声道2采样1 → 声道3采样1 → 声道0采样2 → ...,每个采样占2字节(S16_LE格式)。 - Transform流的作用:自定义的
ChannelTransformer负责处理流的分块特性(stream的chunk可能不是完整帧对齐的),缓存不完整数据,确保只处理完整的采样组,然后提取指定声道的所有采样。 - 多声道分发:把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
相关产品推荐
相关产品推荐

