html5使用websocket发送(PCM)音频数据到服务器,再转在wav文件

本文详细介绍了通过WebSocket发送PCM格式音频数据的过程,包括使用JavaScript录制音频、压缩、编码及发送PCM数据,同时演示了如何接收并转换PCM数据为WAV格式,以及在服务器端保存和播放PCM文件的方法。

PCM格式

 

 

通过websocket发送音频数据。 

<html>
<head>
    <meta charset="utf-8">
    <title>test</title>
</head>
<body>
<div>
    <button id="intercomBegin">开始对讲</button>
    <button id="intercomEnd">关闭对讲</button>
</div>
</body>
<script>
    var begin = document.getElementById('intercomBegin');
    var end = document.getElementById('intercomEnd');
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
    var ws = null;//实现WebSocket
    var record=null;//多媒体对象,用来处理音频
    var timeInte = null;//定义一个定时器
    begin.onclick = function() {
        console.log('开始对讲')
    }
    end.onclick = function() {
        console.log('关闭对讲')
        if(ws) {
            ws.close();
            record.stop();
            clearInterval(timeInte);
        }
    }

    function init(rec){
        record = rec;
    }
    if (!navigator.getUserMedia) {
        alert('浏览器不支持音频输入');
    }else{
        navigator.getUserMedia(
            { audio: true },
            function (mediaStream) {
                init(new Recorder(mediaStream));
            },function(error){
                console.log(error)
            }
        )
    }
    //录音对象
    var Recorder = function(stream) {
        var sampleBits = 16;//输出采样数位 8, 16
        var sampleRate = 8000;//输出采样率
        var context = new AudioContext();
        var audioInput = context.createMediaStreamSource(stream);
        var recorder = context.createScriptProcessor(4096, 1, 1);
        var audioData = {
            size: 0          //录音文件长度
            , buffer: []    //录音缓存
            , inputSampleRate: sampleRate    //输入采样率
            , inputSampleBits: 16      //输入采样数位 8, 16
            , outputSampleRate: sampleRate
            , oututSampleBits: sampleBits
            , clear: function() {
                this.buffer = [];
                this.size = 0;
            }
            , input: function (data) {
                this.buffer.push(new Float32Array(data));
                this.size += data.length;
            }
            , compress: function () { //合并压缩
                //合并
                var data = new Float32Array(this.size);
                var offset = 0;
                for (var i = 0; i < this.buffer.length; i++) {
                    data.set(this.buffer[i], offset);
                    offset += this.buffer[i].length;
                }
                //压缩
                var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
                var length = data.length / compression;
                var result = new Float32Array(length);
                var index = 0, j = 0;
                while (index < length) {
                    result[index] = data[j];
                    j += compression;
                    index++;
                }
                return result;
            }, encodePCM: function(){//这里不对采集到的数据进行其他格式处理,如有需要均交给服务器端处理。
                var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
                var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
                var bytes = this.compress();
                var dataLength = bytes.length * (sampleBits / 8);
                var buffer = new ArrayBuffer(dataLength);
                var data = new DataView(buffer);
                var offset = 0;
                for (var i = 0; i < bytes.length; i++, offset += 2) {
                    var s = Math.max(-1, Math.min(1, bytes[i]));
                    data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                }
                return new Blob([data]);
            }
        };
        this.start = function () {
            audioInput.connect(recorder);
            recorder.connect(context.destination);
        }

        this.stop = function () {
            recorder.disconnect();
        }

        this.getBlob = function () {
            return audioData.encodePCM();
        }

        this.clear = function() {
            audioData.clear();
        }

        recorder.onaudioprocess = function (e) {
            audioData.input(e.inputBuffer.getChannelData(0));
        }
    };


    function receive(data) {
        if( typeof e == 'string' && JSON.parse(e).message=='OK'){
            console.log('OK');
        }else{
            var buffer = (new Response(data)).arrayBuffer();
            buffer.then(function(buf){
                console.log("################recv start ####################################");
                var audioContext = new ( window.AudioContext || window.webkitAudioContext )();
                var fileResult =addWavHeader(buf, '8000', '16', '1');//解析数据转码wav
                audioContext.decodeAudioData(fileResult, function(buffer) {
                    _visualize(audioContext,buffer);//播放
                });
                console.log("################recv end ####################################");
            });
        }
    }


    //处理音频流,转码wav
    var addWavHeader = function(samples,sampleRateTmp,sampleBits,channelCount){
        var dataLength = samples.byteLength;
        var buffer = new ArrayBuffer(44 + dataLength);
        var view = new DataView(buffer);
        function writeString(view, offset, string){
            for (var i = 0; i < string.length; i++){
                view.setUint8(offset + i, string.charCodeAt(i));
            }
        }
        var offset = 0;
        /* 资源交换文件标识符 */
        writeString(view, offset, 'RIFF'); offset += 4;
        /* 下个地址开始到文件尾总字节数,即文件大小-8 */
        view.setUint32(offset, /*32*/ 36 + dataLength, true); offset += 4;
        /* WAV文件标志 */
        writeString(view, offset, 'WAVE'); offset += 4;
        /* 波形格式标志 */
        writeString(view, offset, 'fmt '); offset += 4;
        /* 过滤字节,一般为 0x10 = 16 */
        view.setUint32(offset, 16, true); offset += 4;
        /* 格式类别 (PCM形式采样数据) */
        view.setUint16(offset, 1, true); offset += 2;
        /* 通道数 */
        view.setUint16(offset, channelCount, true); offset += 2;
        /* 采样率,每秒样本数,表示每个通道的播放速度 */
        view.setUint32(offset, sampleRateTmp, true); offset += 4;
        /* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
        view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset +=4;
        /* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
        view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
        /* 每样本数据位数 */
        view.setUint16(offset, sampleBits, true); offset += 2;
        /* 数据标识符 */
        writeString(view, offset, 'data'); offset += 4;
        /* 采样数据总数,即数据总大小-44 */
        view.setUint32(offset, dataLength, true); offset += 4;
        function floatTo32BitPCM(output, offset, input){
            input = new Int32Array(input);
            for (var i = 0; i < input.length; i++, offset+=4){
                output.setInt32(offset,input[i],true);
            }
        }
        function floatTo16BitPCM(output, offset, input){
            input = new Int16Array(input);
            for (var i = 0; i < input.length; i++, offset+=2){
                output.setInt16(offset,input[i],true);
            }
        }
        function floatTo8BitPCM(output, offset, input){
            input = new Int8Array(input);
            for (var i = 0; i < input.length; i++, offset++){
                output.setInt8(offset,input[i],true);
            }
        }
        if(sampleBits == 16){
            floatTo16BitPCM(view, 44, samples);
        }else if(sampleBits == 8){
            floatTo8BitPCM(view, 44, samples);
        }else{
            floatTo32BitPCM(view, 44, samples);
        }
        return view.buffer;
    }
    //播放音频
    var _visualize = function(audioContext, buffer) {
        var audioBufferSouceNode = audioContext.createBufferSource(),
            analyser = audioContext.createAnalyser(),
            that = this;
        //将信号源连接到分析仪
        audioBufferSouceNode.connect(analyser);
        //将分析仪连接到目的地(扬声器),否则我们将听不到声音
        analyser.connect(audioContext.destination);
        //然后将缓冲区分配给缓冲区源节点
        audioBufferSouceNode.buffer = buffer;
        //发挥作用
        if (!audioBufferSouceNode.start) {
            audioBufferSouceNode.start = audioBufferSouceNode.noteOn //在旧浏览器中使用noteOn方法
            audioBufferSouceNode.stop = audioBufferSouceNode.noteOff //在旧浏览器中使用noteOff方法
        };
        //如果有的话,停止前一个声音
        if (this.animationId !== null) {
            cancelAnimationFrame(this.animationId);
        }
        audioBufferSouceNode.start(0);
        audo.source = audioBufferSouceNode;
        audo.audioContext = audioContext;
    }


    begin.onclick = function() {

        var ws = new WebSocket("ws://127.0.0.1:6200");
        ws.binaryType = 'arraybuffer'; //传输的是 ArrayBuffer 类型的数据
        ws.onopen = function(event) {
            console.log('握手成功');
            //业务命令构建
            var data = {
                "cmd": "jtv",//发送命令
                "id": "018665897939",//发送设备id
                "type": 1,//对讲类型
                "channel": 0//语音通道
            }
            //ws.send(JSON.stringify(data));   //这里是发送消息。不包括音频数据,先注掉
        };
        timeInte=setInterval(function(){
            if(ws.readyState==1){//ws进入连接状态,则每隔500毫秒发送一包数据
                record.start();
                console.log("#######################send Blob start ##############################");
                console.log(record.getBlob());
                ws.send(record.getBlob());    //发送音频数据
                console.log("#######################send Blob end ##############################");
                record.clear();	//每次发送完成则清理掉旧数据
            }
        },500);  //每隔500ms发送一次,定时器

        ///////////////////////////////////////////////////////////////////////
        ws.onmessage = function (evt){

            console.log( "Received Message: " + evt.data);

            receive(evt.data);

        }
        ///////////////////////////////////////////////////////////////////////
    }


