我正在从事一个个人项目,其中涉及从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.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());
}
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;
}
答案 0 :(得分:1)
您不能简单地反转字节数组。如您所料,样本将跨越一个以上的字节。
看似样本格式错误似乎是合理的。它可能不是32位浮点数,但可能是带符号的16位整数。记录得不好,但是如果您深入研究node-lame
,you 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位带符号小端开始。