在NodeJS中操作原始PCM数据的缓冲区



我正在进行一个个人项目,该项目涉及从YouTube检索音频、操纵音频并将结果流式传输到浏览器。到目前为止,我已经迈出了第一步和最后一步,但事实证明,中间阶段是一个挑战。

多亏了youtube-audio-stream软件包,获取音频变得很容易。我想处理原始音频样本,所以我遵循了他们的README示例,并将流从lame包管道传输到解码器中。

我做了几次流转换。。。一个是将传入的块合并在一起,直到达到大小阈值,另一个是实际处理这些块。在管道的最后,我添加了一个wav编写器(它添加了wav头,这样浏览器就不会对输入的原始数据感到困惑(。

如果我的音频变换只是在没有任何修改的情况下沿着块传递,这实际上会导致正常的音频输出。所以我知道管道本身并没有破裂。但由于某些原因,执行以下操作会导致混乱的噪声:

chunk.reverse();

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

我本以为这会把声音流转化为相反的声音片段,但它却把它扭曲得面目全非。我知道Node.js缓冲区是Uint8Array,所以我想知道每个样本是否存储为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());
}

转换.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;
}

您不能简单地反转字节数组。正如您所怀疑的,样本将跨越多个字节。

你的样本格式似乎有误。它可能不是32位浮点,但可能是有符号的16位整数。这并没有很好的记录,但如果你深入研究node-lame的源代码,你会发现:

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

看起来底层的MPG123可以以几种格式返回PCM:

  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位带符号的小endian开始。

最新更新