实现一个简单的实时对讲功能,将一台电脑的语音实时传输到另一台电脑并播放。

Socket转发

websocket可以直接转发音频流,无需做更多处理

let WebSocketServer = require('ws').Server
let WebSocket = require('ws')

const wss = new WebSocketServer({ port: 1041 });//服务端口8181
wss.on('connection', function (ws) {
    console.log('客户端已连接');
    ws.on('message', (data, isBinary) => {
        // 收到消息以后,转发给所有连接的客户端
        wss.clients.forEach(function each(client) {
             if (client !== ws && client.readyState === WebSocket.OPEN) {
                 client.send(data, { binary: isBinary });
             }
        });
    });
});

通过麦克风获取声音并传输

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button id="start">start</button>
    <button id="stop">startstop</button>
    <script>
        // 连接 websocket
        const ws = new WebSocket('ws://192.168.220.223:1041')
        ws.onopen = () => {
            console.log('socket 已连接')
        }
        ws.onerror = (e) => {
            console.log('error', e);
        }
        ws.onclose = () => {
            console.log('socket closed')
        }

        document.getElementById('start').onclick = function () {
            // 该变量存储当前MediaStreamAudioSourceNode的引用
            // 可以通过它关闭麦克风停止音频传输
            let mediaStack
            let audioCtx = new AudioContext();
            // 创建一个ScriptProcessorNode 用于接收当前麦克风的音频
            let scriptNode = audioCtx.createScriptProcessor(4096, 1, 1);

            navigator.mediaDevices.getUserMedia({ audio: true, video: false })
                .then(function (stream) {
                    mediaStack = stream
                    var source = audioCtx.createMediaStreamSource(stream)
                    
                    source.connect(scriptNode);
                    scriptNode.connect(audioCtx.destination);
                })
                .catch(function (err) {
                    /* 处理error */
                    console.log('err', err)
                });
            // 当麦克风有声音输入时,会调用此事件
            // 实际上麦克风始终处于打开状态时,即使不说话,此事件也在一直调用
            scriptNode.onaudioprocess = function (audioProcessingEvent) {
                let inputBuffer = audioProcessingEvent.inputBuffer;
                // 由于只创建了一个音轨,这里只取第一个频道的数据
                let inputData = inputBuffer.getChannelData(0);
                console.log(inputData);
                // 通过socket传输数据,实际上传输的是Float32Array
                ws.send(inputData)
            }

            // 关闭麦克风
            document.getElementById('startstop').onclick = function () {
                mediaStack.getTracks()[0].stop()
                scriptNode.disconnect()
            };
        }


    </script>
</body>

</html>

获取socket传输过来的音频流并播放

处理方式一

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button onclick="play()">play</button>
    <script>
        function play() {
            const audioCtx = new AudioContext();
            // 连接socket
            const ws = new WebSocket('ws://127.0.0.1:1041')
            ws.onopen = () => {
                console.log('socket opened')
            }
            // 接收的数据类型是arraybuffer
            ws.binaryType = 'arraybuffer'
            ws.onmessage = ({data}) => {
                // 将接收的数据转换成与传输过来的数据相同的Float32Array
                const buffer = new Float32Array(data)
                // 创建一个空白的AudioBuffer对象,这里的4096跟发送方保持一致,48000是采样率
                const myArrayBuffer = audioCtx.createBuffer(1, 4096, 48000);
                // 也是由于只创建了一个音轨,可以直接取到0
                const nowBuffering = myArrayBuffer.getChannelData(0);
                // 通过循环,将接收过来的数据赋值给简单音频对象
                for (let i = 0; i < 4096; i++) {
                    nowBuffering[i] = buffer[i];
                }
                // 使用AudioBufferSourceNode播放音频                         
                const source = audioCtx.createBufferSource();
                source.buffer = myArrayBuffer
                source.connect(audioCtx.destination);
                source.start();
            }
            ws.onerror = (e) => {
                console.log('error', e);
            }
            ws.onclose = () => {
                console.log('socket closed');
            }
        }
    </script>
</body>

</html>

处理方式二

<html>

<head>
</head>

<body>
    <script>

        var audio_context, gain_node, source_node;
        var chosen_audio_file_url = "output_2.wav";

        // log if an error occurs
        function on_error(e) {

            console.log("ERROR - " + e);
        }

        try {

            window.AudioContext = window.AudioContext ||
            window.webkitAudioContext ||
            window.mozAudioContext ||
            window.oAudioContext ||
            window.msAudioContext;

            audio_context = new AudioContext();
            console.log("cool audio context established");

        } catch (e) {

            alert("Web Audio API is not supported by this browser");
        }


        gain_node = audio_context.createGain(); // Declare gain node
        gain_node.connect(audio_context.destination); // Connect gain node to speakers

        source_node = audio_context.createBufferSource();
        source_node.connect(gain_node);

        var request = new XMLHttpRequest();
        request.open('GET', chosen_audio_file_url, true);
        request.responseType = 'arraybuffer';

        // When loaded decode the data
        request.onload = function() {

            // decode the data
            audio_context.decodeAudioData(request.response, function(buffer) {

                console.log(chosen_audio_file_url, ' ... buffer.length ', buffer.length);

                source_node.buffer = buffer;
                source_node.start(0);

                // ---

            }, on_error);
        }

        function run() {
            request.send();
        }

    </script>

    <a href="javascript:run()">run</a>
</body>

</html>

Web Audio之getChannelData

// 两个通道,也就是立体声
let channels = 2;

// 总共2s,乘以sampleRate采样率就是总的PCM数据长度
let frameCount = audioCtx.sampleRate * 2.0;
// 创建一个buffer,三个参数分别是通道数,存贮在缓冲区中的PCM数据的长度,采样率
let myArrayBuffer = audioCtx.createBuffer(channels, frameCount, audioCtx.sampleRate);

button.onclick = function() {
  // 对每一个频道都填充白噪音
  for (var channel = 0; channel < channels; channel++) {
    // 当前的频道,总共填充两个频道
    var nowBuffering = myArrayBuffer.getChannelData(channel);
    for (let i = 0; i < frameCount; i++) {
      // 对当前频道填充-1到1的随机数
      nowBuffering[i] = Math.random() * 2 - 1;
    }
  }
 
  // 创建一个AudioBuffer的容器,也就是AudioBufferSourceNode
  let source = audioCtx.createBufferSource();
  source.buffer = myArrayBuffer;

  // 链接到总输出
  source.connect(audioCtx.destination);

  // 播放
  source.start();
}