</script>
</html>

通过服务器来接收音频数据。这个时间可以以文件形式保存下来为.pcm就可以了。这里就使用工具来接收

 

 

最后生成pcm文件

 

 

保存好用.用Audacity工具来播放,看是不是正常的音频文件

 

 

播放正常。证明是个pcm的音频文件。得到了pcm(裸数据),我们可以做一个把pcm转成wav文件

 

#include <stdio.h>
#include <string.h>

/**
 * Convert PCM16LE raw data to WAVE format
 * @param pcmpath       Input PCM file.
 * @param channels      Channel number of PCM file.
 * @param sample_rate   Sample rate of PCM file.
 * @param wavepath      Output WAVE file.
 */
int simplest_pcm16le_to_wave(const char *pcmpath, int channels, int sample_rate, const char *wavepath)
{

    printf("#########################1111#############################\n");
    typedef struct WAVE_HEADER{
        char    fccID[4];       //内容为""RIFF
        unsigned long dwSize;   //最后填写,WAVE格式音频的大小
        char    fccType[4];     //内容为"WAVE"
    }WAVE_HEADER;

    typedef struct WAVE_FMT{
        char    fccID[4];          //内容为"fmt "
        unsigned long  dwSize;     //内容为WAVE_FMT占的字节数,为16
        unsigned short wFormatTag; //如果为PCM,改值为 1
        unsigned short wChannels;  //通道数,单通道=1,双通道=2
        unsigned long  dwSamplesPerSec;//采用频率
        unsigned long  dwAvgBytesPerSec;/* ==dwSamplesPerSec*wChannels*uiBitsPerSample/8 */
        unsigned short wBlockAlign;//==wChannels*uiBitsPerSample/8
        unsigned short uiBitsPerSample;//每个采样点的bit数,8bits=8, 16bits=16
    }WAVE_FMT;

    typedef struct WAVE_DATA{
        char    fccID[4];       //内容为"data"
        unsigned long dwSize;   //==NumSamples*wChannels*uiBitsPerSample/8
    }WAVE_DATA;

    if(channels==2 || sample_rate==0)
    {
        channels = 2;
        sample_rate = 44100;
    }
    int bits = 16;

    WAVE_HEADER pcmHEADER;
    WAVE_FMT    pcmFMT;
    WAVE_DATA   pcmDATA;

    unsigned short m_pcmData;
    FILE *fp, *fpout;
    printf("#########################2222#############################\n");
    fp = fopen(pcmpath, "rb+");
    if(fp==NULL)
    {
        printf("Open pcm file error.\n");
        return -1;
    }
    fpout = fopen(wavepath, "wb+");
    if(fpout==NULL)
    {
        printf("Create wav file error.\n");
        return -1;
    }
    printf("########################3333##############################\n");
    /* WAVE_HEADER */
    memcpy(pcmHEADER.fccID, "RIFF", strlen("RIFF"));
    memcpy(pcmHEADER.fccType, "WAVE", strlen("WAVE"));
    fseek(fpout, sizeof(WAVE_HEADER), 1);   //1=SEEK_CUR
    /* WAVE_FMT */
    memcpy(pcmFMT.fccID, "fmt ", strlen("fmt "));
    pcmFMT.dwSize = 16;
    pcmFMT.wFormatTag = 1;
    pcmFMT.wChannels = 2;
    pcmFMT.dwSamplesPerSec = sample_rate;
    pcmFMT.uiBitsPerSample = bits;
    /* ==dwSamplesPerSec*wChannels*uiBitsPerSample/8 */
    pcmFMT.dwAvgBytesPerSec = pcmFMT.dwSamplesPerSec*pcmFMT.wChannels*pcmFMT.uiBitsPerSample/8;
    /* ==wChannels*uiBitsPerSample/8 */
    pcmFMT.wBlockAlign = pcmFMT.wChannels*pcmFMT.uiBitsPerSample/8;

    printf("##################4444####################################\n");

    fwrite(&pcmFMT, sizeof(WAVE_FMT), 1, fpout);

    /* WAVE_DATA */
    memcpy(pcmDATA.fccID, "data", strlen("data"));
    pcmDATA.dwSize = 0;
    fseek(fpout, sizeof(WAVE_DATA), SEEK_CUR);
    printf("##################5555####################################\n");
    fread(&m_pcmData, sizeof(unsigned short), 1, fp);
    while(!feof(fp))
    {
        pcmDATA.dwSize += 2;
        fwrite(&m_pcmData, sizeof(unsigned short), 1, fpout);
        fread(&m_pcmData, sizeof(unsigned short), 1, fp);
    }
    printf("##################4444####################################\n");
    /*pcmHEADER.dwSize = 44 + pcmDATA.dwSize;*/
    //修改时间:2018年1月5日
    pcmHEADER.dwSize = 36 + pcmDATA.dwSize;

    rewind(fpout);
    fwrite(&pcmHEADER, sizeof(WAVE_HEADER), 1, fpout);
    fseek(fpout, sizeof(WAVE_FMT), SEEK_CUR);
    fwrite(&pcmDATA, sizeof(WAVE_DATA), 1, fpout);

    fclose(fp);
    fclose(fpout);
    printf("##################6666####################################\n");
    return 0;
}

