我正在进行一个个人项目,该项目涉及从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.readFloatLE
和Buffer.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开始。