Web Audio API:如何播放 MP3 块流

问题描述 投票:0回答:4

所以我尝试使用

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 播放的建议或见解将不胜感激。

javascript html audio streaming web-audio-api
4个回答
8
投票

目前,

decodeAudioData()
需要完整的文件,无法对不完整的文件提供基于块的解码。 下一版本的 Web Audio API 应提供此功能:https://github.com/WebAudio/web-audio-api/issues/337

同时,我已经开始编写用于解码音频块的示例,直到新的 API 版本可用。

https://github.com/AnthumChris/fetch-stream-audio


6
投票

不,你不能重用 AudioBufferSourceNode,也不能

push
到 AudioBuffer 上。它们的长度是不可变的。

这篇文章 (http://www.html5rocks.com/en/tutorials/audio/scheduling/) 有一些关于使用 Web Audio API 进行调度的好信息。但你走在正确的道路上。


1
投票

我看到至少有两种可能的方法。

  1. 设置

    scriptProcessorNode
    ,它将接收和解码数据的队列提供给网络音频的实时流。

  2. 利用

    audioBufferSource.loop
    的属性 - 根据音频时间更新audioBuffer的内容。

这两种方法都在 https://github.com/audio-lab/web-audio-stream 中实现。从技术上讲,您可以使用它将接收到的数据提供给网络音频。


0
投票

目前仅使用浏览器内置 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
。如果您以不同的方式收到块,您需要首先转换为该格式。

© www.soinside.com 2019 - 2024. All rights reserved.