You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
customAgent/src/utils/record.js

323 lines
14 KiB

/*
* @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 };