语音识别插件
阅读时间:
18
min 文章字数:
4.4k
字 发布日期:
2025-11-24
最近更新:
2025-12-09
阅读量:
-
当前文档以uni-app作为示例进行接入,以VUE3语法进行编写
- 支持vue2、vue3、nvue
- 支持编译成:H5、Android App、iOS App、微信小程序
- 支持已有的大部分录音格式:mp3、wav、pcm、amr、ogg、g711a、g711u等
- 支持实时处理,包括变速变调、实时上传、ASR语音转文字
- 支持可视化波形显示;可配置回声消除、降噪;注意:不支持通话时录音
- 支持PCM音频流式播放、完整播放,App端用原生插件边录音边播放更流畅
- 支持离线使用,本组件和配套原生插件均不依赖网络
- App端有配套的原生录音插件可供搭配使用,兼容性和体验更好
集成项目中
安装依赖
安装recorder-core
shell
npm install recorder-core --registry=https://registry.npmmirror.com/引入组件
导入Recorder-UniCore组件:直接复制本目录下的uni_modules/Recorder-UniCore组件到你项目中,或者到DCloud 插件市场下载此组件
配置录音权限
在调用RecordApp.RequestPermission的时候,Recorder-UniCore组件会自动处理好App的系统录音权限,只需要在uni-app项目的 manifest.json 中配置好Android和iOS的录音权限声明。
//Android需要勾选的权限,第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音,需要启用后台录音保活服务,否则锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音(renderjs中H5录音、原生插件录音均受影响),请参考下面的`androidNotifyService`
//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置,不然App切到后台后立马会停止录音按需引入js
ts
<script setup lang="ts">
/** 先引入Recorder ( 需先 npm install recorder-core )**/
import Recorder from 'recorder-core';
Recorder.a=1
/** H5、小程序环境中:引入需要的格式编码器、可视化插件,App环境中在renderjs中引入 **/
// #ifdef H5 || MP-WEIXIN
//按需引入需要的录音格式编码器,用不到的不需要引入,减少程序体积;H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入,免得这些文件太大
import 'recorder-core/src/engine/wav.js'
//可选引入可视化插件
import 'recorder-core/src/extensions/waveview.js'
// #endif
/** 引入RecordApp **/
import RecordApp from 'recorder-core/src/app-support/app.js'
//【所有平台必须引入】uni-app支持文件
import '../../uni_modules/Recorder-UniCore/app-uni-support.js'
// #ifdef MP-WEIXIN
//可选引入微信小程序支持文件
import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
// #endif
//引入阿里云语音识别插件
import 'recorder-core/src/extensions/asr.aliyun.short.js'
var vue3This=getCurrentInstance().proxy; //必须定义到最外面,getCurrentInstance得到的就是当前实例this
// ASR 相关响应式数据
const asrTokenApi = ref('')
const asrLang = ref('普通话')
const asrTime = ref('')
const asrTxt = ref('')
const SyncID = ref(0) // 同步操作,如果同时操作多次,之前的操作全部终止
const recpowerx = ref(0)
const recpowert = ref('')
const reclogs = ref<Array<{txt: string, color?: string}>>([])
const asr = ref<any>(null) // 语音识别对象
const waveView = ref<any>(null) // 音频可视化对象
const playerRef = ref<any>(null) // 播放器引用
const emit = defineEmits(['close'])
const userStore = useUserStore()
const showPopup = ref(false)
// 获取状态栏高度(APP端)
const statusBarHeight = ref(0)
</script>额外新增一个renderjs模块
照抄下面这段代码放到vue文件末尾
ts
<!-- #ifdef APP -->
<script module="testMainVue" lang="renderjs"> //这地方就别用组合式api了,可能不能import vue
/**============= App中在renderjs中引入RecordApp,这样App中也能使用H5录音、音频可视化 =============**/
/** 先引入Recorder **/
import Recorder from 'recorder-core';
//按需引入需要的录音格式编码器,用不到的不需要引入,减少程序体积;H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入,免得这些文件太大
import 'recorder-core/src/engine/mp3.js'
import 'recorder-core/src/engine/mp3-engine.js'
import 'recorder-core/src/engine/wav.js'
//可选引入可视化插件
import 'recorder-core/src/extensions/waveview.js'
import 'recorder-core/src/extensions/frequency.histogram.view.js'
import 'recorder-core/src/extensions/lib.fft.js'
/** 引入RecordApp **/
import RecordApp from 'recorder-core/src/app-support/app.js'
//【必须引入】uni-app支持文件
import '../../uni_modules/Recorder-UniCore/app-uni-support.js'
export default {
mounted(){
//App的renderjs必须调用的函数,传入当前模块this
RecordApp.UniRenderjsRegister(this);
//测试用
rjsThis=this;
},
methods: {
//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用,二进制数据需转成base64来传递
testCall(val){
this.$ownerInstance.callMethod("reclog",'逻辑层调用renderjs中的testCall结果:'+val);
}
}
}
//测试用,打印this里面的对象
var rjsThis;
window.traceThis__vue3_capi=function(){
var obj=rjsThis;
var str="renderjs可用:<pre style='white-space:pre-wrap'>";
var trace=(val)=>{
if(/func/i.test(typeof val))val="[Func]";
try{ val=""+val;}catch(e){val="[?"+(typeof val)+"]"}
return '<span style="color:#bbb">='+val.substr(0,50)+'</span>';
}
for(var k in obj){
str+='\n this.'+k+trace(obj[k]);
}
for(var k in obj.$ownerInstance){
str+='\n this.$ownerInstance.'+k+trace(obj.$ownerInstance[k]);
}
for(var k in obj.$ownerInstance.$vm){
str+='\n this.$ownerInstance.$vm.'+k+trace(obj.$ownerInstance.$vm[k]);
}
for(var k in uni){
str+='\n uni.'+k+trace(uni[k]);
}
str+='</pre>';
var el=document.querySelector('.testPerfRJsLogs');
el.innerHTML+=str;
}
</script>
<!-- #endif -->调用录音
ts
<script setup>
import { ref, getCurrentInstance, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app'
var vue3This=getCurrentInstance().proxy; //必须定义到最外面,getCurrentInstance得到的就是当前实例this
onShow(()=>{
if(vue3This.isMounted) RecordApp.UniPageOnShow(vue3This); //onShow可能比mounted先执行,页面可能还未准备好
});
onMounted(() => {
vue3This.isMounted=true; RecordApp.UniPageOnShow(vue3This); //onShow可能比mounted先执行,页面准备好了时再执行一次
// 设置当前时间
currentTime.value = getCurrentTime()
// 页面加载时显示popup
setTimeout(() => {
showPopup.value = true
}, 100)
})
</script>录音逻辑代码
ts
/*******************下面的接口实现代码可以直接copy到你的项目里面使用**********************/
/**实现apiRequest接口,tokenApi的请求实现方法**/
var uni_ApiRequest=function(url,args,success,fail){
uni.setStorageSync("page_asr_asrTokenApi", url); //测试用的存起来
//如果你已经获得了token数据,直接success回调即可,不需要发起api请求
if(/^\s*\{.*\}\s*$/.test(url)){ //这里是输入框里面填的json数据解析直接返回
var data; try{ data=JSON.parse(url); }catch(e){};
if(!data || !data.appkey || !data.token){
fail("填写的json数据"+(!data?"解析失败":"中缺少appkey或token"));
}else{
success({ appkey:data.appkey, token:data.token });
}
return;
}
//请求url获得token数据,然后通过success返回结果
uni.request({
url:url, data:args, method:"POST", dataType:"text"
,header:{"content-type":"application/x-www-form-urlencoded"}
,success:(e)=>{
if(e.statusCode!=200){
fail("请求出错["+e.statusCode+"]");
return;
}
try{
var data=JSON.parse(e.data);
}catch(e){
fail("请求结果不是json格式:"+e.data);
return;
}
//【自行修改】根据自己的接口格式提取出数据并回调
if(data.c!==0){
fail("接口调用错误:"+data.m);
return;
}
data=data.v;
success({ appkey:data.appkey, token:data.token });
}
,fail:(e)=>{
fail(e.errMsg||"请求出错");
}
});
};
/**实现compatibleWebSocket接口**/
var uni_WebSocket=function(url){
//事件回调
var ws={
onopen:()=>{}
,onerror:(event)=>{}
,onclose:(event)=>{}
,onmessage:(event)=>{}
};
var store=ws.storeData={};
//发送数据,data为字符串或者arraybuffer
ws.send=(data)=>{
store.wsTask.send({ data:data });
};
//进行连接
ws.connect=()=>{
var wsTask=store.wsTask=uni.connectSocket({
url:url
// protocols:['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjYwMDE3NTIiLCJleHAiOjE3NjM2ODAyNTB9.5bs8MMWWro6DMVu8jzpejIjOZG0n9M4NO89UiDNz1bc']
,success:()=>{ }
,fail:(res)=>{
if(store.isError)return; store.isError=1;
ws.onerror({message:"创建连接出现错误:"+res.errMsg});
}
});
wsTask.onOpen(()=>{
if(store.isOpen)return; store.isOpen=1;
ws.onopen();
});
wsTask.onClose((e)=>{
if(store.isClose)return; store.isClose=1;
ws.onclose({ code:e.code||-1, reason:e.reason||"" });
});
wsTask.onError((e)=>{
if(store.isError)return; store.isError=1;
ws.onerror({ message:e.errMsg||"未知错误" });
});
wsTask.onMessage((e)=>{ ws.onmessage({data:e.data}); });
};
//关闭连接
ws.close=(code,reason)=>{
var obj={};
if(code!=null)obj.code=code;
if(reason!=null)obj.reason=reason;
store.wsTask.close(obj);
};
return ws;
};
// 获取当前录音键标签
const currentKeyTag = () => {
if (!RecordApp.Current) return "[?]";
// #ifdef APP
var tag2 = "Renderjs+H5";
if (RecordApp.UniNativeUtsPlugin) {
tag2 = RecordApp.UniNativeUtsPlugin.nativePlugin ? "NativePlugin" : "UtsPlugin";
}
return RecordApp.Current.Key + "(" + tag2 + ")";
// #endif
return RecordApp.Current.Key;
}
// 开始录音,然后开始语音识别
const recStart = async () => {
var sid = ++SyncID.value;
if (!asrTokenApi.value) {
reclog("需要提供TokenApi", "1");
return;
}
if (asr.value) {
reclog("上次asr未关闭", "1");
return;
}
reclog("正在请求录音权限...");
console.log('开始语音录制')
// APP端开始录音时震动反馈
try {
// 检查是否支持 plus API
if (typeof plus !== 'undefined' && plus.device && typeof plus.device.vibrate === 'function') {
// 使用plus.device.vibrate,增加震动时长到100毫秒(更明显)
plus.device.vibrate(80)
console.log('震动反馈已触发(plus.device,100ms)')
} else if (typeof uni !== 'undefined' && uni.vibrateShort) {
// 备用方案:使用 uni API
uni.vibrateShort({
success: () => {
console.log('震动反馈成功(uni API)')
},
fail: (err) => {
console.warn('震动反馈失败:', err)
}
})
} else {
console.warn('震动功能不可用')
}
} catch (error) {
console.warn('震动功能异常:', error)
}
console.log('UniWebViewActivate 1 ')
// vue3This.$refs.player.setPlayBytes(null);
RecordApp.UniWebViewActivate(vue3This); // App环境下必须先切换成当前页面WebView
console.log('UniWebViewActivate')
RecordApp.UniAppUseLicense='我已获得UniAppID=****的商用授权';
<!-- #ifdef H5 -->
RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置,在h5(H5、App+renderjs)中必须提前配置,因为h5中RequestPermission会直接打开录音
<!-- #endif -->
RecordApp.RequestPermission(() => {
reclog(currentKeyTag() + " 已获得录音权限", "2");
recStart__asrStart(sid);
}, (msg: string, isUserNotAllow?: boolean) => {
if (isUserNotAllow) {
// 用户拒绝了录音权限
// 这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
requestRecordPermission()
}
reclog(currentKeyTag() + " "
+ (isUserNotAllow ? "isUserNotAllow," : "") + "请求录音权限失败:" + msg, "1");
});
isRecording.value = true
recordFilePath.value = ''
recordStartTime.value = Date.now() // 记录录音开始时间
}
// 开始录音
const recStart__2 = (sid: number) => {
if (sid != SyncID.value) {
reclog("sync cancel recStart__2", "#f60");
return;
}
reclog(currentKeyTag() + " 正在打开录音...");
RecordApp.UniWebViewActivate(vue3This); // App环境下必须先切换成当前页面WebView
RecordApp.Start({
type: "wav",
bitRate: 16,
sampleRate: 16000,
onProcess: (buffers: any[], powerLevel: number, duration: number, sampleRate: number, newBufferIdx: number, asyncEnd: any) => {
if (sid != SyncID.value) return;
if (asr.value) { // 已打开实时语音识别
asr.value.input(buffers, sampleRate, newBufferIdx);
}
recpowerx.value = powerLevel;
recpowert.value = formatTime(duration, 1) + " / " + powerLevel;
// H5、小程序等可视化图形绘制,直接运行在逻辑层;App里面需要在onProcess_renderjs中进行这些操作
// #ifdef H5 || MP-WEIXIN
if (waveView.value) {
waveView.value.input(buffers[buffers.length - 1], powerLevel, sampleRate);
}
// #endif
},
onProcess_renderjs: `function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//App中是在renderjs中进行的可视化图形绘制
if(this.waveView){
this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
}
}`,
stop_renderjs: `function(aBuf,duration,mime){
//App中可以放一个函数,在Stop成功时renderjs中会先调用这里的代码,this是renderjs模块的this(也可以用This变量)
this.audioData=aBuf; //留着给Stop时进行转码成wav播放
}`
}, () => {
reclog(currentKeyTag() + " 已开始录音,请讲话(asrProcess中已限制最多识别60*2-5*(2-1)=115秒)...", "2");
// 创建音频可视化图形绘制,App环境下是在renderjs中绘制,H5、小程序等是在逻辑层中绘制,因此需要提供两段相同的代码(宽高值需要和canvas style的宽高一致)
// RecordApp.UniFindCanvas(null, [".recwave-WaveView"], `
// this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
// `, (canvas1: any) => {
// waveView.value = Recorder.WaveView({ compatibleCanvas: canvas1, width: 300, height: 100 });
// });
RecordApp.UniFindCanvas(null,[".recwave-Histogram3"],`
this.waveView=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
,lineCount:20,position:0,minHeight:4,fallDuration:400,stripeEnable:false,mirrorEnable:true
,linear:[0,"#ffffff",1,"#ffffff"]});
`,(canvas1:any)=>{
waveView.value=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
,lineCount:20,position:0,minHeight:4,fallDuration:400,stripeEnable:false,mirrorEnable:true
,linear:[0,"#ffffff",1,"#ffffff"]});
});
}, (msg: string) => {
reclog(currentKeyTag() + " 开始录音失败:" + msg, "1");
recCancel("开始录音失败"); // 立即结束语音识别
});
}
// 开始语音识别
const recStart__asrStart = (sid: number) => {
if (sid != SyncID.value) {
reclog("sync cancel recStart__asrStart", "#f60");
return;
}
// 创建语音识别对象,每次识别都要新建,asr不能共用
var asrInstance = Recorder.ASR_Aliyun_Short({
tokenApi: asrTokenApi.value,
apiArgs: {
lang: asrLang.value
},
apiRequest: uni_ApiRequest, // tokenApi的请求实现方法
compatibleWebSocket: uni_WebSocket, // 返回兼容WebSocket的对象
asrProcess: (text: string, nextDuration: number, abortMsg?: string) => {
/***实时识别结果,必须返回true才能继续识别,否则立即超时停止识别***/
// 检查是否已被取消
if (isRecordingCancelled.value) {
reclog("[asrProcess回调]录音已取消", "1");
recCancel("录音已取消");
isRecordingCancelled.value = false
recordStartTime.value = 0
isRecording.value = false
return false;
}
if (abortMsg) {
// 语音识别中途出错
reclog("[asrProcess回调]被终止:" + abortMsg, "1");
recCancel("语音识别出错"); // 立即结束录音,就算继续录音也不会识别
return false;
}
asrTxt.value = text;
asrTime.value = ("识别时长: " + formatTime(asrInstance.asrDuration())
+ " 已发送数据时长: " + formatTime(asrInstance.sendDuration()));
return nextDuration <= 2 * 60 * 1000; // 允许识别2分钟的识别时长(比录音时长小5秒)
},
log: (msg: string, color?: string) => {
reclog(msg, color == "1" ? "#faa" : "#aaa");
}
});
asr.value = asrInstance;
reclog("语言:" + asrInstance.set.apiArgs.lang + ",tokenApi:" + asrInstance.set.tokenApi + ",正在打开语音识别...");
// 打开语音识别,建议先打开asr,成功后再开始录音
asrInstance.start(() => {
// 无需特殊处理start和stop的关系,只要调用了stop,会阻止未完成的start,不会执行回调
reclog("已开始语音识别", "2");
recStart__2(sid);
}, (errMsg: string) => {
reclog("语音识别开始失败,请重试:" + errMsg, "1");
recCancel("语音识别开始失败");
});
}
// 停止录音,结束语音识别
const recStop = () => {
++SyncID.value;
// 保存当前状态,防止在异步操作中被重置
const currentAction = recordingAction.value
const isCurrentlyRecording = isRecording.value
console.log('recStop 被调用,recordingAction:', currentAction, 'isRecording:', isCurrentlyRecording)
// 如果滑动到取消区域,执行取消操作
if (currentAction === 'cancel' && isCurrentlyRecording) {
console.log('滑动到取消区域,执行取消操作')
// 先设置取消标志,防止后续操作
isRecordingCancelled.value = true
// 先取消录音并关闭语音层(同步操作)
cancelVoiceRecording()
// 然后中断语音识别(异步操作)
recCancel('用户取消录音')
return
}
// 如果滑动到转文字区域,停止录音和语音识别但不关闭遮罩层
if (currentAction === 'text' && isCurrentlyRecording) {
console.log('滑动到转文字区域,停止录音和语音识别但不关闭遮罩层')
// 停止录音但不关闭遮罩层
stopVoiceRecordingForText()
// 停止语音识别但不关闭遮罩层
recCancelForText()
// 标记转文字模式已就绪,按钮将变为发送按钮
isTextModeReady.value = true
return
}
// 正常停止录音
recCancel();
}
const recCancel = (cancelMsg?: string) => {
reclog("正在停止...");
var asr2 = asr.value;
asr.value = null; // 先干掉asr,防止重复stop
if (!asr2) {
reclog("未开始识别", "1");
} else {
// asr.stop 和 rec.stop 无需区分先后,同时执行就ok了
asr2.stop((text: string, abortMsg?: string) => {
if (abortMsg) {
abortMsg = "发现识别中途被终止(一般无需特别处理):" + abortMsg;
}
reclog("语音识别完成" + (abortMsg ? "," + abortMsg : ""), abortMsg ? "#f60" : "2");
reclog("识别最终结果:" + text, "2");
// 检查是否从取消按钮松开(需要同时检查 recordingAction 和 isRecordingCancelled)
const isCanceled = cancelMsg || isRecordingCancelled.value || recordingAction.value === 'cancel'
// 默认解析语音识别的文字并发送出去
// 如果识别结果不为空且未被取消,则自动发送
if (text && text.trim() && !isCanceled && !isWaitingForResponse.value) {
// 设置识别结果到输入框
inputMessage.value = text.trim()
nextTick(() => {
sendMessage()
})
} else if (isCanceled) {
// 如果是取消操作,不发送识别结果,也不弹出任何提示
reclog("录音已取消,不发送识别结果", "1");
} else if (!text || !text.trim()) {
// 只有在非取消状态下,识别结果为空时才弹出提示
reclog("识别结果为空,不发送", "1");
uni.showToast({
title: '未识别出有效内容',
icon: 'none',
duration: 2000
})
} else if (isWaitingForResponse.value) {
reclog("AI正在回答中,不发送识别结果", "1");
}
}, (errMsg: string) => {
reclog("语音识别" + (cancelMsg ? "被取消" : "结束失败") + ":" + errMsg, "1");
});
}
RecordApp.Stop((aBuf: ArrayBuffer, duration: number, mime: string) => {
// 得到录音数据,可以试听参考
var recSet = (RecordApp.GetCurrentRecOrNull() || { set: { type: "wav" } }).set;
reclog("已录制[" + mime + "]:" + formatTime(duration, 1) + " " + aBuf.byteLength + "字节 "
+ recSet.sampleRate + "hz " + recSet.bitRate + "kbps", "2");
var aBuf_renderjs = "this.audioData";
// 播放,部分格式会转码成wav播放
if (playerRef.value) {
playerRef.value.setPlayBytes(aBuf, aBuf_renderjs, duration, mime, recSet, Recorder);
}
}, (msg: string) => {
reclog("结束录音失败:" + msg, "1");
// 如果是取消操作,不弹出提示
if (!cancelMsg && !isRecordingCancelled.value && recordingAction.value !== 'cancel') {
uni.showToast({
title: '未识别出有效内容',
icon: 'none',
duration: 2000
})
}
});
// 关闭语音层(隐藏录音遮罩层)
isRecording.value = false
recordingAction.value = 'none'
isSlidingToAction.value = false
isRecordingCancelled.value = false
recordStartTime.value = 0
}
// 停止语音识别但不关闭遮罩层(用于转文字功能)
const recCancelForText = () => {
reclog("正在停止语音识别(转文字模式)...");
var asr2 = asr.value;
asr.value = null; // 先干掉asr,防止重复stop
if (!asr2) {
reclog("未开始识别", "1");
} else {
// asr.stop 和 rec.stop 无需区分先后,同时执行就ok了
asr2.stop((text: string, abortMsg?: string) => {
if (abortMsg) {
abortMsg = "发现识别中途被终止(一般无需特别处理):" + abortMsg;
}
reclog("语音识别完成" + (abortMsg ? "," + abortMsg : ""), abortMsg ? "#f60" : "2");
reclog("识别最终结果:" + text, "2");
// 转文字模式:将识别结果显示在遮罩层的文字气泡中
if (text && text.trim()) {
asrTxt.value = text.trim()
reclog("识别结果已显示在文字气泡中", "2");
} else {
// 转文字模式下,即使识别结果为空也不弹出提示,让用户自己决定是否取消
reclog("识别结果为空", "1");
asrTxt.value = '' // 清空文字气泡
}
}, (errMsg: string) => {
reclog("语音识别结束失败:" + errMsg, "1");
});
}
RecordApp.Stop((aBuf: ArrayBuffer, duration: number, mime: string) => {
// 得到录音数据,可以试听参考
var recSet = (RecordApp.GetCurrentRecOrNull() || { set: { type: "wav" } }).set;
reclog("已录制[" + mime + "]:" + formatTime(duration, 1) + " " + aBuf.byteLength + "字节 "
+ recSet.sampleRate + "hz " + recSet.bitRate + "kbps", "2");
var aBuf_renderjs = "this.audioData";
// 播放,部分格式会转码成wav播放
if (playerRef.value) {
playerRef.value.setPlayBytes(aBuf, aBuf_renderjs, duration, mime, recSet, Recorder);
}
}, (msg: string) => {
reclog("结束录音失败:" + msg, "1");
// 转文字模式下,即使录音失败也不弹出提示,让用户自己决定是否取消
// 如果是取消操作,也不弹出提示
if (!isRecordingCancelled.value && recordingAction.value !== 'cancel') {
uni.showToast({
title: '未识别出有效内容',
icon: 'none',
duration: 2000
})
}
});
// 注意:不关闭遮罩层,保持 isRecording.value = true,以便显示识别文字
// 不重置 recordingAction,保持 'text' 状态
// 不重置 isSlidingToAction,保持滑动状态
recordStartTime.value = 0
}
// 记录日志
const reclog = (msg: string, color?: string) => {
var now = new Date();
var t = ("0" + now.getHours()).substr(-2)
+ ":" + ("0" + now.getMinutes()).substr(-2)
+ ":" + ("0" + now.getSeconds()).substr(-2);
var txt = "[" + t + "]" + msg;
console.log(txt);
reclogs.value.splice(0, 0, { txt: txt, color: color });
}
// 格式化时间
const formatTime = (ms: number, showSS?: number) => {
var ss = ms % 1000;
ms = (ms - ss) / 1000;
var s = ms % 60;
ms = (ms - s) / 60;
var m = ms % 60;
ms = (ms - m) / 60;
var h = ms;
var v = "";
if (h > 0) v += (h < 10 ? "0" : "") + h + ":";
v += (m < 10 ? "0" : "") + m + ":";
v += (s < 10 ? "0" : "") + s;
if (showSS) v += "″" + ("00" + ss).substr(-3);
return v;
}