int main()
{
    simplest_pcm16le_to_wave("E:\\project\\audio\\in.pcm", 2, 44100, "E:\\project\\audio\\out.wav");

    return 0;
}

 

再用Audacity播放out.wav文件,可正常播放。

涉及到音频领域。后面文章,再继讲如何把pcm转在g711a或是aac音频。

 

<think>我们正在处理一个关于在JavaScript中通过WebSocket接收PCM数据使用Recorder.js进行录音处理的问题。根据用户的问题,我们需要实现以下步骤: 1. 建立WebSocket连接以接收PCM音频数据流。 2. 使用Recorder.js库处理接收到的PCM数据,可能包括录制、播放或保存等操作。 参考引用[1]中提到了通过WebSocket发送PCM音频,但用户的需求是接收并处理。引用[2]展示了一个Vue组件,其中使用WebSocket进行语音识别,但用户需要的是录音处理。 因此,我们将按照以下步骤进行: 1. 初始化WebSocket连接,监听来自服务器PCM数据。 2. 将接收到的二进制数据PCM)转换为Recorder.js可以处理的格式。 3. 使用Recorder.js进行录音处理(例如,录制、播放、保存等)。 注意:Recorder.js通常用于从麦克风获取音频流,但这里我们是从WebSocket接收PCM数据。因此,我们需要模拟一个音频上下文,并将接收到的PCM数据放入音频缓冲区中,然后使用Recorder.js进行编码(如转成WAV)等操作。 具体实现步骤: 步骤1:建立WebSocket连接 步骤2:当接收到PCM数据时,将其转换为Float32Array格式(因为PCM数据通常是16位整数,但Web Audio API需要32位浮点数) 步骤3:使用Recorder.js的record方法录制这些PCM数据,或者我们可以自己构建一个AudioBuffer并播放,或者保存为WAV文件。 但是,Recorder.js的设计主要是从前端麦克风采集音频,而不是从WebSocket接收的数据。因此,我们需要调整思路: 方案一:将接收到的PCM数据通过Recorder.js的`record`方法直接写入到Recorder实例中。但是Recorder.js的record方法需要传入一个AudioNode,所以这个方法可能不适用。 方案二:我们可以自己构建一个AudioBuffer,然后使用Recorder.js的`encodeWAV`方法将PCM数据编码WAV文件。Recorder.js提供了这样的工具函数。 根据Recorder.js的文档,它提供了`Recorder.Support`、`Recorder.Buffer`等工具。我们可以使用`Recorder.Support.CreateBuffer`来创建音频缓冲区,然后使用`Recorder.encodeWAV`来编码WAV。 然而,我们也可以不使用Recorder.js的录制实例,而是直接利用它的工具函数来处理我们接收到的PCM数据。 具体步骤: 1. 创建WebSocket连接,接收二进制数据(ArrayBuffer)。 2. 将接收到的ArrayBuffer转换为Int16Array(假设PCM数据是16位的)。 3. 将Int16Array转换为Float32Array(因为Web Audio API使用Float32Array)。 4. 创建一个AudioBuffer,将Float32Array的数据写入。 5. 使用Recorder.js的`encodeWAV`方法将AudioBuffer编码WAV格式的Blob。 6. 可以播放或保存这个Blob。 但是,Recorder.js的`encodeWAV`函数通常用于处理从麦克风采集的多个通道的数据,并且需要知道采样率等参数。 因此,我们需要知道服务器发送PCM数据的参数(采样率、通道数、位深等)。假设我们已知这些参数(例如,采样率16000,单声道,16位)。 实现代码的大致结构: ```javascript // 假设参数 const sampleRate = 16000; // 采样率 const numChannels = 1; // 单声道 // 创建WebSocket连接 const ws = new WebSocket('ws://example.com/audio-stream'); // 存储接收到的PCM数据(Int16Array数组) let pcmChunks = []; ws.binaryType = 'arraybuffer'; ws.onmessage = function(event) { // 接收到的数据是ArrayBuffer,转换为Int16Array const int16Data = new Int16Array(event.data); pcmChunks.push(int16Data); }; // 当需要停止并生成WAV时 function stopAndSave() { // 合并所有chunk const totalLength = pcmChunks.reduce((acc, chunk) => acc + chunk.length, 0); const mergedData = new Int16Array(totalLength); let offset = 0; pcmChunks.forEach(chunk => { mergedData.set(chunk, offset); offset += chunk.length; }); // 转换为Float32Array const float32Data = new Float32Array(mergedData.length); for (let i = 0; i < mergedData.length; i++) { float32Data[i] = mergedData[i] / 32768.0; // 16位有符号整数转浮点(-1到1) } // 创建AudioBuffer const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const audioBuffer = audioContext.createBuffer(numChannels, float32Data.length, sampleRate); // 将数据写入到第一个声道 audioBuffer.getChannelData(0).set(float32Data); // 使用Recorder.js的encodeWAV方法 const wavBlob = Recorder.encodeWAV(audioBuffer, { type: 'audio/wav' }); // 保存或播放 const url = URL.createObjectURL(wavBlob); const a = document.createElement('a'); a.href = url; a.download = 'recording.wav'; a.click(); } // 注意:Recorder.encodeWAV可能不是Recorder.js默认导出的函数,需要确认Recorder.js的API // 实际上,Recorder.js的官方文档并没有直接提供encodeWAV作为工具函数,但我们可以从Recorder.js源码中提取相关代码。 然而,标准的Recorder.js库并没有将encodeWAV作为公共方法暴露。因此,我们可能需要自己实现一个WAV编码器,或者使用其他库。 替代方案:使用Recorder.js的BufferStream类(如果可用)或者使用其他WAV编码器。 鉴于Recorder.js的设计,我们可能需要修改思路:我们不使用Recorder.js来录制,而是用它来编码。但Recorder.js的主要功能是通过麦克风输入,所以我们可以模拟一个音频源。 另一种方法是:使用ScriptProcessorNode(已弃用,但可用)或AudioWorkletNode(推荐)来动态地将接收到的PCM数据播放出来,同时录制。但这里我们只是需要保存为WAV文件。 因此,我们可以自己实现一个简单的WAV编码器,因为WAV格式相对简单。 这里提供一个简单的WAV编码函数(针对单声道16位PCM): ```javascript function encodeWAV(samples, sampleRate) { const buffer = new ArrayBuffer(44 + samples.length * 2); const view = new DataView(buffer); /* RIFF identifier */ writeString(view, 0, 'RIFF'); /* file length */ view.setUint32(4, 36 + samples.length * 2, true); /* RIFF type */ writeString(view, 8, 'WAVE'); /* format chunk identifier */ writeString(view, 12, 'fmt '); /* format chunk length */ view.setUint32(16, 16, true); /* sample format (raw) */ view.setUint16(20, 1, true); /* channel count */ view.setUint16(22, 1, true); /* sample rate */ view.setUint32(24, sampleRate, true); /* byte rate (sample rate * block align) */ view.setUint32(28, sampleRate * 2, true); // 2 bytes per sample (16bit) * 1 channel /* block align (channel count * bytes per sample) */ view.setUint16(32, 2, true); /* bits per sample */ view.setUint16(34, 16, true); /* data chunk identifier */ writeString(view, 36, 'data'); /* data chunk length */ view.setUint32(40, samples.length * 2, true); // Convert to 16-bit PCM floatTo16BitPCM(view, 44, samples); return new Blob([view], { type: 'audio/wav' }); } function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } function floatTo16BitPCM(view, offset, input) { for (let i = 0; i < input.length; i++, offset += 2) { const s = Math.max(-1, Math.min(1, input[i])); view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } ``` 然后,我们可以将之前合并的Float32Array数据直接传入这个函数(注意:这个函数需要的是Float32Array,但我们之前已经转换了)。 但是,我们也可以直接从Int16Array生成WAV,因为服务器发送的就是16位PCM,我们不需要转换成浮点数再转回去。 因此,我们可以修改WAV编码函数,直接使用Int16Array: ```javascript function encodeWAVFromInt16(samples, sampleRate) { const buffer = new ArrayBuffer(44 + samples.length * 2); const view = new DataView(buffer); /* RIFF identifier */ writeString(view, 0, 'RIFF'); /* file length */ view.setUint32(4, 36 + samples.length * 2, true); /* RIFF type */ writeString(view, 8, 'WAVE'); /* format chunk identifier */ writeString(view, 12, 'fmt '); /* format chunk length */ view.setUint32(16, 16, true); /* sample format (raw) */ view.setUint16(20, 1, true); /* channel count */ view.setUint16(22, 1, true); /* sample rate */ view.setUint32(24, sampleRate, true); /* byte rate (sample rate * block align) */ view.setUint32(28, sampleRate * 2, true); // 2 bytes per sample * 1 channel /* block align (channel count * bytes per sample) */ view.setUint16(32, 2, true); /* bits per sample */ view.setUint16(34, 16, true); /* data chunk identifier */ writeString(view, 36, 'data'); /* data chunk length */ view.setUint32(40, samples.length * 2, true); // Write the samples let offset = 44; for (let i = 0; i < samples.length; i++) { view.setInt16(offset, samples[i], true); offset += 2; } return new Blob([view], { type: 'audio/wav' }); } ``` 这样,我们在停止录制时,可以直接使用合并后的Int16Array(即mergedData)来生成WAV文件。 所以,完整的流程: 1. 建立WebSocket连接,接收ArrayBuffer,转换为Int16Array并保存到数组。 2. 停止时,合并所有Int16Array为一个大的Int16Array。 3. 使用`encodeWAVFromInt16`函数生成WAV Blob并保存。 代码示例: ```javascript // 参数 const sampleRate = 16000; // 采样率,根据实际情况设置 const ws = new WebSocket('ws://example.com/audio-stream'); let pcmChunks = []; ws.binaryType = 'arraybuffer'; ws.onmessage = function(event) { // 将ArrayBuffer转换为Int16Array const data = new Int16Array(event.data); pcmChunks.push(data); }; function stopRecording() { ws.close(); // 合并所有chunk const totalLength = pcmChunks.reduce((acc, chunk) => acc + chunk.length, 0); const mergedSamples = new Int16Array(totalLength); let offset = 0; for (const chunk of pcmChunks) { mergedSamples.set(chunk, offset); offset += chunk.length; } // 生成WAV const wavBlob = encodeWAVFromInt16(mergedSamples, sampleRate); // 创建下载链接 const url = URL.createObjectURL(wavBlob); const a = document.createElement('a'); a.href = url; a.download = 'recording.wav'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); } // 实现encodeWAVFromInt16 function encodeWAVFromInt16(samples, sampleRate) { // ... 如上所述 } ``` 注意:这个方案假设我们是一次性接收到所有数据然后保存。如果是流式传输且数据量很大,可能需要分片保存或使用MediaRecorder API(但MediaRecorder不支持PCM,所以这里不使用)。 另外,如果服务器发送的是Float32Array或其他格式,需要相应调整。 最后,关于Recorder.js的使用,如果我们不需要保存为WAV,而是需要播放,我们可以将接收到的PCM数据通过Web Audio API播放出来,或者使用Recorder.js的播放功能(但Recorder.js主要针对麦克风输入)。 因此,根据用户需求,我们提供了保存为WAV文件的方法。 相关问题: 1. 如何实时播放接收到的PCM音频流? 2. 如何将PCM数据转换为其他音频格式(如MP3)? 3. 在WebSocket传输中,如何处理音频流的延迟和丢包?</think>### 使用WebSocket接收PCM数据并通过Recorder.js处理的实现方法 #### 1. **建立WebSocket连接接收PCM数据** ```javascript // 建立WebSocket连接 const ws = new WebSocket('wss://your-audio-server.com/stream'); // 存储接收到的PCM数据块 const pcmChunks = []; ws.binaryType = 'arraybuffer'; // 指定接收二进制数据 ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // 将接收到的PCM数据存入数组 pcmChunks.push(new Int16Array(event.data)); } }; ``` #### 2. **使用Recorder.js处理PCM数据** ```javascript // 初始化Recorder.js navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); const recorder = new Recorder(source, { type: 'audio/wav', // 输出格式 sampleRate: 16000, // 需与服务器采样率一致 bufferSize: 4096 }); // 开始录制 recorder.start(); // 停止录制并处理数据 function stopRecording() { recorder.stop().then(() => { // 合并所有PCM数据块 const totalLength = pcmChunks.reduce((acc, chunk) => acc + chunk.length, 0); const mergedPCM = new Int16Array(totalLength); let offset = 0; pcmChunks.forEach(chunk => { mergedPCM.set(chunk, offset); offset += chunk.length; }); // 将PCM转为Recorder.js支持的格式 recorder.importPCM(mergedPCM, recorder.sampleRate, 1, 16); // 导出WAV文件 recorder.exportWAV(blob => { const url = URL.createObjectURL(blob); // 创建下载链接或播放音频 const audio = new Audio(url); audio.play(); }); }); } }); ``` #### 3. **关键参数说明** - **采样率匹配**:确保`sampleRate`与服务器发送PCM采样率一致(常见值:$16000\ \text{Hz}$ 或 $44100\ \text{Hz}$) - **数据类型**:WebSocket接收的PCM通常是`Int16Array`($16$位有符号整数) - **通道数**:单声道($1$)或立体声($2$),需在`importPCM`中指定 #### 4. **实时处理优化** ```javascript // 实时处理接收到的PCM数据 ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { const pcmData = new Int16Array(event.data); recorder.importPCM(pcmData, recorder.sampleRate, 1, 16); } }; // 持续导出音频片段(可选) setInterval(() => { recorder.exportWAV(blob => { // 上传或实时播放片段 }); }, 5000); // 每5秒导出一次 ``` #### 5. **注意事项** 1. **音频上下文恢复**:浏览器自动暂停时需手动恢复 ```javascript document.addEventListener('click', () => { if (audioContext.state === 'suspended') { audioContext.resume(); } }); ``` 2. **内存管理**:长时间录制需分块处理数据,避免内存溢出 3. **错误处理**:添加WebSocket音频错误监听 ```javascript ws.onerror = (error) => console.error("WebSocket错误:", error); recorder.onerror = (error) => console.error("Recorder错误:", error); ``` ### 完整示例代码 ```html <!DOCTYPE html> <script src="https://cdn.jsdelivr.net/npm/recorder-core@latest/recorder-core.min.js"></script> <script> const ws = new WebSocket('wss://your-server.com/audio'); const pcmChunks = []; ws.binaryType = 'arraybuffer'; ws.onmessage = handlePCMData; async function initRecorder() { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const audioContext = new AudioContext(); const recorder = new Recorder(audioContext.createMediaStreamSource(stream), { type: 'audio/wav', sampleRate: 16000 }); recorder.start(); return recorder; } function handlePCMData(event) { if (event.data instanceof ArrayBuffer) { pcmChunks.push(new Int16Array(event.data)); } } async function saveRecording() { const recorder = await initRecorder(); const mergedPCM = mergeChunks(pcmChunks); recorder.importPCM(mergedPCM, 16000, 1, 16); recorder.exportWAV(blob => { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'recording.wav'; link.click(); }); } function mergeChunks(chunks) { const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Int16Array(totalLength); let offset = 0; chunks.forEach(chunk => { result.set(chunk, offset); offset += chunk.length; }); return result; } </script> <button onclick="saveRecording()">保存录音</button> ``` ### 相关问题 1. **如何优化WebSocket传输的PCM音频延迟?** 建议使用二进制分帧传输,采用$20-40\ \text{ms}$的小数据包,并启用WebSocket压缩[^1]。 2. **Recorder.js支持哪些音频格式导出?** 支持WAV、MP3、OGG等格式,通过插件扩展(如`recorder-wav.js`、`recorder-mp3.js`)[^2]。 3. **如何处理多通道(立体声)PCM数据?** 修改`importPCM`的通道参数为$2$,并确保PCM数据是交错的左右声道样本。 4. **浏览器兼容性有哪些限制?** Web Audio API在iOS Safari有严格限制,需用户手势触发;旧版Edge需polyfill支持[^1]。 [^1]: WebSocket音频传输最佳实践,https://xiangyuecn.gitee.io/recorder/assets/%E5%B7%A5%E5%85%B7-%E4%BB%A3%E7%A0%81%E8%BF%90%E8%A1%8C%E5%92%8C%E9%9D%99%E6%80%81%E5%88%86%E5%8F%91Runtime.html?jsname=teach.realtime.encode_transfer_frame_pcm [^2]: Recorder.js官方文档,https://github.com/xiangyuecn/Recorder
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值