/* * @Author: DUDU * @Date: 2022-12-13 18:07:36 * @LastEditors: DUDU * @LastEditTime: 2023-04-12 18:24:22 * @Description: */ import { Toast } from "vant"; import Recorder from "recorder-core"; import "recorder-core/src/engine/wav"; import "recorder-core/src/engine/pcm"; // Recorder.ConnectEnableWorklet = true var testSampleRate = 16000; var testBitRate = 16; var SendFrameSize = 3200; // 每次发送指定二进制数据长度的数据帧,单位字节 var realTimeSendTryNumber; var transferUploadNumberMax; var realTimeSendTryChunk; var realTimeSendTryChunks; var currentReqNo = ''; var recordedChunks = []; var vacantBase64 = "6P/p/+//7P/m/+3/5v/E/9//4v/Z/+f/AgDt/wAA/P/v/+z/5P/n/+n/4//m/+j/7f/o/9f/8P/t//X/8P/s/+//4v/p/+b/6f/k/+b/5v/n/+n/0//m/+r/6v/s//b/8f/i/+X/6P/m/+//7P/f/9z/5f/Q/+3////Z//n/8//x/77/AwALAAAA8//p/9z/5f/t/9b/3f/l/6r/7v8UAPv/7f/Z/+7/8P/1/+v/7P/j/+v/0P/y//T/7P/a/wEADAACAPb/7v/i/97/4v/j/+X/4f/T//3/3v/k/+f/4P/W//b/3//x/93/4P+8/6r/1//H/wQACQAAAM//5v/f/9b/7P8iAAIA5//p//H/3f/7/+P/3f/6//L/5v/q/+j/+P/W/97/of+4/9n/IQAVAP7/+P/K//b/8//2/+7/y//A/9v/+v8QAPX/9f/N/8f/p/+7/9//CwD4/xAAFAC4/xsACQB8/yEAEQAAAPL/4P/m/+n/sf8UAOv/vv/v/wEA9f/Y////IQAGAAAA9v/z/+z/8f+3/wAA7v/w/9v/AgC2/4b/v//W/9z/7f8dAPf/AADu//X/7/8PAPP//P/u/7//8f/h/+f/1v/s/+D/p//R/w8AAAD//+H/1P8YAAAA7//x/+z/7P/n/+f/xv/i/wEA8f+4//v/8v/y//f/8//r/+7/8f/w//H/7f/x//L/6f/l/7r/AAADAMb/CgD7//X/9//v/+f/6v/o/w4A+//u/+P/uf/9//v/+//3//L/8P/w//X/+P/1//D/3v/7//H/9f98//j/AwDp//3/9P/z//L/9v/2//b/+f/5//b/9f/y/77/AwD8/8D/BAD+//D/8v/w//D/8v/n//T/y//O/83/xP8xABoABwD///b/8f/z//X/9f/x//T/9f/z//P/8f+9//7/AQDt/wIA/v/5//b/8//u//T/5v8WAAMA+//0/8L//v///8v/BAD8//n/8//1//j/9f/4//H/+f/1//b/z//4/wkAwP8aAAkAAAD6//v//f/6//v/9f/7//n//P/W/7X/o/+u/83/bwBTACgADwABAP///f/5////+f/9/9X/9P8AANj/BQD///r/+f/y//H/8P/v//L/8f/z//f/0v/t/wQA/P/0//z/9f/4//3//f/8//v//v8CAAAAAwDm//r/EwAIAP7//v/2//L/+v/1//v/+/////7/AAD+/+n/+/8LAPz/CgACAAIA/v/8/wAA/P/8/////f/9//z/2P+F/6H/BgCh/zkARAASANX/5//r/6f/6v/k/xYAAQACAAoAAQDL/wAA8f/L///////3/+T/5//y//P/OQAXAAAA6/9GABcASwBCACAAEQAIAOz/+v/2/wgA9f/v//v/FgAJABgAEAAGAPr/AwD6//f/+//9//T/9v/+/////f8DAPT/FAAZAA0ACwDp/wgACgAAAP7/+P/5/wEAAAAAAAQA0v/d/+b/CAAKACsAGAAGAAQABAASAAYABQALAAMA///u/xMA7v8NAAwADgAQAAYACAALAAQACgAGAA4ACwAUAAIAFAAUABkADwAXAAAADAD//wQAxf/D//D/4P8TANz/MAD0/woAKwAeAA0ADQAEAOj/IAAQAB0A3f/F/63/3f9WAE8ALAAKAAYA8/8FAA4ACAAJAAgA+P8MAAkADAAPAPD/FwATAAgACwAIAAUADQAbAPX/tv+2/8n/5P8pACAAJQAcAFYAOAAxAB0ABgAIANj/+P/x/x8AGgASAAEAJADX/0MALgAXABcACAANAAoAFAALAAkA7f/c/ysA3P/S/wEAKgANABgAEQDJ//D/IAAHAP3/CwAMAAQABADY/wUA+/8vACEAEAA+ACEAFQAPAA8ACgAMAAwACgAKAAoACACh//v/CQBAACwAGQARAAYABgD8/x0AEwANAAwABAAHANH/IAAUAAwABgAIAAQABQAGAAMA9/+5/7X/1f8QAPP/AQADALn/GAACACMAKwDZ/ycAGgAjAFQAIAAuACMAEwDs/wIA7P8OAO3/8f8HAM//KwDb/zIAHgAaABMADwDi//r/HgAXAC4AIgAQAAUA+v/x/wcABwAHADAAJAAOAAYAzv8hABgAEgAPAAoA+f8CAAoABwADAAQA7//b/+3/+f/a//z/8P8TAB8A0f8pABcA/v8OAAgAKwAcAAoABwABANP/FwAZAAgACwAHAAQAAAAEAAYABwAGAAgADwAJAAYA2/8QABIACgDg/xEAz//W/yMAKwAgABQACwAKANT/EwDm/xQAEgAGAAoAAwABAPn/CQAEAAUAAwAEAAQAEgACAL3/1/8qACgAHAAXAAwAz//E//z/9v8fABEABgDI/yIA4f8dABwAJADs/8L/r//u/xYA9f/l/ycA1v8gACEAAgAuAAsA8f8YAOv/IQDx/wQA6v8fAAkAFgAEAAEABAD4/w8AEwAFAPf/9/8KAPf/CQAfAA4A+f8PAAEA+////xcA1v/z//3/9v8DAPP/+f/v/+z/9P8MAAgABgAOAB0A///6/w4ACwDv/+X/3v/m//f/EQAWACAA+P/s//f/AAD5/woACwAAAO//9f///x4ALAD5/wAA2v8CAPr/DwD0/xAA+v8JAAMA8P/y//P/BAAIAAAA///o/xQA+v8eABcAHAD7/+f/7v8EAB8A//8FAP//BwAHAPL/BAALABAA7f/j//f/CQAHAOz/AwAGAAkACgD4/+X/9//3//7/AwDo/xQA9v/x//z/8f/8//r/AADv/wAA4//z/w8AEwD0//n/9v/4/+7/AQD8//n/8P/n//n/8v8DAOL/+f8AAPj/+v/o/+f/HwAOAAoA8v8HAOn/5v/0/wAA9f/q//v/DQAGABAAAgD0//D/HgAHAAEA5f8BABAA5//v//z/BQDo//n/BAAMAPH/GQAIAPv///8NAAYA9P8KAAAA6f/u//n/6v8BAP3/7P////H/7P8GAP//AAD3//L/CgD1/wAA//8DAAMABgD9/w0A7P/a/+X/CQARAAEA6f8CAA4A+//5/wEAEQAKAPn/FAASAPX/2P8PANj/8P8oABUAwv/W/+r/6f8cAPT/BwBCABMAHQAWAP7//v8bABIAMQDg//j/8v/U/0sAKgDs/w0At//u/+n/AwAAAAIACwALABgAKgAIABoA///u/87/2P/6/x8AIgAUAAkABgAAAAAA/v8CAPz//P+v/7D/2v/o/9z/6/8JAPj//P/1/wEADQAJAAEAAAAEAAEACwD8/wgA/P8MAPr/8P/z/wAA+/8NAPD/9f8IAPb//f/3/+//6f/0/+r/+P8BAAAA+//4//v/9P8EAP//AAAMAPb/7v8HABcABAAkAO7/8f8RAEsANwA5ABkADQAMAP//9//G/8v/4v/m/+3/+f/g//3/DwAVAAsA6f/7/9///P/r/wkA/v/u/+P/GwAAAPn/EwD4//n/3f/Z/wMA+v/6//v/0f8GAB4A8v8MABYAQAAoAAwAAwD3//b/EAAKAPT/9f/M/wIA/f8DAP7////3//3/+v///wsA/f/X/yYAzf8eANj/GQAIAAIA//8AAO3//f/7//r//v/8//n/0//n/+T/yf/4/zMAIAAHAAYAAAAAAAUAAAAAAP///v/+/wIAAADK/wgACAADAAAA+v/3//n/+f/4//b/9//7//f/9v/w/8b/AwACAP3//f/5/8X/2P/b/7j/sf/Y/2sAXQA0ABsA2P8KAAwAAwABAP3/+//w//P/8P/x//j/AAAAAAIAAQDa/wwAEAADAAAA/v/x//f/8//5/+b/DQAAAPn//P/7/87/AAAGAAMABAD6//7//v/9//v/+v/k/9z/0f8XABwA4f8CAAQA/f/7//7/9f/6//n/+v/6//v/9f/3//n/+f/e//P/AgAAAPj/+f/m//b//f/2//r/+//2//b/9v/9/+L/9P8FAPr/+//4//j/7f/7//L/8f/1//f/+f/1//n/5P/w/73/GQAKAAAA+f/1//L/9v/1//P/9P/w//b/v//Q/w8AFQAIAAAA9f/n//D/+P/4//v/9f/2//T/9P/1/+v/7f8HAAAAAAD//8X/6P/x/wUA8P+r/6H/xv8BANf/5//D//b/+/8MAOj/8f9LAAsAEAABAAoA9P/W//P/5v///+L/FwDs/9r/7v/O/xMADwDp//H/CQAKAOD/DAANANj/zf8TAAEA4P98/9z/6f/i/zAAwv/g/wMA1P/t//T/8f8IAB0AEwA="; // 重置环境,每次开始录音时必须先调用此方法,清理环境 function RealTimeSendTryReset() { realTimeSendTryChunks = null; recordedChunks = []; } // 当 reqNo 更新时,这个函数将被调用 function onReqNoUpdate(newReqNo) { // console.log(newReqNo); currentReqNo = newReqNo; } // 实时处理核心函数 function RealTimeSendTry(buffers, bufferSampleRate, isClose) { if (realTimeSendTryChunks == null) { realTimeSendTryNumber = 0; transferUploadNumberMax = 0; realTimeSendTryChunk = null; realTimeSendTryChunks = []; } // 配置有效性检查 if (testBitRate == 16 && SendFrameSize % 2 == 1) { console.log("16位SendFrameSize 必须为2的整数倍", 1); return; } var pcm = [], pcmSampleRate = 0; if (buffers.length > 0) { // 借用SampleData函数进行数据的连续处理,采样率转换是顺带的,得到新的pcm数据 var chunk = Recorder.SampleData( buffers, bufferSampleRate, testSampleRate, realTimeSendTryChunk ); // 清理已处理完的缓冲数据,释放内存以支持长时间录音,最后完成录音时不能调用stop,因为数据已经被清掉了 for ( var i = realTimeSendTryChunk ? realTimeSendTryChunk.index : 0; i < chunk.index; i++ ) { buffers[i] = null; } realTimeSendTryChunk = chunk; // 此时的chunk.data就是原始的音频16位pcm数据(小端LE),直接保存即为16位pcm文件、加个wav头即为wav文件、丢给mp3编码器转一下码即为mp3文件 pcm = chunk.data; pcmSampleRate = chunk.sampleRate; if (pcmSampleRate != testSampleRate) // 除非是onProcess给的bufferSampleRate低于testSampleRate throw new Error( "不应该出现pcm采样率" + pcmSampleRate + "和需要的采样率" + testSampleRate + "不一致" ); } // 将pcm数据丢进缓冲,凑够一帧发送,缓冲内的数据可能有多帧,循环切分发送 if (pcm.length > 0) { realTimeSendTryChunks.push({ pcm: pcm, pcmSampleRate: pcmSampleRate, }); } // 储存pcm recordedChunks.push(new Int16Array(pcm)); //从缓冲中切出一帧数据 var chunkSize = SendFrameSize / (testBitRate / 8); //8位时需要的采样数和帧大小一致,16位时采样数为帧大小的一半 var pcm = new Int16Array(chunkSize), pcmSampleRate = 0; var pcmOK = false, pcmLen = 0; for1: for (var i1 = 0; i1 < realTimeSendTryChunks.length; i1++) { var chunk = realTimeSendTryChunks[i1]; pcmSampleRate = chunk.pcmSampleRate; for (var i2 = chunk.offset || 0; i2 < chunk.pcm.length; i2++) { pcm[pcmLen] = chunk.pcm[i2]; pcmLen++; //满一帧了,清除已消费掉的缓冲 if (pcmLen == chunkSize) { pcmOK = true; chunk.offset = i2 + 1; for (var i3 = 0; i3 < i1; i3++) { realTimeSendTryChunks.splice(0, 1); } break for1; } } } //缓冲的数据不够一帧时,不发送 或者 是结束了 if (!pcmOK) { if (isClose) { var number = ++realTimeSendTryNumber; TransferUpload.call(this, number, null, 0, null, isClose); } return; } // 16位pcm格式可以不经过mock转码,直接发送new Blob([pcm.buffer],{type:"audio/pcm"}) 但8位的就必须转码,通用起见,均转码处理,pcm转码速度极快 var number = ++realTimeSendTryNumber; var encStartTime = Date.now(); var recMock = Recorder.call(this, { type: "pcm", sampleRate: testSampleRate, //需要转换成的采样率 bitRate: testBitRate, //需要转换成的比特率 }); recMock.mock(pcm, pcmSampleRate); recMock.stop( (blob, duration) => { blob.encTime = Date.now() - encStartTime; //转码好就推入传输 TransferUpload.call(this, number, blob, duration, recMock, false); //循环调用,继续切分缓冲中的数据帧,直到不够一帧 RealTimeSendTry.call(this, [], 0, isClose); }, function (msg) { //转码错误?没想到什么时候会产生错误! console.log("不应该出现的错误:" + msg, 1); } ); } // 数据传输函数 function TransferUpload(number, blobOrNull, duration, blobRec, isClose) { transferUploadNumberMax = Math.max(transferUploadNumberMax, number); if (blobOrNull) { var blob = blobOrNull; var encTime = blob.encTime; var reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = (e) => readerOnLoadend.call(this, e); function readerOnLoadend(e) { // 得到 base64 字符串,但这包含了数据类型前缀(如 'data:image/png;base64,') let base64data = reader.result; // 去除前缀,获取纯 Base64 字符串 let base64String = base64data.split(',')[1]; const param = { token: this.asrData.token, appId: this.asrData.appId, bizNo: "", reqNo: currentReqNo, rate: "16k", language: "1", resId: "", audioStatus: "2", eos: "3500", bos: "5000", voiceData: this.recorderEnded ? vacantBase64 : base64String, isBackMid: "", } if (this.trainWebsocket.readyState == 1) { this.asrLoading.close(); this.asrLoadingState = false; this.trainWebsocket.send(JSON.stringify(param)); } } } if (isClose) { console.log( "No." + (number < 100 ? ("000" + number).substr(-3) : number) + ":已停止传输" ); } } // pcm文件合并核心函数 Recorder.PCMMerge = function (fileBytesList, bitRate, sampleRate, True, False) { //计算所有文件总长度 var size = 0; for (var i = 0; i < fileBytesList.length; i++) { size += fileBytesList[i].byteLength; } //全部直接拼接到一起 var fileBytes = new Uint8Array(size); var pos = 0; for (var i = 0; i < fileBytesList.length; i++) { var bytes = fileBytesList[i]; fileBytes.set(bytes, pos); pos += bytes.byteLength; } //计算合并后的总时长 var duration = Math.round(((size * 8) / bitRate / sampleRate) * 1000); True(fileBytes, duration, { bitRate: bitRate, sampleRate: sampleRate, }); }; let rec; function recorderInit() { currentReqNo = ''; rec = Recorder.call(this, { type: "unknown", onProcess: (buffers, powerLevel, bufferDuration, bufferSampleRate) => { // console.log(`onProcess------------------buffers`, buffers); // 推入实时处理,因为是unknown格式,buffers和rec.buffers是完全相同的,只需清理buffers就能释放内存 RealTimeSendTry.call(this, buffers, bufferSampleRate, false); }, }); } function recorderStart() { // if (rec) { // rec.close() // Recorder.Destroy() // } // 打开麦克风授权获得相关资源 rec.open( () => { this.isRecording = true; // 重置环境,开始录音时必须调用一次 RealTimeSendTryReset(); // 开始录音 rec.start(); }, function (msg, isUserNotAllow) { if (this.recordRight) { Toast("无法录音,请检查录音权限是否开启"); } this.trainWebsocket?.close(); this.asrLoading.close(); this.asrLoadingState = false; console.log( (isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg, 1 ); return false }.bind(this) ); } function recorderStop() { rec.pause(); this.isRecording = false; // this.trainWebsocket.send("END_SOUND_RECORDING"); RealTimeSendTry.call(this, [], 0, true); } var _this = null; function recorderClose() { _this = this; if (rec) { rec.close(); Recorder.Destroy(); } } function mergeWav() { // 合并录音片段 // console.log('=======mergeWav=========='); var _this = this; Recorder.PCMMerge(recordedChunks, testBitRate, testSampleRate, function (mergedPCM, duration, formatInfo) { // 处理合并后的音频 let totalLength = recordedChunks.reduce((sum, chunk) => sum + chunk.length, 0); mergedPCM = new Int16Array(totalLength); let offset = 0; recordedChunks.forEach(chunk => { mergedPCM.set(chunk, offset); offset += chunk.length; }); var newrec = new Recorder({ type: "wav", sampleRate: 16000, bitRate: 16 }); newrec.mock(mergedPCM, testSampleRate); newrec.stop(function (blob) { var reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = function (e) { _this.recorderBlob = reader.result; console.log("合并后的录音文件base64:" + reader.result); }; }) }, function (error) { console.error("音频合并失败:", error); }); this.isRecording = false; } export { recorderInit, recorderStart, recorderStop, recorderClose, onReqNoUpdate, mergeWav };