在NodeJS中处理原始PCM数据的缓冲区

时间:2018-11-14 03:20:09

标签: node.js audio

我正在从事一个个人项目,其中涉及从YouTube检索音频,处理音频并将结果流式传输到浏览器。到目前为止,我已经走下了第一步,也是最后一步,但是中间正在证明是一个挑战。

借助youtube-audio-stream软件包,获得音频非常容易。我想处理原始音频样本,所以我遵循了他们的README示例,并将流通过lame包通过管道传递到Decoder。

我将几个流转换放在一起……一个将输入的块合并在一起,直到满足大小阈值为止,另一个将这些块实际做些事情。在管道的最后,我添加了一个wav编写器(它添加了WAV标头,因此浏览器不会对传入的原始数据感到困惑)。

如果我的音频变换仅沿块传递而不进行任何修改,则实际上会导致正常的音频输出。因此,我知道管道本身并未中断。但是由于某些原因,执行以下操作会导致杂乱的声音:

chunk.reverse();

(这不是最终目标-涉及FFT-但我认为反转音频块是一个很好的操作。)

我希望这可以将流转换成声音的反向片段,但会扭曲它,使其无法识别。我知道Node.js缓冲区是Uint8Arrays,所以我想知道每个样本是否存储为4个单独的8位整数。但是我尝试做这样的事情:

const arr = Float32Array.from(chunk);
this.push(new Buffer(arr.reverse()));

它仍然是乱码。我还尝试编写一个使用Buffer.readFloatLEBuffer.writeFloatLE的循环,但也未达到预期的效果。我在这里想念什么?如何在Node.js缓冲区中检索和设置音频样本数据?

编辑:添加示例代码(我正在使用micro作为微服务在本地运行):

index.js

const stream = require('youtube-audio-stream');
const wav = require('wav');
const decoder = require('lame').Decoder;
const { Chunker, AudioThing } = require('./transforms');

module.exports = (req, res) => {
  const url = 'https://www.youtube.com/watch?v=-L7IdUqaZxo';
  res.setHeader('Content-Type', 'audio/wav');
  return stream(url)
    .pipe(decoder())
    .pipe(new Chunker(2 ** 16))
    .pipe(new AudioThing())
    .pipe(new wav.Writer());
}

transforms.js

const { Transform } = require('stream');

class Chunker extends Transform {
  constructor(threshold) {
    super();
    this.size = 0;
    this.chunks = [];
    this.threshold = threshold;
  }

  _transform(chunk, encoding, done) {
    this.size += chunk.length;
    this.chunks.push(chunk);
    if (this.size >= this.threshold) {
      this.push(Buffer.concat(this.chunks, this.size));
      this.chunks = [];
      this.size = 0;
    }
    done();
  }
}

class AudioThing extends Transform {
  _transform(chunk, encoding, done) {
    this.push(chunk.reverse());
    done();
  }
}

module.exports = { Chunker, AudioThing };

编辑2:解决了!为了将来参考,以下是我编写的用于对音频数据进行解码/编码的实用程序功能:

function decodeBuffer (buffer) {
  return Array.from(
    { length: buffer.length / 2 },
    (v, i) => buffer.readInt16LE(i * 2) / (2 ** 15)
  );
}

function encodeArray (array) {
  const buf = Buffer.alloc(array.length * 2);
  for (let i = 0; i < array.length; i++) {
    buf.writeInt16LE(array[i] * (2 ** 15), i * 2);
  }
  return buf;
}

1 个答案:

答案 0 :(得分:1)

您不能简单地反转字节数组。如您所料,样本将跨越一个以上的字节。

看似样本格式错误似乎是合理的。它可能不是32位浮点数,但可能是带符号的16位整数。记录得不好,但是如果您深入研究node-lameyou find this的源代码:

if (ret == MPG123_NEW_FORMAT) {
  var format = binding.mpg123_getformat(mh);
  debug('new format: %j', format);
  self.emit('format', format);
  return read();
}

基础MPG123似乎可以return PCM in several formats

  if (ret == MPG123_OK) {
    Local<Object> o = Nan::New<Object>();
    Nan::Set(o, Nan::New<String>("raw_encoding").ToLocalChecked(), Nan::New<Number>(encoding));
    Nan::Set(o, Nan::New<String>("sampleRate").ToLocalChecked(), Nan::New<Number>(rate));
    Nan::Set(o, Nan::New<String>("channels").ToLocalChecked(), Nan::New<Number>(channels));
    Nan::Set(o, Nan::New<String>("signed").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_SIGNED));
    Nan::Set(o, Nan::New<String>("float").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_FLOAT));
    Nan::Set(o, Nan::New<String>("ulaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ULAW_8));
    Nan::Set(o, Nan::New<String>("alaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ALAW_8));
    if (encoding & MPG123_ENC_8)
      Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(8));
    else if (encoding & MPG123_ENC_16)
      Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(16));
    else if (encoding & MPG123_ENC_24)
      Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(24));
    else if (encoding & MPG123_ENC_32 || encoding & MPG123_ENC_FLOAT_32)
      Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(32));
    else if (encoding & MPG123_ENC_FLOAT_64)
      Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(64));
    rtn = o;

我会再次尝试使用循环技术反转样本,同时保持每个样本中的字节完整无缺,但是尝试使用不同的样本大小进行尝试。从16位带符号小端开始。