如何播放从 websocket 流接收的 PCM 音频?

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

问题:我正在使用 NodeJS 制作一个应用程序,用户加载页面,麦克风将数据传输到 NodeJS(我使用 Socket.IO 作为 websocket 部分)。我的流媒体工作正常,但现在我想知道如何播放收到的音频?

这是我从流中收到的消息的图片,我尝试在浏览器上播放它,我猜它是 PCM 音频,但我不是专家。 https://i.stack.imgur.com/bZzfs.png这个物体有1023长。

我在浏览器上使用的代码如下(太长了,直接放在这里):https://gist.github.com/ZeroByter/f5690fa9a7c20e2b24cccaa5a8cf3b86

问题:我从

这里
撕下了socket.on("mic")。但我不确定它如何有效地播放它正在接收的音频数据。

这不是我第一次使用 WebSocket,我非常了解 WebSocket 工作的基础知识,但这是我第一次使用 Web Audio API。所以我需要一些帮助。

javascript node.js audio websocket pcm
2个回答
2
投票

是的,您的图像剪辑看起来确实像 PCM 音频,这是 Web 音频 API 友好的

我编写了这样一个基于 Web Socket 的浏览器客户端,使用 Web Audio API 渲染从我的 Nodejs 服务器接收到的 PCM 音频...渲染音频很简单,但是必须在单线程 javascript 环境中照顾 Web Socket,才能接收下一个音频缓冲区本质上是抢占式的,如果没有下面概述的技巧,将导致可听见的爆音/故障

最终有效的解决方案是将所有 Web Socket 逻辑放入 Web Worker 中,该 Web Worker 填充 WW 侧循环队列。然后,浏览器端从 WW 队列中提取下一个音频缓冲区的数据,并填充从 Wed Audio API 事件循环内部驱动的 Web Audio API 内存缓冲区。这一切都归结为不惜一切代价避免在浏览器端进行任何实际工作,这会导致音频事件循环匮乏或无法及时完成以服务其自己的下一个事件循环事件。

我写这篇文章是我第一次涉足 javascript,所以……你还必须使用浏览器 F5 来重新加载屏幕以播放不同的流(4 个不同的音频源文件可供选择)……

https://github.com/scottstensland/websockets-streaming-audio

我想简化使用,成为 API 驱动,而不是融入到相同的代码库中(将低级逻辑与用户空间调用分开)

希望这有帮助

更新 - 这个 git 存储库使用 Web Audio API 渲染麦克风音频 - 这个独立的示例展示了如何访问音频内存缓冲区...存储库还有另一个最小的内联 html 示例,可以播放所示的麦克风音频

<html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>capture microphone then show time & frequency domain output</title>

<script type="text/javascript">

var webaudio_tooling_obj = function () {

    // see this code at repo :   
    //    https://github.com/scottstensland/webaudioapi-microphone

    // if you want to see logic to access audio memory buffer
    // to record or send to downstream processing look at
    // other file :  microphone_inline.html
    // this file contains just the mimimum logic to render mic audio

    var audioContext = new AudioContext(); // entry point of Web Audio API

    console.log("audio is starting up ...");

    var audioInput = null,
    microphone_stream = null,
    gain_node = null,
    script_processor_node = null,
    script_processor_analysis_node = null,
    analyser_node = null;

    // get browser media handle
    if (!navigator.getUserMedia)
        navigator.getUserMedia =navigator.getUserMedia || 
                                navigator.webkitGetUserMedia ||
                                navigator.mozGetUserMedia || 
                                navigator.msGetUserMedia;

    if (navigator.getUserMedia) { //register microphone as source of audio

        navigator.getUserMedia({audio:true}, 
            function(stream) {
                start_microphone(stream);
            },
            function(e) {
                alert('Error capturing audio.');
            }
            );

    } else { alert('getUserMedia not supported in this browser.'); }

    // ---

    function start_microphone(stream) {

        // create a streaming audio context where source is microphone
        microphone_stream = audioContext.createMediaStreamSource(stream);

        // define as output of microphone the default output speakers
        microphone_stream.connect( audioContext.destination ); 

    } // start_microphone

}(); //  webaudio_tooling_obj = function()

</script>

</head>
<body></body>
</html>

我在上面的 git repo 中给出了如何设置此文件...上面的代码显示了在浏览器中使用 Web Audio API 渲染音频(麦克风)的最小逻辑


0
投票

在我的实验中,从任意源(例如 websocket)传输音频的正确方法是使用 AudioWorkletProcessor

最小代码示例。假设来自套接字的 16 位 48 kHz PCM 数据。 这会连接到 http://localhost/ws 上的 websocket

const sample_rate = 48000; // Hz

// Websocket url
const ws_url = "http://localhost/ws"

let audio_context = null;
let ws = null;

async function start() {
    if (ws != null) {
        return;
    }

    // Create an AudioContext that plays audio from the AudioWorkletNode  
    audio_context = new AudioContext();
    await audio_context.audioWorklet.addModule('audioProcessor.js');
    const audioNode = new AudioWorkletNode(audio_context, 'audio-processor');
    audioNode.connect(audio_context.destination);

    // Setup the websocket 
    ws = new WebSocket(ws_url);
    ws.binaryType = 'arraybuffer';

    // Process incoming messages
    ws.onmessage =  (event) => {
        // Convert to Float32 lpcm, which is what AudioWorkletNode expects
        const int16Array = new Int16Array(event.data);
        let float32Array = new Float32Array(int16Array.length);
        for (let i = 0; i < int16Array.length; i++) {
            float32Array[i] = int16Array[i] / 32768.; 
        }

        // Send the audio data to the AudioWorkletNode
        audioNode.port.postMessage({ message: 'audioData', audioData: float32Array });
    }
    
    ws.onopen = () => {
        console.log('WebSocket connection opened.');
    };

    ws.onclose = () => {
        console.log('WebSocket connection closed.');
    };
    
    ws.onerror = error => {
        console.error('WebSocket error:', error);
    };
}

async function stop() {
    console.log('Stopping audio');
    if (audio_context) {
        await audio_context.close();
        audio_context = null;
        ws.close();
        ws = null;
    }
}

这还需要:

音频处理器.js

class AudioProcessor extends AudioWorkletProcessor {

    constructor() {
        super();
        this.buffer = new Float32Array();

        // Receive audio data from the main thread, and add it to the buffer
        this.port.onmessage = (event) => {
            let newFetchedData = new Float32Array(this.buffer.length + event.data.audioData.length);
            newFetchedData.set(this.buffer, 0);
            newFetchedData.set(event.data.audioData, this.buffer.length); 
            this.buffer = newFetchedData;
        };
    }

    // Take a chunk from the buffer and send it to the output to be played
    process(inputs, outputs, parameters) {
        const output = outputs[0];
        const channel = output[0];
        const bufferLength = this.buffer.length;
        for (let i = 0; i < channel.length; i++) {
            channel[i] = (i < bufferLength) ? this.buffer[i] : 0;
        }
        this.buffer = this.buffer.slice(channel.length);
        return true;
    }
}

registerProcessor('audio-processor', AudioProcessor);
© www.soinside.com 2019 - 2024. All rights reserved.