所以我尝试使用
Web Audio API
来解码和播放使用 Node.js 和 Socket.IO 流式传输到浏览器的 MP3 文件块。
在这种情况下,我唯一的选择是为收到的每个音频数据块创建一个新的
AudioBufferSourceNode
,或者是否可以为所有块创建一个 AudioBufferSourceNode
并简单地将新音频数据附加到源节点的末尾buffer
属性?
目前,这就是我接收 MP3 块、解码它们并安排它们播放的方式。我已经验证收到的每个块都是“有效的 MP3 块”并且正在被 Web Audio API 成功解码。
audioContext = new AudioContext();
startTime = 0;
socket.on('chunk_received', function(chunk) {
audioContext.decodeAudioData(toArrayBuffer(data.audio), function(buffer) {
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(startTime);
startTime += buffer.duration;
});
});
任何关于如何最好地使用新音频数据“更新”网络音频 API 播放的建议或见解将不胜感激。
目前,
decodeAudioData()
需要完整的文件,无法对不完整的文件提供基于块的解码。 下一版本的 Web Audio API 应提供此功能:https://github.com/WebAudio/web-audio-api/issues/337
同时,我已经开始编写用于解码音频块的示例,直到新的 API 版本可用。
不,你不能重用 AudioBufferSourceNode,也不能
push
到 AudioBuffer 上。它们的长度是不可变的。
这篇文章 (http://www.html5rocks.com/en/tutorials/audio/scheduling/) 有一些关于使用 Web Audio API 进行调度的好信息。但你走在正确的道路上。
我看到至少有两种可能的方法。
设置
scriptProcessorNode
,它将接收和解码数据的队列提供给网络音频的实时流。利用
audioBufferSource.loop
的属性 - 根据音频时间更新audioBuffer的内容。这两种方法都在 https://github.com/audio-lab/web-audio-stream 中实现。从技术上讲,您可以使用它将接收到的数据提供给网络音频。
目前仅使用浏览器内置 API 还没有好的解决方案:MediaSource API 不支持在我测试过的任何浏览器上解码 MP3,并且 WebCodec 尚未得到广泛支持。然而,有一个解决方案几乎可以普遍使用,直到它们为止!
假设客户端浏览器支持 WebAssembly(兼容性)和 WebAudio(兼容性)——它们都得到非常广泛的支持并在 MDN 上标记为“Baseline”——可以使用流行的 mgp123 解码器进行解码,该解码器已编译WASM 并打包在这个维护良好且广泛使用的 npm 包中;然后使用 WebAudio 的
AudioBufferSourceNode
进行播放。
这是一个快速的 TypeScript 实现(如果需要,可以轻松获取 JavaScript,通过粘贴并选择“.JS”选项卡来使用 TS Playground 发出的 JS 代码):
import { MPEGDecoderWebWorker } from 'mpg123-decoder';
export class AudioPlayer {
private decoder: MPEGDecoderWebWorker | null = null;
private audioContext: AudioContext | null = null;
private nextPieceStartTime: number = 0;
private sourceQueue: AudioBufferSourceNode[] = []; // FIFO queue to keep track of playing pieces
// Can't have async constructors, so we have a separate initializer
public async init() {
if (this.audioContext === null) {
this.audioContext = new AudioContext();
}
this.nextPieceStartTime = this.audioContext.currentTime;
if (this.decoder === null) {
this.decoder = new MPEGDecoderWebWorker();
await this.decoder.ready;
}
}
public addAudio(chunk: Uint8Array) {
this.decoder.decodeFrame(chunk).then((c) => this.playAudio(c));
}
private playAudio({ channelData, samplesDecoded, sampleRate }) {
const audioBuffer = this.audioContext.createBuffer(
channelData.length,
samplesDecoded,
sampleRate
);
for (let channel = 0; channel < channelData.length; channel++) {
const bufferChannelData = audioBuffer.getChannelData(channel);
bufferChannelData.set(channelData[channel]);
}
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
// Manage queue of sources - first in / first out
this.sourceQueue.push(source);
source.addEventListener("ended", (_) => {
this.sourceQueue.shift();
});
source.connect(this.audioContext.destination);
source.start(this.nextPieceStartTime);
this.nextPieceStartTime += audioBuffer.duration;
}
/* Can be called when playback is active, will return when last piece is done playing. */
public async onPlaybackDone() {
if (this.sourceQueue.length === 0) {
// If not playing, return immediately
return;
}
return new Promise<void>((resolve) => {
const lastSource = this.sourceQueue[this.sourceQueue.length - 1];
lastSource.addEventListener("ended", (_) => {
resolve();
})
})
}
}
然后可以轻松地使用此类:使用
const player = new AudioPlayer();
创建实例并使用 await player.init()
进行初始化,每当收到块时,调用 player.addAudio(chunk)
。如果您需要知道播放何时完成,请在最后一次 await player.onPlaybackDone();
通话后拨打 addAudio
。
结果是播放时没有暂停或延迟(当然,如果块的接收速度足够快,可以在前一个块结束之前开始播放)。
Uint8Array
。如果您以不同的方式收到块,您需要首先转换为该格式。