WEB前端语音对讲实现方案以及示例

WEB前端语音对讲实现方案以及示例

  • 需求
  • 实现
  • 难点
    • 1.实时输出音频数据
    • 2.实时数据解析,播放

需求

司机需要通过车载终端设备与WEB平台进行语音对讲,目前已实现WebSocket服务端,需要实现客户端

说到websocket想比大家不会陌生,如果陌生的话也没关系,一句话概括
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信

实现

我们需要在web端实现双向对讲,首先创建html文件,这个只是用来做个demo,比较简单:



    
	test


难点

第一个是实时输出音频数据,第二是实时解析输入的音频流并进行播放

1.实时输出音频数据

这里要介绍一下Navigator.getUserMedia()方法,点我查看API文档
该方法提醒用户需要使用的设备类型,音频(true或者false)和视频(true或者false),比如麦克风。
如下例,我们使用音频输入,获取mediaStream,构建相关业务对象:

function init(rec){
	record = red;
}
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));
    }
  };

至此我们可以拿到麦克风输入的音频数据,但是要实现语音对讲,还需要进行一些处理,将数据实时输出到服务器,我这边使用了定时器,每隔500毫秒发送一次语音数据给服务器,代码如下:

begin.onclick = function() {
	var ws = new WebSocket("ws://192.168.168.4:6604");
 
    ws.onopen = function() {
        console.log('握手成功');
        //业务命令构建
		var data = {
		  "cmd": "jtv",//发送命令
		  "id": "018665897939",//发送设备id
		  "type": 1,//对讲类型
		  "channel": 0//语音通道
		}
        ws.send(JSON.stringify(data));
    };
    timeInte=setInterval(function(){
		if(gRecorder&&ws.readyState==1){//ws进入连接状态,则每隔500毫秒发送一包数据
			record.start();
			ws.send(record.getBlob());
 			record.clear();	//每次发送完成则清理掉旧数据		
		}
	},500);
}

注意:该功能前置条件是已经实现WebSocket服务器端。

2.实时数据解析,播放

接上以上示例,我们已经实现了WebSocket,服务器端会实时返回二进制数据到前端,我们需要对数据进行解析和处理,转码成WAV格式进行播放,废话不多说,先上代码:

 	ws.onmessage = function(e) {
        receive(e.data);
    };

由于websocket属于长连接,所以我们这边再也不需要进行传统的轮询等复杂操作,只需要实现onmessage即可实时获取服务器发送过来的数据,服务器首先会返回一串字符串,对我们发送的命令进行回馈,OK之后才会开始发送音频数据给我们,ws支持字符串和二进制数据发送,音频数据自然属于后者,所以我们需要对数据进行解析,这里我们用到了window.AudioContext ,介绍一下它吧,AudioContext接口是一个音频上下文对象,表示由音频模块连接而成的音频处理对象,AudioContext可以控制它所包含的节点的创建,以及音频处理、解码操作的执行。
接下来我们进行二进制数据的解析和播放:

    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){
				var audioContext = new ( window.AudioContext || window.webkitAudioContext )();
				var fileResult =addWavHeader(buf, '8000', '16', '1');//解析数据转码wav
				audioContext.decodeAudioData(fileResult, function(buffer) {
				   _visualize(audioContext,buffer);//播放
				});
			});
		}
	}

DataView 视图是一个可以从 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
详细介绍点我

//处理音频流,转码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;
}

到此,我们就实现了双向对讲全部功能。

本篇文章也参考以下资料:
https://blog.csdn.net/tiantangyouzui/article/details/71083330

你可能感兴趣的:(JS,前端,HTML5)