首页 > 解决方案 > NodeJS:将立体声 PCM 波流捕获到单声道 AudioBuffer

问题描述

我正在使用node-microphone(这只是 arecord 的 javascript 接口)从 nodejs 录制音频,并希望将流块存储在AudioBufferusing web-audio-api(这是 Web Audio API 的 nodejs 实现)中。

我的音频源有两个通道,而我AudioBuffer只有一个(故意)。

这是我通过 USB 声卡使用 arecord 录制音频的工作配置(我使用的是在 Raspbian buster 上运行的 Raspberry pi 3):

arecord -D hw:1,0 -c 2 -f S16_LE -r 44100

使用输出路径运行此命令并使用 aplay 播放生成的 wav 文件就可以了。所以 node-microphone 能够使用这些参数录制音频,最后我得到一个 nodejs 可读的流数据流。

我正在努力做从流块(Buffer实例)到AudioBuffer. 更确切地说; 我不确定传入数据的格式,不确定目标格式,也不确定如何进行转换:

流块是Buffers,所以它们也是Uint8Arrays。关于我的配置,我猜它们是 16 位有符号整数的二进制表示(小端,我不知道它是什么意思)。

AudioBuffer保存多个缓冲区(每个通道一个,所以在我的情况下只有一个),我可以通过Float32Array调用AudioBuffer.prototype.getChannelData(). MDN还说:

缓冲区包含以下格式的数据:非交错 IEEE754 32 位线性 PCM,标称范围在 -1 和 +1 之间,即 32 位浮点缓冲区,每个样本在 -1.0 和 1.0 之间。

关键是要找到我必须从传入Buffer的 s 中提取的内容以及我应该如何转换它以使其适合Float32Array目的地(并且仍然是有效的波形数据),知道音频源是立体声而AudioBuffer不是立体声。

到目前为止,我最好的竞争者是Buffer.prototype.readFloatLE()那个名字看起来可以解决我的问题的方法,但这并不成功(只是噪音)。

我的第一次尝试(在进行研究之前)只是天真地将缓冲区数据复制到Float32Array并交叉索引以处理立体声/单声道转换。显然它主要产生噪音,但我可以听到我录制的一些声音(令人难以置信的失真但肯定存在)所以我想我应该提到这一点。

这是我天真的尝试的简化版本(我知道这并不意味着效果很好,我只是将它包含在我的问题中作为讨论的基础):

import { AudioBuffer } from 'web-audio-api'
import Microphone from 'node-microphone'

const rate = 44100
const channels = 2 // Number of source channels

const microphone = new Microphone({ // These parameters result to the arecord command above
  channels,
  rate,
  device: 'hw:1,0',
  bitwidth: 16,
  endian: 'little',
  encoding: 'signed-integer'
})

const audioBuffer = new AudioBuffer(
  1, // 1 channel
  30 * rate, // 30 seconds buffer
  rate
})

const chunks = []
const data = audioBuffer.getChannelData(0) // This is the Float32Array
const stream = microphone.startRecording()

setTimeout(() => microphone.stopRecording(), 5000) // Recording for 5 seconds

stream.on('data', chunk => chunks.push(chunk))

stream.on('close', () => {
  chunks.reduce((offset, chunk) => {
    for (var index = 0; index < chunk.length; index += channels) {
      let value = 0

      for (var channel = 0; channel < channels; channel++) {
        value += chunk[index + channel]
      }

      data[(offset + index) / channels] = value / channels // Average value from the two channels
    }

    return offset + chunk.length // Since data comes as chunks, this offsets AudioBuffer's index
  }, 0)
})

如果您能提供帮助,我将不胜感激:)

标签: javascriptnode.jsaudioweb-audio-apialsa

解决方案


因此输入立体声信号以 16 位有符号整数的形式出现,左右声道交错,这意味着相应的缓冲区(8 位无符号整数)对于单个立体声样本具有以下格式:

[LEFT ] 8 bits (LSB)
[LEFT ] 8 bits (MSB)
[RIGHT] 8 bits (LSB)
[RIGHT] 8 bits (MSB)

由于 arecord 配置为little endian格式,因此最低有效字节(LSB) 位于第一位,最高有效字节(MSB) 紧随其后。

由 a 表示的AudioBuffer单通道缓冲区Float32Array需要介于-1和之间的1值(每个样本一个值)。

因此,要将值从输入映射Buffer到目标Float32Array,我必须使用Buffer.prototype.readInt16LE(offset)方法将 bytesoffset参数增加 4 每个样本(2 个左字节 + 2 个右字节 = 4 个字节),并从范围内插值输入值[-32768;+32768](16 位有符号整数范围) 到范围[-1;+1]

import { AudioBuffer } from 'web-audio-api'
import Microphone from 'node-microphone'

const rate = 44100
const channels = 2 // 2 input channels

const microphone = new Microphone({
  channels,
  rate,
  device: 'hw:1,0',
  bitwidth: 16,
  endian: 'little',
  encoding: 'signed-integer'
})

const audioBuffer = new AudioBuffer(
  1, // 1 channel
  30 * rate, // 30 seconds buffer
  rate
})

const chunks = []
const data = audioBuffer.getChannelData(0)
const stream = microphone.startRecording()

setTimeout(() => microphone.stopRecording(), 5000) // Recording for 5 seconds

stream.on('data', chunk => chunks.push(chunk))

stream.on('close', () => {
  chunks.reduce((offset, chunk) => {
    for (var index = 0; index < chunk.length; index += channels + 2) {
      let value = 0

      for (var channel = 0; channel < channels; channel++) {
        // Iterates through input channels and adds the values
        // of all the channel so we can compute the
        // average value later to reduce them into a mono signal

        // Multiplies the channel index by 2 because
        // there are 2 bytes per channel sample

        value += chunk.readInt16LE(index + channel * 2)
      }

      // Interpolates index according to the number of input channels
      // (also divides it by 2 because there are 2 bytes per channel sample)
      // and computes average value as well as the interpolation
      // from range [-32768;+32768] to range [-1;+1]
      data[(offset + index) / channels / 2] = value / channels / 32768
    }

    return offset + chunk.length
  }, 0)
})

推荐阅读