基于H5的实时语音聊天
#基于H5的實時語音聊天
業務需求:網頁和移動端的通訊,移動端播放g711alaw,難點如下:
- 網頁如何調用系統api錄音
- 錄音后的數據是什么格式?如何轉碼?
- 如何實時通訊
<input type="text" id="a"/> <button id="b">buttonB</button> <button id="c">停止</button>******************************************var a = document.getElementById('a'); var b = document.getElementById('b'); var c = document.getElementById('c');navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;var gRecorder = null; var audio = document.querySelector('audio'); var door = false; var ws = null;b.onclick = function() {if(a.value === '') {alert('請輸入用戶名');return false;}if(!navigator.getUserMedia) {alert('抱歉您的設備無法語音聊天');return false;}SRecorder.get(function (rec) {gRecorder = rec;});ws = new WebSocket("wss://x.x.x.x:8888");ws.onopen = function() {console.log('握手成功');ws.send('user:' + a.value);};ws.onmessage = function(e) {receive(e.data);};document.onkeydown = function(e) {if(e.keyCode === 65) {if(!door) {gRecorder.start();door = true;}}};document.onkeyup = function(e) {if(e.keyCode === 65) {if(door) {ws.send(gRecorder.getBlob());gRecorder.clear();gRecorder.stop();door = false;}}} }c.onclick = function() {if(ws) {ws.close();} }var SRecorder = function(stream) {config = {};config.sampleBits = config.smapleBits || 8; //輸出采樣位數config.sampleRate = config.sampleRate || (44100 / 6); //輸出采樣頻率var context = new AudioContext();var audioInput = context.createMediaStreamSource(stream);var recorder = context.createScriptProcessor(4096, 1, 1); //錄音緩沖區大小,輸入通道數,輸出通道數var audioData = {size: 0 //錄音文件長度, buffer: [] //錄音緩存, inputSampleRate: context.sampleRate //輸入采樣率, inputSampleBits: 16 //輸入采樣數位 8, 16, outputSampleRate: config.sampleRate //輸出采樣率, oututSampleBits: config.sampleBits //輸出采樣數位 8, 16, 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;}, encodeWAV: 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(44 + dataLength);var data = new DataView(buffer);var channelCount = 1;//單聲道var offset = 0;var writeString = function (str) {for (var i = 0; i < str.length; i++) {data.setUint8(offset + i, str.charCodeAt(i));}};// 資源交換文件標識符writeString('RIFF'); offset += 4;// 下個地址開始到文件尾總字節數,即文件大小-8data.setUint32(offset, 36 + dataLength, true); offset += 4;// WAV文件標志writeString('WAVE'); offset += 4;// 波形格式標志writeString('fmt '); offset += 4;// 過濾字節,一般為 0x10 = 16data.setUint32(offset, 16, true); offset += 4;// 格式類別 (PCM形式采樣數據)data.setUint16(offset, 1, true); offset += 2;// 通道數data.setUint16(offset, channelCount, true); offset += 2;// 采樣率,每秒樣本數,表示每個通道的播放速度data.setUint32(offset, sampleRate, true); offset += 4;// 波形數據傳輸率 (每秒平均字節數) 單聲道×每秒數據位數×每樣本數據位/8data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;// 快數據調整數 采樣一次占用字節數 單聲道×每樣本的數據位數/8data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;// 每樣本數據位數data.setUint16(offset, sampleBits, true); offset += 2;// 數據標識符writeString('data'); offset += 4;// 采樣數據總數,即數據總大小-44data.setUint32(offset, dataLength, true); offset += 4;// 寫入采樣數據if (sampleBits === 8) {for (var i = 0; i < bytes.length; i++, offset++) {var s = Math.max(-1, Math.min(1, bytes[i]));var val = s < 0 ? s * 0x8000 : s * 0x7FFF;val = parseInt(255 / (65535 / (val + 32768)));data.setInt8(offset, val, true);}} else {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], { type: 'audio/wav' });}};this.start = function () {audioInput.connect(recorder);recorder.connect(context.destination);}this.stop = function () {recorder.disconnect();}this.getBlob = function () {return audioData.encodeWAV();}this.clear = function() {audioData.clear();}recorder.onaudioprocess = function (e) {audioData.input(e.inputBuffer.getChannelData(0));} };SRecorder.get = function (callback) {if (callback) {if (navigator.getUserMedia) {navigator.getUserMedia({ audio: true },function (stream) {var rec = new SRecorder(stream);callback(rec);})}} }function receive(e) {audio.src = window.URL.createObjectURL(e); }
上面是一段調用H5 Api發送語音的代碼片段,按住A鍵錄音,松開A鍵發送,實現功能類似于微信,下面先對上面代碼進行分析和理解,便于實現我們的實時語音需求。
首先創建AudioContext對象,有這個對象才有后面的H5調用底層硬件功能,
var recorder = context.createScriptProcessor(4096, 1, 1);
這句話創建的4096是錄音一次錄多少個字節,錄到4096字節后會走后面的回調:
按住A鍵后調用gRecorder.start();
this.start = function () {audioInput.connect(recorder);recorder.connect(context.destination); //把麥克風的輸入和音頻采集相連起來 context.destination返回代表在環境中的音頻的最終目的地。}然后走上面的回調不停的采集麥克風的音頻數據input進去,input在這里:
input: function (data) {this.buffer.push(new Float32Array(data)); //buffer存儲float32,size為字節大小,buffer大小為float大小this.size += data.length;}這里將字節流數據,存成Float32Array存在buffer中,也就是說成員變量里面的buffer實際上是多個Float32Array組成的
合并和壓縮
因為不同的業務可能要將錄制的PCM轉成需要的音頻格式,所以了解錄制的音頻是什么格式很有必要
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); //把buffer中的各個Float32數組合并成一個offset += this.buffer[i].length;}//壓縮var compression = parseInt(this.inputSampleRate / this.outputSampleRate); //6var length = data.length / compression; //600/6 = 10var result = new Float32Array(length); // 32 位浮點值的類型化數組var index = 0, j = 0;while (index < length) {result[index] = data[j];j += compression; //j+=6index++;}return result;}合并,不多說,就是將buffer中的各個Float32Array合并成一個,壓縮,根據輸入采樣率和輸出采樣率計算出一個比例,像我的項目中輸入采樣頻率為48000 我計劃生成的音頻采樣頻率為8000 那這里的比值為6,計算出比值后,每隔這個比值6采樣一次,平均取點,(經測試這樣的簡單取點不會有雜音)。
這里再啰嗦下音頻的基本知識:
采樣頻率:一秒鐘采樣多少次
采樣位數:一次采樣多大 如果16bit那就是一次采樣2byte
聲道數(通道數):幾個通道采樣
這樣計算下來你一秒鐘采樣的大小為:聲道數 * 采樣頻率 * 采樣位數/8 (單位:byte)
所以上面的4096字節收集一次數據大概用了多少秒就可以計算出來了 :4096/一秒鐘采樣大小 (單位:s)
編碼
前言:
PCM:電腦錄制出的原聲,未經壓縮,聲音還原度高,文件較大
wav:符合RIFF標準的音頻文件,不具體只某一種音頻編碼算法,如wav可以有PCM編碼的wav,g711編碼的wav…等等,只要他符合RIFF標準
wav頭:那么如何叫符合RIFF呢?自己百度。。。wav文件有44個字節的頭,頭里面告訴你音頻是什么格式的,音頻數據有多大。。。其實就是一對符合RIFF的結構體
這里面編碼成wav,為什么要編碼成wav呢,因為他符合RIFF標準,在H5的頁面能播放,轉成其他音頻也方便。因為我們錄制的聲音文件是PCM格式的
我們錄制的PCM數據太大了,也不方便傳輸,那么就根據需要壓縮了一下,再編碼成wav,根據自己需要,如果不需要轉成wav,那么直接將PCM發送出去就好,后臺再對數據進行處理。
###發送方式與后臺接收
發送方式基于websocket,我這里后臺接收用的java,需要注意兩點:1.注意接收緩沖區大小設置足夠2.注意捕獲onclose里面的reson @OnMessage(maxMessageSize=160000) //最大160000字節public void OnMessage2(byte[] message, Session session) {logger.info("byte:" + message.length);System.out.println("轉化為16進制:"+byteArrayToHexStr(message));} @OnClosepublic void onClose(Session session, CloseReason reason) {connList.remove(this);logger.error("onclose被調用"+reason.toString());}如何實時播放語音?
有兩種播放聲音的方法:
1. audio.src = window.URL.createObjectURL(e); 2.audioContext.decodeAudioData(play_queue[index_play], function(buffer) {//解碼成pcm流var audioBufferSouceNode = audioContext.createBufferSource();audioBufferSouceNode.buffer = buffer;audioBufferSouceNode.connect(audioContext.destination);audioBufferSouceNode.start(0);}, function(e) {console.log("failed to decode the file");});這里很明顯第一種對于實時播放并不管用,因為語音包來的太頻繁了,不停得變換src是有聲音卡頓的,第二種親測,有效
這里注意啦:decodeAudioData這個Api很強大,從后臺回傳來的數據,用前臺那個encodeWAV編碼成wav后,傳入decodeAudioData,是可以轉換成聲卡直接播放的數據的,也就是說大家不用絞盡腦汁思考如何把后臺傳來的數據變成Float32Array了,沒那個必要。
再一個這里用了個循環緩沖隊列,將回傳來編碼后的音頻存在循環緩沖隊列中,而播放線程叢這個隊列里取數據:play_queue[index_play],給大家提供個思路,有人有需要再貼代碼吧。。
老東家代碼不能上傳,只有測試demo
前后端demo: https://download.csdn.net/download/qq422243639/10734859
如不清楚轉碼是否正確,在使用轉碼方法轉碼后,寫入文件,用coolpro2 這個工具打開,選擇碼率,比特率等,然后打開,聽聽語音是否清晰,工具下載地址:
https://download.csdn.net/download/qq422243639/10734845
附贈IE基于ActiveX插件的實時語音解決方式:
https://download.csdn.net/download/qq422243639/10734877
總結
以上是生活随笔為你收集整理的基于H5的实时语音聊天的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux锐捷代码_锐捷认证 For L
- 下一篇: 基于卡方的独立性检验