generated from template/vite-react-template
temp
This commit is contained in:
parent
e66f7ce00e
commit
a696bc3bbe
5
backend/demo/index.html
Normal file
5
backend/demo/index.html
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
<script src="recorder.mp3.min.js"></script> <!--已包含recorder-core和mp3格式支持, CDN不稳定仅供测试: https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/recorder.mp3.min.js-->
|
||||
|
||||
|
||||
<script src="./index.js"></script>
|
393
backend/demo/index.js
Normal file
393
backend/demo/index.js
Normal file
@ -0,0 +1,393 @@
|
||||
/******************
|
||||
《【教程】【ASR】实时语音识别、音频文件转文字-阿里云版》
|
||||
作者:高坚果
|
||||
时间:2020-7-22 22:37:09
|
||||
|
||||
通过阿里云语音识别(语音转文字)插件 /src/extensions/asr.aliyun.short.js,可实现实时语音识别、单个语音文件转文字。
|
||||
|
||||
只需要后端提供一个Token生成接口,就能进行语音识别,可直接参考或本地运行此NodeJs后端测试程序:/assets/demo-asr/NodeJsServer_asr.aliyun.short.js,配置好代码里的阿里云账号后,在目录内直接命令行执行`node NodeJsServer_asr.aliyun.short.js`即可运行提供本地测试接口。
|
||||
|
||||
目前暂未提供其他版本的语音识别插件,比如腾讯云、讯飞等,搭配使用Recorder的onProcess实时处理,可根据自己的业务需求选择对应厂家自行对接即可,如需定制开发请联系作者。
|
||||
******************/
|
||||
|
||||
var asr;
|
||||
|
||||
/*************单个语音文件转文字例子,你也可以录完音后再一次性进行识别***************/
|
||||
//将录音文件进行语音识别,支持的录音格式取决于浏览器的支持,兼容性mp3最好,wav次之,其他格式不一定能够解码
|
||||
var fileToText=function(audioBlob,fileName){
|
||||
if(asr){
|
||||
Runtime.Log("上次asr未关闭",1);
|
||||
return;
|
||||
};
|
||||
Runtime.Log("开始识别文件:《"+fileName+"》,asrProcess中已限制最多识别前60*3-5*(3-1)=170秒 ...");
|
||||
$(".recAsrTxt").text("");
|
||||
$(".recAsrTime").html("");
|
||||
|
||||
var url=$(".asrTokenApi").val();
|
||||
var urlReq=null;
|
||||
if(/^\s*\{.*\}\s*$/.test(url)){
|
||||
//这里是输入框里面填的json数据,直接success回调即可
|
||||
urlReq=function(url,args,success,fail){
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var asr2=asr=Recorder.ASR_Aliyun_Short({
|
||||
tokenApi:url
|
||||
,apiArgs:{
|
||||
lang:$("[name=arsLang]:checked").val()
|
||||
,xxx:"其他请求参数"
|
||||
}
|
||||
,apiRequest:urlReq //如果提供了token数据,可不发起api请求
|
||||
,asrProcess:function(text,nextDuration,abortMsg){
|
||||
/***识别中间结果实回调,必须返回true才能继续识别,否则立即超时停止识别***/
|
||||
if(abortMsg){
|
||||
//语音识别中途出错,单个文件识别无需任何处理,会自动回调结果
|
||||
Runtime.Log("[asrProcess回调]被终止:"+abortMsg,1);
|
||||
return false;
|
||||
};
|
||||
|
||||
$(".recAsrTxt").text(text);
|
||||
$(".recAsrTime").html("识别时长: "+formatTime(asr2.asrDuration())
|
||||
+" 已发送数据时长: "+formatTime(asr2.sendDuration()));
|
||||
return nextDuration<=3*60*1000;//允许识别3分钟的识别时长(比音频时长小5*2秒)
|
||||
}
|
||||
,log:function(msg,color){
|
||||
Runtime.Log('<span style="opacity:0.15">'+msg+'</span>',color);
|
||||
}
|
||||
});
|
||||
Runtime.Log("语言:"+asr.set.apiArgs.lang);
|
||||
//语音文件识别只需调用audioToText即可完成识别,简单快速
|
||||
asr.audioToText(audioBlob,function(text,abortMsg){
|
||||
asr=null;
|
||||
if(abortMsg){
|
||||
Runtime.Log("发现识别中途被终止(一般无需特别处理):"+abortMsg,"#fb8");
|
||||
};
|
||||
Runtime.Log("文件识别最终结果:"+text, 2);
|
||||
},function(errMsg){
|
||||
asr=null;
|
||||
Runtime.Log("文件识别结束失败:"+errMsg, 1);
|
||||
});
|
||||
};
|
||||
/*************单个语音文件转文字例子 END***************/
|
||||
|
||||
|
||||
|
||||
/*************实时语音识别例子*************************/
|
||||
/******界面交互处理、打开录音权限******/
|
||||
//使用长按录音的方式,可有效控制转换时长,避免不必要的资源浪费
|
||||
//长按按钮功能已经封装好了,直接调用 BindTouchButton 即可快速实现长按
|
||||
var bindTouchButton=function(){
|
||||
DemoFragment.BindTouchButton(
|
||||
"recTouchBtn"
|
||||
,"按住进行录音+识别"
|
||||
,"松开结束"
|
||||
,{}
|
||||
,asrOnTouchStart
|
||||
,asrOnTouchEnd
|
||||
);
|
||||
};
|
||||
|
||||
var rec;
|
||||
/**打开录音,先得到录音权限**/
|
||||
function recOpenClick(){
|
||||
$(".recOpenBtn").hide();
|
||||
$(".recCloseBtn").show();
|
||||
var end=function(isOk){
|
||||
if(isOk){
|
||||
$(".asrStartBtns").show();
|
||||
$(".asrStopBtn").show();
|
||||
};
|
||||
};
|
||||
|
||||
rec=Recorder({
|
||||
type:"wav"
|
||||
,sampleRate:16000
|
||||
,bitRate:16
|
||||
,onProcess:function(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd){
|
||||
Runtime.Process.apply(null,arguments);
|
||||
|
||||
//实时推入asr处理。asr.input随时都可以调用,就算asr并未start,会缓冲到asr.start完成然后将已input的数据进行识别
|
||||
if(asr){
|
||||
//buffers是从录音开头到现在的缓冲,因此需要提供 buffersOffset=newBufferIdx
|
||||
asr.input(buffers, bufferSampleRate, newBufferIdx);
|
||||
};
|
||||
}
|
||||
});
|
||||
var t=setTimeout(function(){
|
||||
recAsrStatus("无法录音:权限请求被忽略(超时假装手动点击了确认对话框)",1);
|
||||
rec=null;
|
||||
end(false);
|
||||
},8000);
|
||||
|
||||
recAsrStatus("正在打开录音权限,请稍后...");
|
||||
rec.open(function(){//打开麦克风授权获得相关资源
|
||||
clearTimeout(t);
|
||||
recAsrStatus("录音已打开,可以长按录音+识别了",2);
|
||||
end(true);
|
||||
},function(msg,isUserNotAllow){//用户拒绝未授权或不支持
|
||||
clearTimeout(t);
|
||||
recAsrStatus((isUserNotAllow?"UserNotAllow,":"")+"无法录音:"+msg, 1);
|
||||
rec=null;
|
||||
end(false);
|
||||
});
|
||||
};
|
||||
var recCloseClick=function(){
|
||||
$(".recOpenBtn").show();
|
||||
$(".recCloseBtn").hide();
|
||||
$(".asrStartBtns").hide();
|
||||
if(rec){
|
||||
Runtime.Log("已关闭录音");
|
||||
rec.close();
|
||||
rec=null;
|
||||
}else{
|
||||
Runtime.Log("未打开录音",1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//免长按,这里就是调用的长按时的两个状态方法 功能是一样的
|
||||
var asrStartClick_NoTouch=function(){
|
||||
asrOnTouchStart(function(){});
|
||||
};
|
||||
var asrStopClick_NoTouch=function(){
|
||||
if(!asr){
|
||||
Runtime.Log("未开始识别",1);
|
||||
return;
|
||||
};
|
||||
asrOnTouchEnd(false,true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/******核心的长按录音识别******/
|
||||
/**长按开始录音**/
|
||||
var asrOnTouchStart=function(cancel){
|
||||
if(!rec){
|
||||
cancel("未打开录音");
|
||||
recAsrStatus("未打开录音",1);
|
||||
return;
|
||||
};
|
||||
rec.s_isStart=false;
|
||||
if(asr){
|
||||
cancel("上次asr未关闭");
|
||||
recAsrStatus("上次asr未关闭",1);
|
||||
return;
|
||||
};
|
||||
$(".recAsrTxt").text("");
|
||||
$(".recAsrTime").html("");
|
||||
|
||||
var url=$(".asrTokenApi").val();
|
||||
var urlReq=null;
|
||||
if(/^\s*\{.*\}\s*$/.test(url)){
|
||||
//这里是输入框里面填的json数据,直接success回调即可
|
||||
urlReq=function(url,args,success,fail){
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//创建语音识别对象,每次识别都要新建,asr不能共用
|
||||
var asr2=asr=Recorder.ASR_Aliyun_Short({
|
||||
tokenApi:url
|
||||
,apiArgs:{
|
||||
lang:$("[name=arsLang]:checked").val()
|
||||
,xxx:"其他请求参数"
|
||||
}
|
||||
,apiRequest:urlReq //如果提供了token数据,可不发起api请求
|
||||
,asrProcess:function(text,nextDuration,abortMsg){
|
||||
/***实时识别结果,必须返回true才能继续识别,否则立即超时停止识别***/
|
||||
if(abortMsg){
|
||||
//语音识别中途出错
|
||||
recAsrStatus("[asrProcess回调]被终止:"+abortMsg,1);
|
||||
cancel("语音识别出错");//立即结束录音,就算继续录音也不会识别
|
||||
return false;
|
||||
};
|
||||
|
||||
$(".recAsrTxt").text(text);
|
||||
$(".recAsrTime").html("识别时长: "+formatTime(asr2.asrDuration())
|
||||
+" 已发送数据时长: "+formatTime(asr2.sendDuration()));
|
||||
return nextDuration<=2*60*1000;//允许识别2分钟的识别时长(比录音时长小5秒)
|
||||
}
|
||||
,log:function(msg,color){
|
||||
Runtime.Log('<span style="opacity:0.15">'+msg+'</span>',color);
|
||||
}
|
||||
});
|
||||
Runtime.Log("语言:"+asr.set.apiArgs.lang);
|
||||
recAsrStatus("连接服务器中,请稍后...");
|
||||
//打开语音识别,建议先打开asr,成功后再开始录音
|
||||
asr.start(function(){//无需特殊处理start和stop的关系,只要调用了stop,会阻止未完成的start,不会执行回调
|
||||
//开始录音
|
||||
Runtime.Log("开始录音...");
|
||||
rec.start();
|
||||
rec.s_isStart=true;
|
||||
|
||||
recAsrStatus("滴~~ 已开始语音识别,请讲话(asrProcess中已限制最多识别60*2-5*(2-1)=115秒)...",2);
|
||||
},function(errMsg){
|
||||
recAsrStatus("语音识别开始失败,请重试:"+errMsg,1);
|
||||
|
||||
cancel("语音识别开始失败");
|
||||
});
|
||||
};
|
||||
|
||||
/**松开停止录音**/
|
||||
var asrOnTouchEnd=function(isCancel,isUser){
|
||||
recAsrStatus(isCancel?"已取消":isUser?"已松开":"长按被取消",isUser?0:1);
|
||||
|
||||
var asr2=asr;asr=null;//先干掉asr,防止重复stop
|
||||
if(!asr2){
|
||||
Runtime.Log("未开始识别",1);
|
||||
}else{
|
||||
//asr.stop 和 rec.stop 无需区分先后,同时执行就ok了
|
||||
asr2.stop(function(text,abortMsg){
|
||||
if(abortMsg){
|
||||
abortMsg="发现识别中途被终止(一般无需特别处理):"+abortMsg;
|
||||
};
|
||||
recAsrStatus("语音识别完成"+(abortMsg?","+abortMsg:""),abortMsg?"#f60":2);
|
||||
Runtime.Log("识别最终结果:"+text, 2);
|
||||
},function(errMsg){
|
||||
recAsrStatus("语音识别"+(!isUser?"被取消":"结束失败")+":"+errMsg, 1);
|
||||
});
|
||||
};
|
||||
|
||||
var rec2=rec;
|
||||
if(rec2.s_isStart){
|
||||
rec2.s_isStart=false;
|
||||
rec2.stop(function(blob,duration){
|
||||
Runtime.LogAudio(blob,duration,rec2);
|
||||
},function(errMsg){
|
||||
Runtime.Log("录音失败:"+errMsg, 1);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**更新状态**/
|
||||
var recAsrStatus=function(html,color){
|
||||
var elem=document.querySelector(".recAsrStatus");
|
||||
elem.style.color=color==1?"red":color==2?"#0b1":(color||null);
|
||||
elem.innerHTML=html;
|
||||
|
||||
Runtime.Log(html,color);
|
||||
};
|
||||
/*************实时语音识别例子 END***************/
|
||||
|
||||
|
||||
|
||||
//破坏环境,测试错误是否被正确处理
|
||||
var killToken=function(){
|
||||
if(!asr){
|
||||
Runtime.Log("未开始语音识别",1);
|
||||
return;
|
||||
}
|
||||
asr.set.apiRequest=function(url,args,success,fail){
|
||||
fail("不让获取Token");
|
||||
}
|
||||
Runtime.Log("已设置ASR的apiRequest,下一分钟将无法获得Token");
|
||||
};
|
||||
var killWs=function(){
|
||||
if(!asr || !asr.wsCur){
|
||||
Runtime.Log("未开始语音识别",1);
|
||||
return;
|
||||
}
|
||||
asr.wsCur.close();
|
||||
Runtime.Log("已强制关闭了ASR的WebSocket连接");
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//=====以下代码无关紧要,音频数据源和界面==================
|
||||
//加载录音框架
|
||||
Runtime.Import([
|
||||
{url:RootFolder+"/src/recorder-core.js",check:function(){return !window.Recorder}}
|
||||
,{url:RootFolder+"/src/engine/wav.js",check:function(){return !Recorder.prototype.wav}}
|
||||
,{url:RootFolder+"/src/extensions/asr.aliyun.short.js",check:function(){return !Recorder.ASR_Aliyun_Short}}
|
||||
|
||||
,{url:RootFolder+"/assets/runtime-codes/fragment.touch_button.js",check:function(){return !window.DemoFragment||!DemoFragment.BindTouchButton}}//引入BindTouchButton
|
||||
]);
|
||||
|
||||
|
||||
//显示控制按钮
|
||||
Runtime.Ctrls([
|
||||
{html:'\
|
||||
<hr/><div class="arsLangs" style="padding:5px 0">识别语言模型:\
|
||||
<style>.arsLangs label{cursor: pointer;margin-right:10px}</style>\
|
||||
<label><input type="radio" name="arsLang" value="普通话" checked>普通话</label>\
|
||||
<label><input type="radio" name="arsLang" value="粤语">粤语</label>\
|
||||
<label><input type="radio" name="arsLang" value="英语">英语</label>\
|
||||
<label><input type="radio" name="arsLang" value="日语">日语</label>\
|
||||
</div>\
|
||||
<hr/><div style="padding:5px 0">Token Api:\
|
||||
<input class="asrTokenApi" value="'+localTokenApi(1).replace(/"/g,""")+'" placeholder="请填写api地址 或 token的json数据" style="width:240px"/>\
|
||||
<div style="font-size:13px;color:#999;word-break:break-all;">\
|
||||
<div>你可以下载Recorder仓库<a href="https://gitee.com/xiangyuecn/Recorder/tree/master/assets/demo-asr" target="_blank">/assets/demo-asr</a>内的nodejs服务器端脚本到电脑上,配置好代码里的阿里云账号,然后运行此服务器端脚本即可提供本地测试接口</div>\
|
||||
<div>如果无法访问此api地址,比如手机上,你可以根据服务器脚本中的提示在电脑上打http地址,手动复制或自行提供 {appkey:"...",token:"..."} ,先删掉上面输入框中的url再粘贴json进去即可使用</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
'}
|
||||
,{html:'<hr/>\
|
||||
<div>\
|
||||
<span style="margin-right:170px">\
|
||||
<button class="mainBtn recOpenBtn" onclick="recOpenClick()">单击此处打开 录音+识别 功能</button>\
|
||||
<button class="mainBtn recCloseBtn" onclick="recCloseClick()" style="display:none;">关闭录音</button>\
|
||||
</span>\
|
||||
\
|
||||
<button class="mainBtn asrStartBtns" style="display:none;" onclick="asrStartClick_NoTouch()">免按住开始录音+识别</button>\
|
||||
<button class="mainBtn asrStopBtn" onclick="asrStopClick_NoTouch()" style="display:none;">结束语音识别</button>\
|
||||
</div>\
|
||||
\
|
||||
<div>\
|
||||
<button class="mainBtn recTouchBtn asrStartBtns" style="width:260px;height:60px;line-height:60px;display:none;"></button>\
|
||||
</div>\
|
||||
\
|
||||
<div class="recAsrStatus"></div>\
|
||||
\
|
||||
<hr/>\
|
||||
<div style="margin:12px 0 6px;font-size:12px">实时识别结果: <span class="recAsrTime"></span></div>\
|
||||
<div class="recAsrTxt" style="padding:15px 10px;min-height:50px;margin-bottom:12px;border:3px dashed #a2a1a1"></div>\
|
||||
'}
|
||||
|
||||
,{name:"破坏ASR配置,下一分钟得不到Token",click:"killToken"}
|
||||
,{name:"强制断开ASR的WebSocket连接",click:"killWs"}
|
||||
,{choiceFile:{
|
||||
multiple:false
|
||||
,name:"音频(已限制最多前3分钟内转成文字)"
|
||||
,mime:"audio/*"
|
||||
,process:function(fileName,arrayBuffer,filesCount,fileIdx,endCall){
|
||||
fileToText(new Blob([arrayBuffer]),fileName);
|
||||
endCall();
|
||||
}
|
||||
}}
|
||||
]);
|
||||
|
||||
bindTouchButton();
|
||||
|
||||
var formatTime=function(n){//格式化毫秒成 分:秒
|
||||
n=Math.round(n/1000);
|
||||
var s=n%60;
|
||||
var m=(n-s)/60;
|
||||
return m+":"+("0"+s).substr(-2);
|
||||
};
|
||||
|
||||
|
||||
function localTokenApi(useSet){
|
||||
var url="http://127.0.0.1:9527/token";
|
||||
if(useSet){
|
||||
url=localStorage["ASR_Aliyun_Short_TokenApi"]||url;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
$(".asrTokenApi").bind("change",function(){
|
||||
localStorage["ASR_Aliyun_Short_TokenApi"]=this.value==localTokenApi()?"":this.value;
|
||||
});
|
6
backend/demo/recorder.mp3.min.js
vendored
Normal file
6
backend/demo/recorder.mp3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
backend/package.json
Normal file
14
backend/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.7.1",
|
||||
"type": "module"
|
||||
}
|
312
backend/src/NodeJsServer_asr.aliyun.short.cjs
Normal file
312
backend/src/NodeJsServer_asr.aliyun.short.cjs
Normal file
@ -0,0 +1,312 @@
|
||||
/*******************************************
|
||||
ASR 阿里云-智能语音交互-一句话识别 生成token的api接口,本地测试http后端服务
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
【运行】直接在当前文件夹内用命令行执行命令,即可运行:
|
||||
node NodeJsServer_asr.aliyun.short.js
|
||||
|
||||
【运行前先修改以下配置必填项】:
|
||||
*******************************************/
|
||||
global.Config = {
|
||||
//请到阿里云开通 一句话识别 服务(可试用一段时间,正式使用时应当开通商用版,很便宜),得到AccessKey、Secret,参考:https://help.aliyun.com/document_detail/324194.html
|
||||
AccessKey: 'LTAI5tDazeiB3pYYUwnMuLAt', //【必填】根据文档创建RAM用户,分配好权限,生成访问密钥
|
||||
Secret: '2UnGJO6GLZIVXS249B4wgi9uTKknLY', //【必填】
|
||||
|
||||
//请到阿里云智能语音交互控制台创建相应的语音识别项目,每个项目可以设置一种语言模型,要支持多种语言就创建多个项目,然后填写每个项目对应的Appkey到这里,前端调用本接口的时候传入lang参数来获取到对应的Appkey
|
||||
Langs: {
|
||||
普通话: 'xGuvvqhseLDauN2r', //【必填】至少创建一个普通话语言模型的项目用于测试,填对应的Appkey
|
||||
粤语: '',
|
||||
英语: '',
|
||||
日语: '',
|
||||
//...... 按需提供语言对应的项目Appkey
|
||||
},
|
||||
};
|
||||
|
||||
global.Port = 9527; //http服务端口
|
||||
global.SSL = {
|
||||
CertFile: '', //cert.pem 证书路径,不为空将开启https
|
||||
KeyFile: '', //cert.key 密钥路径
|
||||
};
|
||||
const Config = global.Config;
|
||||
|
||||
//如果提供了这个本地文件,就加载过来,可覆盖global下挂载的对象
|
||||
var fs = require('fs');
|
||||
var localFile = 'NodeJsServer_asr.aliyun.short__local.cjs';
|
||||
fs.existsSync(localFile) && require('./' + localFile);
|
||||
|
||||
var GetLangKeys = function () {
|
||||
var keys = [];
|
||||
for (var k in Config.Langs) if (Config.Langs[k]) keys.push(k);
|
||||
return keys;
|
||||
};
|
||||
|
||||
/***************一句话识别Token生成***************/
|
||||
var GetAsrToken = async function (ctx, args) {
|
||||
var lang = args.lang; //unsafe
|
||||
var rtv = {};
|
||||
ctx.setValue(rtv);
|
||||
var errTag = '[GetAsrToken]';
|
||||
|
||||
if (!Config.AccessKey || !Config.Secret) {
|
||||
return ctx.error(errTag + '未配置AccessKey');
|
||||
}
|
||||
console.log('Config', lang);
|
||||
|
||||
//根据前端需要的语言获得appkey,一种语言模型对应一个项目
|
||||
var appkey = Config.Langs[lang];
|
||||
if (!appkey) {
|
||||
var errMsg = 'lang[' + lang + ']未配置Appkey';
|
||||
if (!lang) errMsg = '请求需要提供lang参数';
|
||||
return ctx.error(errTag + errMsg + ',已配置的可用lang值' + JSON.stringify(GetLangKeys()));
|
||||
}
|
||||
rtv.appkey = appkey;
|
||||
|
||||
//获得Access Token,Token有效期内可以重复使用,实际使用时应当全局共享,并做好并发控制
|
||||
var cache = AccessToken_RedisCache; //假装读取Redis缓存数据
|
||||
if (!cache || cache.ExpireTime - Date.now() / 1000 < 60) {
|
||||
//缓存无效
|
||||
/*RedisLock()*/ {
|
||||
//假装加锁,得到锁的允许更新Token
|
||||
cache = AccessToken_RedisCache; //假装得到锁后,二次确认是否抢占到更新机会
|
||||
if (!cache || cache.ExpireTime - Date.now() / 1000 < 60) {
|
||||
cache = await NewAccessToken(ctx);
|
||||
if (ctx.isError()) {
|
||||
return ctx.error(errTag + ctx.getMsg());
|
||||
}
|
||||
console.log('NewAccessToken', cache);
|
||||
AccessToken_RedisCache = cache; //假装写入Redis缓存
|
||||
}
|
||||
}
|
||||
}
|
||||
rtv.token = cache.Id;
|
||||
|
||||
ctx.endLog = function () {
|
||||
console.log(
|
||||
' \x1B[32m' +
|
||||
lang +
|
||||
'token:' +
|
||||
JSON.stringify(rtv) +
|
||||
'\x1B[0m' +
|
||||
'\n 此token可重复使用,有效期到:' +
|
||||
new Date(cache.ExpireTime * 1000).toLocaleString(),
|
||||
);
|
||||
};
|
||||
return ctx;
|
||||
};
|
||||
|
||||
var AccessToken_RedisCache = null;
|
||||
//调用阿里云接口,获得新的Access Token,文档:https://help.aliyun.com/document_detail/113251.html
|
||||
var NewAccessToken = async function (ctx) {
|
||||
var params = {
|
||||
AccessKeyId: Config.AccessKey,
|
||||
Action: 'CreateToken',
|
||||
Version: '2019-02-28',
|
||||
Format: 'JSON',
|
||||
RegionId: 'cn-shanghai',
|
||||
Timestamp: JSON.stringify(new Date()).replace(/"/g, ''),
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
SignatureNonce: RandomHEX(32).replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'),
|
||||
};
|
||||
|
||||
var arr = [];
|
||||
for (var k in params) {
|
||||
arr.push(urlEncodeX(k) + '=' + urlEncodeX(params[k]));
|
||||
}
|
||||
arr.sort();
|
||||
params = arr.join('&');
|
||||
var signStr = 'GET&' + urlEncodeX('/') + '&' + urlEncodeX(params);
|
||||
var sign = HMAC('sha1', Config.Secret + '&', signStr, 'base64');
|
||||
params = 'Signature=' + urlEncodeX(sign) + '&' + params;
|
||||
|
||||
//直接发起get请求
|
||||
var load = LoadRequest('http://nls-meta.cn-shanghai.aliyuncs.com/?' + params);
|
||||
await load.getString(false);
|
||||
var respTxt = load.response.data;
|
||||
|
||||
var data = {};
|
||||
try {
|
||||
data = JSON.parse(respTxt);
|
||||
} catch (e) {}
|
||||
if (!data.Token) {
|
||||
ctx.error('获取Token失败[' + data.Code + ']:' + data.Message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.Token;
|
||||
};
|
||||
|
||||
//一些函数
|
||||
var LoadRequest = require('./lib.load.cjs');
|
||||
var Crypto = require('crypto');
|
||||
var urlEncodeX = function (val) {
|
||||
return encodeURIComponent(val).replace(/[^\w\-\.\~\%]/g, function (a) {
|
||||
return '%' + ('0' + a.charCodeAt(0).toString(16)).substr(-2).toUpperCase();
|
||||
});
|
||||
};
|
||||
var RandomHEX = function (len) {
|
||||
var s = [];
|
||||
for (var i = 0, r; i < len; i++) {
|
||||
r = Math.floor(Math.random() * 16);
|
||||
if (r < 10) {
|
||||
s.push(String.fromCharCode(r + 48));
|
||||
} else {
|
||||
s.push(String.fromCharCode(r - 10 + 97));
|
||||
}
|
||||
}
|
||||
return s.join('');
|
||||
};
|
||||
var HMAC = function (hash, key, data, resType) {
|
||||
var hmac = Crypto.createHmac(hash, key);
|
||||
hmac.update(data);
|
||||
return hmac.digest(resType === null ? null : resType || 'hex');
|
||||
};
|
||||
|
||||
/***************提供接口***************/
|
||||
var Api = {}; //async method_path0(ctx,args)
|
||||
|
||||
//内容回显
|
||||
Api.GET_ = Api.GET_echo = async function (ctx, args) {
|
||||
ctx.setValue('<h1>阿里云一句话识别TokenApi服务正在运行...</h1>', 'html');
|
||||
};
|
||||
Api.POST_ = Api.POST_echo = async function (ctx, args) {
|
||||
ctx.setValue({ url: ctx.req.url, method: ctx.req.method, headers: ctx.req.headers, args: args });
|
||||
};
|
||||
|
||||
//token接口
|
||||
Api.GET_token = Api.POST_token = async function (ctx, args) {
|
||||
await GetAsrToken(ctx, args);
|
||||
};
|
||||
|
||||
/***开启http服务***/
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var querystring = require('querystring');
|
||||
var reqProcess = function (req, rep) {
|
||||
var ctx = {
|
||||
req: req,
|
||||
rep: req,
|
||||
data: { c: 0, m: '', v: '' },
|
||||
status: 200,
|
||||
setCT: false,
|
||||
contentType: 'text/json; charset=utf-8',
|
||||
};
|
||||
ctx.setValue = function (val, contentType) {
|
||||
if (contentType != null) {
|
||||
ctx.setCT = true;
|
||||
ctx.contentType = !contentType || contentType == 'html' ? 'text/html; charset=utf-8' : contentType;
|
||||
}
|
||||
ctx.data.v = val;
|
||||
};
|
||||
ctx.error = function (msg) {
|
||||
ctx.data.c = 1;
|
||||
ctx.data.m = msg;
|
||||
return ctx;
|
||||
};
|
||||
ctx.isError = function () {
|
||||
return ctx.data.c != 0;
|
||||
};
|
||||
ctx.getMsg = function () {
|
||||
return ctx.data.m;
|
||||
};
|
||||
ctx.endLog = function () {};
|
||||
|
||||
var post = Buffer.alloc(0);
|
||||
req.on('data', function (chunk) {
|
||||
post = Buffer.concat([post, chunk], post.length + chunk.length);
|
||||
});
|
||||
req.on('end', function () {
|
||||
(async function () {
|
||||
if (req.method != 'GET' && req.method != 'POST') {
|
||||
ctx.setValue('Method: ' + req.method, 'html');
|
||||
} else
|
||||
try {
|
||||
//解析url查询参数 和 post表单
|
||||
var params = querystring.parse((/\?(.+)/.exec(req.url) || [])[1] || '');
|
||||
params = Object.assign(params, querystring.parse(post.toString()));
|
||||
|
||||
var path0 = (/^\/([^\/\?]+)/.exec(req.url) || [])[1] || '';
|
||||
var fn = Api[req.method + '_' + path0];
|
||||
if (fn) {
|
||||
await fn(ctx, params);
|
||||
} else {
|
||||
ctx.status = 404;
|
||||
ctx.error(req.method + '路径不存在:' + req.url);
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.error('执行出错:' + e.stack);
|
||||
}
|
||||
if (!ctx.setCT) {
|
||||
try {
|
||||
var sendData = JSON.stringify(ctx.data);
|
||||
} catch (e) {
|
||||
sendData = JSON.stringify({ c: 1, m: '返回数据失败:' + e.stack });
|
||||
}
|
||||
} else {
|
||||
var sendData = ctx.data.v;
|
||||
}
|
||||
|
||||
rep.writeHead(ctx.status, {
|
||||
'Content-Type': ctx.contentType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
});
|
||||
rep.end(sendData);
|
||||
|
||||
console.log(
|
||||
'[' +
|
||||
new Date().toLocaleString() +
|
||||
']:' +
|
||||
Port +
|
||||
' ' +
|
||||
req.method +
|
||||
' ' +
|
||||
ctx.status +
|
||||
' ' +
|
||||
Buffer.byteLength(sendData) +
|
||||
' ' +
|
||||
post.length +
|
||||
' ' +
|
||||
req.url,
|
||||
);
|
||||
if (ctx.isError()) {
|
||||
console.log(' \x1B[31m' + ctx.getMsg() + '\x1B[0m');
|
||||
}
|
||||
ctx.endLog();
|
||||
})();
|
||||
});
|
||||
};
|
||||
|
||||
var UrlProtocol = 'http';
|
||||
var UrlPort = Port == 80 ? '' : ':' + Port;
|
||||
if (SSL.CertFile) {
|
||||
UrlProtocol = 'https';
|
||||
UrlPort = Port == 443 ? '' : ':' + Port;
|
||||
}
|
||||
|
||||
console.log('正在创建' + UrlProtocol + '服务(port:' + Port + ')...');
|
||||
if (SSL.CertFile) {
|
||||
https
|
||||
.createServer(
|
||||
{
|
||||
cert: fs.readFileSync(SSL.CertFile),
|
||||
key: fs.readFileSync(SSL.KeyFile),
|
||||
},
|
||||
reqProcess,
|
||||
)
|
||||
.listen(Port);
|
||||
} else {
|
||||
http.createServer(reqProcess).listen(Port);
|
||||
}
|
||||
|
||||
console.log('\x1B[32m%s\x1B[0m', UrlProtocol.toUpperCase() + '服务正在运行...');
|
||||
console.log(`
|
||||
请在语音识别测试页面填写接口地址: \x1B[33m
|
||||
${UrlProtocol}://127.0.0.1${UrlPort}/token
|
||||
${UrlProtocol}://localhost${UrlPort}/token
|
||||
${UrlProtocol}://你的局域网IP${UrlPort}/token
|
||||
${UrlProtocol}://你的域名${UrlPort}/token
|
||||
\x1B[0m
|
||||
如果你的测试页面\x1B[35m无法访问http地址\x1B[0m或\x1B[35m存在跨域问题\x1B[0m时,请在本电脑上直接访问 \x1B[33m${UrlProtocol}://127.0.0.1${UrlPort}/token?lang=你需要lang语言\x1B[0m
|
||||
然后手动复制 \x1B[33m{appkey:'...',token:'...'}\x1B[0m json到页面中使用,复制的token可以重复使用,本控制台中会显示有效期
|
||||
已配置的可用lang语言\x1B[33m${JSON.stringify(GetLangKeys())}\x1B[0m
|
||||
`);
|
199
backend/src/lib.load.cjs
Normal file
199
backend/src/lib.load.cjs
Normal file
@ -0,0 +1,199 @@
|
||||
//请求数据封装
|
||||
var LoadFn=module.exports=function(url){
|
||||
return new Load(url);
|
||||
};
|
||||
|
||||
var QueryString = require('querystring');
|
||||
var Http=require('http');
|
||||
var Https=require('https');
|
||||
|
||||
var Load=function(url){
|
||||
this.response={//响应数据
|
||||
url:"" //最终响应的url,如果发生了30x跳转为跳转后的地址
|
||||
,status:0
|
||||
,statusDesc:"" //Not Found
|
||||
,headers:[] //["k: v",...] k大小写未知
|
||||
,data:null //string或buffer
|
||||
};
|
||||
|
||||
this.params={};//表单参数 GET或POST表单
|
||||
this.rawPost=null;//POST数据 string或buffer
|
||||
this.headers={};
|
||||
this.contentType="application/x-www-form-urlencoded";
|
||||
this.timeout=15019;//默认GET的,POST为30000
|
||||
this.redirect=true;//是否允许重定向
|
||||
this.debug=false;//调试模式,会忽略证书的校验
|
||||
|
||||
this._url=url;
|
||||
var m=/((\w+:)?\/\/([^\/:]+)(\:\d+)?)(.*)/.exec(url)||[];
|
||||
this._url_left=m[1];
|
||||
this._url_protocol=(m[2]||"http:").toLowerCase();
|
||||
this._url_isHttps=this._url_protocol=="https:";
|
||||
this._url_host=m[3]||"";
|
||||
this._url_port=+(m[4]||":"+(this._url_isHttps?443:80)).substr(1);
|
||||
this._url_pq=m[5]||"";
|
||||
};
|
||||
Load.prototype={
|
||||
|
||||
_redirect:async function(url,checkSuccessStatus,method,encode){
|
||||
if(/(^https?:\/\/)|(^\/)/i.test(url)){
|
||||
if(RegExp.$2){
|
||||
url=this._url_left+url;
|
||||
};
|
||||
}else{
|
||||
throw new Error("未支持的重定向地址格式:"+url);
|
||||
};
|
||||
|
||||
var redirectCount=this.redirectCount||0;
|
||||
if(redirectCount>=10){
|
||||
throw new Error("重定向超过次数");
|
||||
};
|
||||
var load=new Load(url);
|
||||
|
||||
load.params=this.params;
|
||||
load.rawPost=this.rawPost;
|
||||
load.headers=this.headers;
|
||||
load.contentType=this.contentType;
|
||||
load.timeout=this.timeout;
|
||||
load.redirect=this.redirect;
|
||||
load.debug=this.debug;
|
||||
|
||||
|
||||
load.redirectCount=redirectCount+1;
|
||||
await load.send(checkSuccessStatus,method,encode);
|
||||
return load;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
,set:function(k,v){
|
||||
this[k]=v;
|
||||
return this;
|
||||
}
|
||||
|
||||
,send:async function(checkSuccessStatus,method,encode){
|
||||
var This=this;
|
||||
return await new Promise(function(resolve, reject){
|
||||
var isGET=method=="GET";
|
||||
var url=This._url_pq;
|
||||
var params=QueryString.stringify(This.params);
|
||||
if(isGET && params){
|
||||
if(url.indexOf("?")+1){
|
||||
url+="&"+params;
|
||||
}else{
|
||||
url+="?"+params;
|
||||
};
|
||||
};
|
||||
if(!isGET){
|
||||
This.headers["Content-Length"]=Buffer.byteLength(This.rawPost||params);
|
||||
if(!This.headers["Content-Type"]){
|
||||
This.headers["Content-Type"]=This.contentType;
|
||||
};
|
||||
};
|
||||
|
||||
var scope=This._url_isHttps?Https:Http;
|
||||
var obj={
|
||||
method:method
|
||||
,headers:This.headers
|
||||
,timeout:This.timeout!=15019?This.timeout:isGET?15000:30000
|
||||
,rejectUnauthorized:This.debug?false:true
|
||||
|
||||
,protocol:This._url_protocol
|
||||
,host:This._url_host
|
||||
,port:This._url_port
|
||||
,path:url
|
||||
};
|
||||
var req=scope.request(obj,function(resp){
|
||||
var status=resp.statusCode;
|
||||
var headers=[],location="";
|
||||
var arr=resp.rawHeaders;
|
||||
for(var i=0;i<arr.length;i+=2){
|
||||
var k=arr[i], v=arr[i+1];
|
||||
headers.push(k+": "+v);
|
||||
|
||||
if(k.toLowerCase()=="location"){
|
||||
location=v;
|
||||
};
|
||||
};
|
||||
This.response={
|
||||
url:This._url
|
||||
,status:status
|
||||
,statusDesc:resp.statusMessage
|
||||
,headers:headers
|
||||
};
|
||||
if((status==301||status==302) && This.redirect){
|
||||
This._redirect(location,checkSuccessStatus,method,encode)
|
||||
.then(function(o){
|
||||
This.response=o.response;
|
||||
resolve(This.response.data);
|
||||
},reject);
|
||||
return;
|
||||
};
|
||||
|
||||
var buffers=[];
|
||||
resp.on('data',function(buffer){
|
||||
buffers.push(buffer);
|
||||
});
|
||||
resp.on('end',function(){
|
||||
if(checkSuccessStatus!==false){
|
||||
if(!( status>=200 && status<300 )){
|
||||
reject(new Error("请求失败["+status+"]"));
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
This.response.data=Buffer.concat(buffers);
|
||||
if(encode!=null){
|
||||
var enc=(encode=="json"?"":encode)||'utf8';
|
||||
var val=This.response.data.toString(enc);
|
||||
if(encode=="json"){
|
||||
try{
|
||||
val=JSON.parse(val);
|
||||
}catch(e){
|
||||
reject(new Error("请求结果解析失败"));
|
||||
return;
|
||||
};
|
||||
};
|
||||
This.response.data=val;
|
||||
};
|
||||
|
||||
resolve(This.response.data);
|
||||
});
|
||||
});
|
||||
req.on("error",function(e){
|
||||
reject(new Error("请求错误:"+e.message));
|
||||
});
|
||||
|
||||
if(!isGET){
|
||||
if(This.rawPost){
|
||||
req.write(This.rawPost);
|
||||
}else if(params){
|
||||
req.write(params);
|
||||
};
|
||||
};
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
,getBytes:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"GET");
|
||||
}
|
||||
,getString:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"GET","");
|
||||
}
|
||||
,getJson:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"GET","json");
|
||||
}
|
||||
|
||||
,postBytes:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"POST");
|
||||
}
|
||||
,postString:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"POST","");
|
||||
}
|
||||
,postJson:async function(checkSuccessStatus){
|
||||
return await this.send(checkSuccessStatus,"POST","json");
|
||||
}
|
||||
|
||||
};
|
@ -6,6 +6,11 @@
|
||||
<link rel="icon" type="image/png" href="https://kevisual.xiongxiao.me/root/center/panda.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<script src="./recorder.mp3.min.js"></script>
|
||||
<!-- <script src="./recorder.wav.min.js"></script> -->
|
||||
|
||||
<script src="./extensions/asr.aliyun.short.js"></script>
|
||||
<script src="./extensions/waveview.js"></script>
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<style>
|
||||
html,
|
||||
|
@ -36,13 +36,13 @@
|
||||
"react-router": "^7.5.0",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"recorder-core": "^1.3.25011100",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/components": "workspace:*",
|
||||
"@kevisual/query": "0.0.17",
|
||||
"@kevisual/types": "^0.0.6",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
|
327
pnpm-lock.yaml
generated
327
pnpm-lock.yaml
generated
@ -53,13 +53,13 @@ importers:
|
||||
react-toastify:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
recorder-core:
|
||||
specifier: ^1.3.25011100
|
||||
version: 1.3.25011100
|
||||
zustand:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(@types/react@19.1.0)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.2.2(react@19.1.0))
|
||||
devDependencies:
|
||||
'@kevisual/components':
|
||||
specifier: workspace:*
|
||||
version: link:packages/components
|
||||
'@kevisual/query':
|
||||
specifier: 0.0.17
|
||||
version: 0.0.17(ws@8.18.1)
|
||||
@ -100,46 +100,6 @@ importers:
|
||||
specifier: ^6.2.5
|
||||
version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.5.1)
|
||||
|
||||
packages/components:
|
||||
dependencies:
|
||||
'@emotion/react':
|
||||
specifier: ^11.14.0
|
||||
version: 11.14.0(@types/react@19.1.0)(react@19.1.0)
|
||||
'@emotion/styled':
|
||||
specifier: ^11.14.0
|
||||
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
|
||||
'@kevisual/query-login':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4(@kevisual/query@0.0.17(ws@8.18.1))(rollup@4.34.8)(typescript@5.8.3)
|
||||
'@mui/material':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
re-resizable:
|
||||
specifier: ^6.11.2
|
||||
version: 6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0
|
||||
react-dom:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
react-draggable:
|
||||
specifier: ^4.4.6
|
||||
version: 4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.55.0
|
||||
version: 7.55.0(react@19.1.0)
|
||||
react-i18next:
|
||||
specifier: ^15.4.1
|
||||
version: 15.4.1(i18next@24.2.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
devDependencies:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
tailwind-merge:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
|
||||
packages:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@ -468,14 +428,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@kevisual/cache@0.0.1':
|
||||
resolution: {integrity: sha512-yjQJ47NdE3smtJahA3UMcEEBU86uI3V93WnQZHTgFP1S1L8iD0Abct1cFWkuPIlsow8uBxbn4z4iN58KrsQlpA==}
|
||||
|
||||
'@kevisual/query-login@0.0.4':
|
||||
resolution: {integrity: sha512-ibdSkMsoWYYvM9l5YqWbxVvNb+uTqLyfeS0wJqLumPyYFx3mSwFweI+isbtJQqpP/G3CywsXYrrbZbelSw124Q==}
|
||||
peerDependencies:
|
||||
'@kevisual/query': ^0.0.15
|
||||
|
||||
'@kevisual/query@0.0.17':
|
||||
resolution: {integrity: sha512-WMvWM+3pNlPKNhoxPX9fldMp1tOeJrkRM/tXA4bvOnftIoX2yeI4v0wTpbGJXES/bLlo7OC2kV8SeKF0K6dnxQ==}
|
||||
|
||||
@ -568,46 +520,6 @@ packages:
|
||||
'@popperjs/core@2.11.8':
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.3':
|
||||
resolution: {integrity: sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==}
|
||||
engines: {node: '>=16.0.0 || 14 >= 14.17'}
|
||||
peerDependencies:
|
||||
rollup: ^2.68.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/plugin-node-resolve@16.0.1':
|
||||
resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^2.78.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/plugin-typescript@12.1.2':
|
||||
resolution: {integrity: sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^2.14.0||^3.0.0||^4.0.0
|
||||
tslib: '*'
|
||||
typescript: '>=3.7.0'
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
tslib:
|
||||
optional: true
|
||||
|
||||
'@rollup/pluginutils@5.1.4':
|
||||
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.34.8':
|
||||
resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==}
|
||||
cpu: [arm]
|
||||
@ -830,9 +742,6 @@ packages:
|
||||
'@types/react@19.1.0':
|
||||
resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
'@vitejs/plugin-basic-ssl@2.0.0':
|
||||
resolution: {integrity: sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@ -876,10 +785,6 @@ packages:
|
||||
caniuse-lite@1.0.30001684:
|
||||
resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==}
|
||||
|
||||
clsx@1.2.1:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -892,9 +797,6 @@ packages:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
|
||||
convert-source-map@1.9.0:
|
||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||
|
||||
@ -924,10 +826,6 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@ -939,10 +837,6 @@ packages:
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
dotenv@16.4.7:
|
||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -986,21 +880,10 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fdir@6.4.3:
|
||||
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
find-root@1.1.0:
|
||||
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
|
||||
|
||||
@ -1061,23 +944,9 @@ packages:
|
||||
hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
i18next@24.2.3:
|
||||
resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
idb-keyval@6.2.1:
|
||||
resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==}
|
||||
|
||||
immer@10.1.1:
|
||||
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||
|
||||
@ -1096,12 +965,6 @@ packages:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-module@1.0.0:
|
||||
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
|
||||
|
||||
is-reference@1.2.1:
|
||||
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
|
||||
|
||||
jiti@2.4.2:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
@ -1204,9 +1067,6 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1293,10 +1153,6 @@ packages:
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@4.0.2:
|
||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postcss@8.5.3:
|
||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@ -1304,42 +1160,17 @@ packages:
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
re-resizable@6.11.2:
|
||||
resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==}
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-dom@19.1.0:
|
||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||
peerDependencies:
|
||||
react: ^19.1.0
|
||||
|
||||
react-draggable@4.4.6:
|
||||
resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==}
|
||||
peerDependencies:
|
||||
react: '>= 16.3.0'
|
||||
react-dom: '>= 16.3.0'
|
||||
|
||||
react-hook-form@7.55.0:
|
||||
resolution: {integrity: sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-i18next@15.4.1:
|
||||
resolution: {integrity: sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@ -1383,6 +1214,9 @@ packages:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
recorder-core@1.3.25011100:
|
||||
resolution: {integrity: sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA==}
|
||||
|
||||
regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
|
||||
@ -1395,13 +1229,6 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-dts@6.2.1:
|
||||
resolution: {integrity: sha512-sR3CxYUl7i2CHa0O7bA45mCrgADyAQ0tVtGSqi3yvH28M+eg1+g5d7kQ9hLvEz5dorK3XVsH5L2jwHLQf72DzA==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
rollup: ^3.29.4 || ^4
|
||||
typescript: ^4.5 || ^5.0
|
||||
|
||||
rollup@4.34.8:
|
||||
resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@ -1436,9 +1263,6 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
tailwind-merge@3.2.0:
|
||||
resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
|
||||
|
||||
tailwindcss@4.1.3:
|
||||
resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==}
|
||||
|
||||
@ -1518,10 +1342,6 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
@ -1884,28 +1704,6 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@kevisual/cache@0.0.1(rollup@4.34.8)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 28.0.3(rollup@4.34.8)
|
||||
'@rollup/plugin-node-resolve': 16.0.1(rollup@4.34.8)
|
||||
'@rollup/plugin-typescript': 12.1.2(rollup@4.34.8)(typescript@5.8.3)
|
||||
idb-keyval: 6.2.1
|
||||
rollup-plugin-dts: 6.2.1(rollup@4.34.8)(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- tslib
|
||||
- typescript
|
||||
|
||||
'@kevisual/query-login@0.0.4(@kevisual/query@0.0.17(ws@8.18.1))(rollup@4.34.8)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@kevisual/cache': 0.0.1(rollup@4.34.8)(typescript@5.8.3)
|
||||
'@kevisual/query': 0.0.17(ws@8.18.1)
|
||||
dotenv: 16.4.7
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- tslib
|
||||
- typescript
|
||||
|
||||
'@kevisual/query@0.0.17(ws@8.18.1)':
|
||||
dependencies:
|
||||
openai: 4.89.0(ws@8.18.1)
|
||||
@ -2006,44 +1804,6 @@ snapshots:
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.3(rollup@4.34.8)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.34.8)
|
||||
commondir: 1.0.1
|
||||
estree-walker: 2.0.2
|
||||
fdir: 6.4.3(picomatch@4.0.2)
|
||||
is-reference: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
picomatch: 4.0.2
|
||||
optionalDependencies:
|
||||
rollup: 4.34.8
|
||||
|
||||
'@rollup/plugin-node-resolve@16.0.1(rollup@4.34.8)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.34.8)
|
||||
'@types/resolve': 1.20.2
|
||||
deepmerge: 4.3.1
|
||||
is-module: 1.0.0
|
||||
resolve: 1.22.10
|
||||
optionalDependencies:
|
||||
rollup: 4.34.8
|
||||
|
||||
'@rollup/plugin-typescript@12.1.2(rollup@4.34.8)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.34.8)
|
||||
resolve: 1.22.10
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
rollup: 4.34.8
|
||||
|
||||
'@rollup/pluginutils@5.1.4(rollup@4.34.8)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 4.0.2
|
||||
optionalDependencies:
|
||||
rollup: 4.34.8
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.34.8':
|
||||
optional: true
|
||||
|
||||
@ -2220,8 +1980,6 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
'@vitejs/plugin-basic-ssl@2.0.0(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.5.1))':
|
||||
dependencies:
|
||||
vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.5.1)
|
||||
@ -2269,8 +2027,6 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001684: {}
|
||||
|
||||
clsx@1.2.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
@ -2279,8 +2035,6 @@ snapshots:
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commondir@1.0.1: {}
|
||||
|
||||
convert-source-map@1.9.0: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
@ -2303,8 +2057,6 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
@ -2314,8 +2066,6 @@ snapshots:
|
||||
'@babel/runtime': 7.27.0
|
||||
csstype: 3.1.3
|
||||
|
||||
dotenv@16.4.7: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@ -2380,14 +2130,8 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
fdir@6.4.3(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
find-root@1.1.0: {}
|
||||
|
||||
form-data-encoder@1.7.2: {}
|
||||
@ -2449,22 +2193,10 @@ snapshots:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
i18next@24.2.3(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
idb-keyval@6.2.1: {}
|
||||
|
||||
immer@10.1.1:
|
||||
optional: true
|
||||
|
||||
@ -2481,12 +2213,6 @@ snapshots:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
is-module@1.0.0: {}
|
||||
|
||||
is-reference@1.2.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@ -2558,10 +2284,6 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
@ -2623,8 +2345,6 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.2: {}
|
||||
|
||||
postcss@8.5.3:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
@ -2637,36 +2357,15 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
re-resizable@6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-dom@19.1.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
scheduler: 0.26.0
|
||||
|
||||
react-draggable@4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
clsx: 1.2.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-hook-form@7.55.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
react-i18next@15.4.1(i18next@24.2.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 24.2.3(typescript@5.8.3)
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@19.1.0: {}
|
||||
@ -2706,6 +2405,8 @@ snapshots:
|
||||
|
||||
react@19.1.0: {}
|
||||
|
||||
recorder-core@1.3.25011100: {}
|
||||
|
||||
regenerator-runtime@0.14.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@ -2716,14 +2417,6 @@ snapshots:
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
rollup-plugin-dts@6.2.1(rollup@4.34.8)(typescript@5.8.3):
|
||||
dependencies:
|
||||
magic-string: 0.30.17
|
||||
rollup: 4.34.8
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
|
||||
rollup@4.34.8:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
@ -2768,8 +2461,6 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
tailwind-merge@3.2.0: {}
|
||||
|
||||
tailwindcss@4.1.3: {}
|
||||
|
||||
tapable@2.2.1: {}
|
||||
@ -2809,8 +2500,6 @@ snapshots:
|
||||
lightningcss: 1.29.2
|
||||
yaml: 2.5.1
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
910
public/extensions/asr.aliyun.short.js
Normal file
910
public/extensions/asr.aliyun.short.js
Normal file
@ -0,0 +1,910 @@
|
||||
/*
|
||||
录音 Recorder扩展,ASR,阿里云语音识别(语音转文字),支持实时语音识别、单个音频文件转文字
|
||||
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
- 本扩展通过调用 阿里云-智能语音交互-一句话识别 接口来进行语音识别,无时长限制。
|
||||
- 识别过程中采用WebSocket直连阿里云,语音数据无需经过自己服务器。
|
||||
- 自己服务器仅需提供一个Token生成接口即可(本库已实现一个本地测试NodeJs后端程序 /assets/demo-asr/NodeJsServer_asr.aliyun.short.js)。
|
||||
|
||||
本扩展单次语音识别时虽长无限制,最佳使用场景还是1-5分钟内的语音识别;60分钟以上的语音识别本扩展也能胜任(需自行进行重试容错处理),但太长的识别场景不太适合使用阿里云一句话识别(阿里云单次一句话识别最长60秒,本扩展自带拼接过程,所以无时长限制);为什么采用一句话识别:因为便宜。
|
||||
|
||||
|
||||
【对接流程】
|
||||
1. 到阿里云开通 一句话识别 服务(可试用一段时间,正式使用时应当开通商用版,很便宜),得到AccessKey、Secret,参考:https://help.aliyun.com/document_detail/324194.html ;
|
||||
2. 到阿里云智能语音交互控制台创建相应的语音识别项目,并配置好项目,得到Appkey,每个项目可以设置一种语言模型,要支持多种语言就创建多个项目;
|
||||
3. 需要后端提供一个Token生成接口(用到上面的Key和Secret),可直接参考或本地运行此NodeJs后端测试程序:/assets/demo-asr/NodeJsServer_asr.aliyun.short.js,配置好代码里的阿里云账号后,在目录内直接命令行执行`node NodeJsServer_asr.aliyun.short.js`即可运行提供本地测试接口;
|
||||
4. 前端调用ASR_Aliyun_Short,传入tokenApi,即可很简单的实现语音识别功能;
|
||||
|
||||
在线测试例子:
|
||||
https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.asr.aliyun.short
|
||||
调用示例:
|
||||
var rec=Recorder(recSet);rec.open(...) //进行语音识别前,先打开录音,获得录音权限
|
||||
|
||||
var asr=Recorder.ASR_Aliyun_Short(set); //创建asr对象,参数详情请参考下面的源码
|
||||
|
||||
//asr创建好后,随时调用strat,开始进行语音识别
|
||||
asr.start(function(){
|
||||
rec.start();//一般在start成功之后,调用rec.start()开始录音,此时可以通知用户讲话了
|
||||
},fail);
|
||||
|
||||
//实时处理输入音频数据,一般是在rec.set.onProcess中调用本方法,输入实时录制的音频数据,输入的数据将会发送语音识别;不管有没有start,都可以调用本方法,start前输入的数据会缓冲起来等到start后进行识别
|
||||
asr.input([[Int16,...],...],48000,0);
|
||||
|
||||
//话讲完后,调用stop结束语音识别,得到识别到的内容文本
|
||||
asr.stop(function(text,abortMsg){
|
||||
//text为识别到的最终完整内容;如果存在abortMsg代表识别中途被某种错误停止了,text是停止前的内容识别到的完整内容,一般早在asrProcess中会收到abort事件然后要停止录音
|
||||
},fail);
|
||||
|
||||
更多的方法:
|
||||
asr.inputDuration() 获取input已输入的音频数据总时长,单位ms
|
||||
asr.sendDuration() 获取已发送识别的音频数据总时长,存在重发重叠部分,因此比inputDuration长
|
||||
asr.asrDuration() 获取已识别的音频数据总时长,去除了sendDuration的重叠部分,值<=inputDuration
|
||||
asr.getText() 获取实时结果文本,如果已stop返回的就是最终文本,一般无需调用此方法,因为回调中都提供了此方法的返回值
|
||||
|
||||
//一次性将单个完整音频Blob文件转成文字,无需start、stop,创建好asr后直接调用本方法即可
|
||||
asr.audioToText(audioBlob,success,fail)
|
||||
//一次性的将单个完整PCM音频数据转成文字,无需start、stop,创建好asr后直接调用本方法即可
|
||||
asr.pcmToText(buffer,sampleRate,success,fail)
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
var ASR_Aliyun_Short=function(set){
|
||||
return new fn(set);
|
||||
};
|
||||
var ASR_Aliyun_ShortTxt="ASR_Aliyun_Short";
|
||||
var fn=function(set){
|
||||
var This=this;
|
||||
var o={
|
||||
tokenApi:"" /*必填,调用阿里云一句话识别需要的token获取api地址
|
||||
接口实现请参考本地测试NodeJs后端程序:/assets/demo-asr/NodeJsServer_asr.aliyun.short.js
|
||||
此接口默认需要返回数据格式:
|
||||
{
|
||||
c:0 //code,0接口调用正常,其他数值接口调用出错
|
||||
,m:"" //message,接口调用出错时的错误消息
|
||||
,v:{ //value,接口成功调用返回的结果【结果中必须包含下面两个值】
|
||||
appkey:"aaaa" //lang语言模型对应的项目appkey
|
||||
,token:"bbbb" //语音识别Access Token
|
||||
}
|
||||
}
|
||||
如果不是返回的这个格式的数据,必须提供apiRequest配置,自行请求api*/
|
||||
,apiArgs:{ //请求tokenApi时要传的参数
|
||||
action:"token"
|
||||
,lang:"普通话" //语言模型设置(具体取值取决于tokenApi支持了哪些语言)
|
||||
}
|
||||
,apiRequest:null /*tokenApi的请求实现方法,默认使用简单的ajax实现
|
||||
如果你接口返回的数据格式和默认格式不一致,必须提供一个函数来自行请求api
|
||||
方法参数:fn(url,args,success,fail)
|
||||
url:"" == tokenApi
|
||||
args:{} == apiArgs
|
||||
success:fn(value) 接口调用成功回调,value={appkey:"", token:""}
|
||||
fail:fn(errMsg) 接口调用出错回调,errMsg="错误消息"
|
||||
*/
|
||||
,compatibleWebSocket:null /*提供一个函数返回兼容WebSocket的对象,一般也需要提供apiRequest
|
||||
如果你使用的环境不支持WebSocket,需要提供一个函数来返回一个兼容实现对象
|
||||
方法参数:fn(url) url为连接地址,返回一个对象,需支持的回调和方法:{
|
||||
onopen:fn() 连接成功回调
|
||||
onerror:fn({message}) 连接失败回调
|
||||
onclose:fn({code, reason}) 连接关闭回调
|
||||
onmessage:fn({data}) 收到消息回调
|
||||
connect:fn() 进行连接
|
||||
close:fn(code,reason) 关闭连接
|
||||
send:fn(data) 发送数据,data为字符串或者arraybuffer
|
||||
}
|
||||
binaryType固定使用arraybuffer类型
|
||||
*/
|
||||
|
||||
//,asrProcess:null //fn(text,nextDuration,abortMsg) 当实时接收到语音识别结果时的回调函数(对单个完整音频文件的识别也有效)
|
||||
//此方法需要返回true才会继续识别,否则立即当做识别超时处理,你应当通过nextDuration来决定是否继续识别,避免无限制的识别大量消耗阿里云资源额度;如果不提供本回调,默认1分钟超时后终止识别(因为没有绑定回调,你不知道已经被终止了)
|
||||
//text为中间识别到的内容(并非已有录音片段的最终结果,后续可能会根据语境修整)
|
||||
//nextDuration 为当前回调时下次即将进行识别的总时长,单位毫秒,通过这个参数来限制识别总时长,超过时长就返回false终止识别(第二分钟开始每分钟会多识别前一分钟结尾的5秒数据,用于两分钟之间的拼接,相当于第二分钟最多识别55秒的新内容)
|
||||
//abortMsg如不为空代表识别中途因为某种原因终止了识别(比如超时、接口调用失败),收到此信息时应当立即调用asr的stop方法得到最终结果,并且终止录音
|
||||
|
||||
,log:NOOP //fn(msg,color)提供一个日志输出接口,默认只会输出到控制台,color: 1:红色,2绿色,不为空时为颜色字符串
|
||||
|
||||
//高级选项
|
||||
,fileSpeed:6 //单个文件识别发送速度控制,取值1-n;1:为按播放速率发送,最慢,识别精度完美;6:按六倍播放速度发送,花10秒识别60秒文件比较快,精度还行;再快测试发现似乎会缺失内容,可能是发送太快底层识别不过来导致返回的结果缺失。
|
||||
};
|
||||
for(var k in set){
|
||||
o[k]=set[k];
|
||||
};
|
||||
This.set=set=o;
|
||||
This.state=0;//0 未start,1 start,2 stop
|
||||
This.started=0;
|
||||
|
||||
This.sampleRate=16000;//发送的采样率
|
||||
//This.tokenData
|
||||
|
||||
This.pcmBuffers=[];//等待发送的缓冲数据
|
||||
This.pcmTotal=0;//输入的总量
|
||||
This.pcmOffset=0;//缓冲[0]的已发送位置
|
||||
This.pcmSend=0;//发送的总量,不会重复计算重发的量
|
||||
|
||||
This.joinBuffers=[];//下一分钟左移5秒,和上一分钟重叠5秒
|
||||
This.joinSize=0;//左移的数据量
|
||||
This.joinSend=0;//单次已发送量
|
||||
This.joinOffset=-1;//左移[0]的已发送位置,-1代表可以进行整理buffers
|
||||
This.joinIsOpen=0;//是否开始发送
|
||||
This.joinSendTotal=0;//已发送重叠的总量
|
||||
|
||||
This.sendCurSize=0;//单个wss发送量,不能超过1分钟的量
|
||||
This.sendTotal=0;//总计的发送量,存在重发重叠部分
|
||||
|
||||
//This.stopWait=null
|
||||
//This.sendWait=0
|
||||
//This.sendAbort=false
|
||||
//This.sendAbortMsg=""
|
||||
|
||||
//This.wsCur 当前的wss
|
||||
//This.wsLock 新的一分钟wss准备
|
||||
This.resTxts=[];//每分钟结果列表 resTxt object: {tempTxt:"efg",okTxt:"efgh",fullTxt:"abcdefgh"}
|
||||
|
||||
if(!set.asrProcess){
|
||||
This.log("未绑定asrProcess回调无法感知到abort事件",3);
|
||||
};
|
||||
};
|
||||
var CLog=function(){
|
||||
var v=arguments; v[0]="["+ASR_Aliyun_ShortTxt+"]"+v[0];
|
||||
Recorder.CLog.apply(null,v);
|
||||
};
|
||||
fn.prototype=ASR_Aliyun_Short.prototype={
|
||||
log:function(msg,color){
|
||||
CLog(msg,typeof color=="number"?color:0);
|
||||
this.set.log("["+ASR_Aliyun_ShortTxt+"]"+msg,color==3?"#f60":color);
|
||||
}
|
||||
|
||||
|
||||
//input已输入的音频数据总时长
|
||||
,inputDuration:function(){
|
||||
return Math.round(this.pcmTotal/this.sampleRate*1000);
|
||||
}
|
||||
//已发送识别的音频数据总时长,存在重发重叠部分,因此比inputDuration长
|
||||
,sendDuration:function(add){
|
||||
var size=this.sendTotal;
|
||||
size+=add||0;
|
||||
return Math.round(size/this.sampleRate*1000);
|
||||
}
|
||||
//已识别的音频数据总时长,去除了sendDuration的重叠部分,值<=inputDuration
|
||||
,asrDuration:function(){
|
||||
return this.sendDuration(-this.joinSendTotal);
|
||||
}
|
||||
|
||||
|
||||
/**一次性将单个完整音频文件转成文字,支持的文件类型由具体的浏览器决定,因此存在兼容性问题,兼容性mp3最好,wav次之,其他格式不一定能够解码。实际就是调用:浏览器解码音频得到PCM -> start -> input ... input -> stop
|
||||
blob:Blob 音频文件Blob对象,如:rec.stop得到的录音结果、file input选择的文件、XMLHttpRequest的blob结果、new Blob([TypedArray])创建的blob
|
||||
success fn(text,abortMsg) text为识别到的完整内容,abortMsg参考stop
|
||||
fail:fn(errMsg)
|
||||
**/
|
||||
,audioToText:function(blob,success,fail){
|
||||
var This=this;
|
||||
var failCall=function(err){
|
||||
This.log(err,1);
|
||||
fail&&fail(err);
|
||||
};
|
||||
if(!Recorder.GetContext()){//强制激活Recorder.Ctx 不支持大概率也不支持解码
|
||||
failCall("浏览器不支持音频解码");
|
||||
return;
|
||||
};
|
||||
|
||||
var reader=new FileReader();
|
||||
reader.onloadend=function(){
|
||||
var ctx=Recorder.Ctx;
|
||||
ctx.decodeAudioData(reader.result,function(raw){
|
||||
var src=raw.getChannelData(0);
|
||||
var sampleRate=raw.sampleRate;
|
||||
|
||||
var pcm=new Int16Array(src.length);
|
||||
for(var i=0;i<src.length;i++){//floatTo16BitPCM
|
||||
var s=Math.max(-1,Math.min(1,src[i]));
|
||||
s=s<0?s*0x8000:s*0x7FFF;
|
||||
pcm[i]=s;
|
||||
};
|
||||
|
||||
This.pcmToText(pcm,sampleRate,success,fail);
|
||||
},function(e){
|
||||
failCall("音频解码失败["+blob.type+"]:"+e.message);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
/**一次性的将单个完整音频转成文字。实际就是调用:start -> input ... input -> stop
|
||||
buffer:[Int16,...] 16位单声道音频pcm数据,一维数组
|
||||
sampleRate pcm的采样率
|
||||
success fn(text,abortMsg) text为识别到的完整内容,abortMsg参考stop
|
||||
fail:fn(errMsg)
|
||||
**/
|
||||
,pcmToText:function(buffer,sampleRate,success,fail){
|
||||
var This=this;
|
||||
This.start(function(){
|
||||
This.log("单个文件"+Math.round(buffer.length/sampleRate*1000)+"ms转文字");
|
||||
This.sendSpeed=This.set.fileSpeed;
|
||||
This.input([buffer],sampleRate);
|
||||
This.stop(success,fail);
|
||||
},fail);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**开始识别,开始后需要调用input输入录音数据,结束时调用stop来停止识别。如果start之前调用了input输入数据,这些数据将会等到start成功之后进行识别。
|
||||
建议在success回调中开始录音(即rec.start);当然asr.start和rec.start同时进行调用,或者任意一个先调用都是允许的,不过当出现fail时,需要处理好asr和rec各自的状态。
|
||||
无需特殊处理start和stop的关系,只要调用了stop,会阻止未完成的start,不会执行回调。
|
||||
success:fn()
|
||||
fail:fn(errMsg)
|
||||
**/
|
||||
,start:function(success,fail){
|
||||
var This=this,set=This.set;
|
||||
var failCall=function(err){
|
||||
This.sendAbortMsg=err;
|
||||
fail&&fail(err);
|
||||
};
|
||||
if(!set.compatibleWebSocket){
|
||||
if(!isBrowser){
|
||||
failCall("非浏览器环境,请提供compatibleWebSocket配置来返回一个兼容的WebSocket");
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
if(This.state!=0){
|
||||
failCall("ASR对象不可重复start");
|
||||
return;
|
||||
};
|
||||
This.state=1;
|
||||
|
||||
var stopCancel=function(){
|
||||
This.log("ASR start被stop中断",1);
|
||||
This._send();//调用了再说,不管什么状态
|
||||
};
|
||||
This._token(function(){
|
||||
if(This.state!=1){
|
||||
stopCancel();
|
||||
}else{
|
||||
This.log("OK start",2);
|
||||
This.started=1;
|
||||
success&&success();
|
||||
|
||||
This._send();//调用了再说,不管什么状态
|
||||
};
|
||||
},function(err){
|
||||
err="语音识别token接口出错:"+err;
|
||||
This.log(err,1);
|
||||
if(This.state!=1){
|
||||
stopCancel();
|
||||
}else{
|
||||
failCall(err);
|
||||
This._send();//调用了再说,不管什么状态
|
||||
};
|
||||
});
|
||||
}
|
||||
/**结束识别,一般在调用了本方法后,下一行代码立即调用录音rec.stop结束录音
|
||||
success:fn(text,abortMsg) text为识别到的最终完整内容;如果存在abortMsg代表识别中途被某种错误停止了,text是停止前的内容识别到的完整内容,一般早在asrProcess中会收到abort事件然后要停止录音
|
||||
fail:fn(errMsg)
|
||||
**/
|
||||
,stop:function(success,fail){
|
||||
success=success||NOOP;
|
||||
fail=fail||NOOP;
|
||||
var This=this;
|
||||
var failCall=function(err){
|
||||
err="语音识别stop出错:"+err;
|
||||
This.log(err,1);
|
||||
fail(err);
|
||||
};
|
||||
|
||||
if(This.state==2){
|
||||
failCall("ASR对象不可重复stop");
|
||||
return;
|
||||
};
|
||||
This.state=2;
|
||||
|
||||
This.stopWait=function(){
|
||||
This.stopWait=null;
|
||||
if(!This.started){
|
||||
fail(This.sendAbortMsg||"未开始语音识别");
|
||||
return;
|
||||
};
|
||||
var txt=This.getText();
|
||||
if(!txt && This.sendAbortMsg){
|
||||
fail(This.sendAbortMsg);//仅没有内容时,才走异常
|
||||
}else{
|
||||
success(txt, This.sendAbortMsg||"");//尽力返回已有内容
|
||||
};
|
||||
};
|
||||
//等待数据发送完
|
||||
This._send();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**实时处理输入音频数据;不管有没有start,都可以调用本方法,start前输入的数据会缓冲起来等到start后进行识别
|
||||
buffers:[[Int16...],...] pcm片段列表,为二维数组,第一维数组内存放1个或多个pcm数据;比如可以是:rec.buffers、onProcess中的buffers截取的一段新二维数组
|
||||
sampleRate:48000 buffers中pcm的采样率
|
||||
|
||||
buffersOffset:0 可选,默认0,从buffers第一维的这个位置开始识别,方便rec的onProcess中使用
|
||||
**/
|
||||
,input:function(buffers,sampleRate ,buffersOffset){
|
||||
var This=this;
|
||||
|
||||
if(This.state==2){//已停止,停止输入数据
|
||||
This._send();
|
||||
return;
|
||||
};
|
||||
var msg="input输入的采样率低于"+This.sampleRate;
|
||||
if(sampleRate<This.sampleRate){
|
||||
CLog(msg+",数据已丢弃",3);
|
||||
if(!This.pcmTotal){
|
||||
This.sendAbortMsg=msg;
|
||||
};
|
||||
This._send();
|
||||
return;
|
||||
};
|
||||
if(This.sendAbortMsg==msg){
|
||||
This.sendAbortMsg="";
|
||||
};
|
||||
|
||||
if(buffersOffset){
|
||||
var newBuffers=[];
|
||||
for(var idx=buffersOffset;idx<buffers.length;idx++){
|
||||
newBuffers.push(buffers[idx]);
|
||||
};
|
||||
buffers=newBuffers;
|
||||
};
|
||||
|
||||
var pcm=Recorder.SampleData(buffers,sampleRate,This.sampleRate).data;
|
||||
This.pcmTotal+=pcm.length;
|
||||
This.pcmBuffers.push(pcm);
|
||||
This._send();
|
||||
}
|
||||
,_send:function(){
|
||||
var This=this,set=This.set;
|
||||
if(This.sendWait){
|
||||
//阻塞中
|
||||
return;
|
||||
};
|
||||
var tryStopEnd=function(){
|
||||
This.stopWait&&This.stopWait();
|
||||
};
|
||||
if(This.state==2 && (!This.started || !This.stopWait)){
|
||||
//已经stop了,并且未ok开始 或者 未在等待结果
|
||||
tryStopEnd();
|
||||
return;
|
||||
};
|
||||
if(This.sendAbort){
|
||||
//已异常中断了
|
||||
tryStopEnd();
|
||||
return;
|
||||
};
|
||||
|
||||
//异常提前终止
|
||||
var abort=function(err){
|
||||
if(!This.sendAbort){
|
||||
This.sendAbort=1;
|
||||
This.sendAbortMsg=err||"-";
|
||||
processCall(0,1);//abort后只调用最后一次
|
||||
};
|
||||
This._send();
|
||||
};
|
||||
var processCall=function(addSize,abortLast){
|
||||
if(!abortLast && This.sendAbort){
|
||||
return false;
|
||||
};
|
||||
addSize=addSize||0;
|
||||
if(!set.asrProcess){
|
||||
//默认超过1分钟自动停止
|
||||
return This.sendTotal+addSize<=size60s;
|
||||
};
|
||||
//实时回调
|
||||
var val=set.asrProcess(This.getText()
|
||||
,This.sendDuration(addSize)
|
||||
,This.sendAbort?This.sendAbortMsg:"");
|
||||
if(!This._prsw && typeof(val)!="boolean"){
|
||||
CLog("asrProcess返回值必须是boolean类型,true才能继续识别,否则立即超时",1);
|
||||
};
|
||||
This._prsw=1;
|
||||
return val;
|
||||
};
|
||||
var size5s=This.sampleRate*5;
|
||||
var size60s=This.sampleRate*60;
|
||||
|
||||
//建立ws连接
|
||||
var ws=This.wsCur;
|
||||
if(!ws){
|
||||
if(This.started){//已start才创建ws
|
||||
var resTxt={};
|
||||
This.resTxts.push(resTxt);
|
||||
ws=This.wsCur=This._wsNew(
|
||||
This.tokenData
|
||||
,"ws:"+This.resTxts.length
|
||||
,resTxt
|
||||
,function(){
|
||||
processCall();
|
||||
}
|
||||
,function(){
|
||||
This._send();
|
||||
}
|
||||
,function(err){
|
||||
//异常中断
|
||||
if(ws==This.wsCur){
|
||||
abort(err);
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
return;
|
||||
};
|
||||
|
||||
//正在新建新1分钟连接,等着
|
||||
if(This.wsLock){
|
||||
return;
|
||||
};
|
||||
//已有ok的连接,直接陆续将所有缓冲分段发送完
|
||||
if(ws._s!=2 || ws.isStop){
|
||||
//正在关闭或者其他状态不管,等着
|
||||
return;
|
||||
};
|
||||
//没有数据了
|
||||
if(This.pcmSend>=This.pcmTotal){
|
||||
if(This.state==1){
|
||||
//缓冲数据已发送完,等待新数据
|
||||
return;
|
||||
};
|
||||
|
||||
//已stop,结束识别得到最终结果
|
||||
ws.stopWs(function(){
|
||||
tryStopEnd();
|
||||
},function(err){
|
||||
abort(err);
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
//准备本次发送数据块
|
||||
var minSize=This.sampleRate/1000*50;//最小发送量50ms ≈1.6k
|
||||
var maxSize=This.sampleRate;//最大发送量1000ms ≈32k
|
||||
//速度控制1,取决于网速
|
||||
if((ws.bufferedAmount||0)/2>maxSize*3){
|
||||
//传输太慢,阻塞一会再发送
|
||||
This.sendWait=setTimeout(function(){
|
||||
This.sendWait=0;
|
||||
This._send();
|
||||
},100);
|
||||
return;
|
||||
};
|
||||
//速度控制2,取决于已发送时长,单个文件才会被控制速率
|
||||
if(This.sendSpeed){
|
||||
var spMaxMs=(Date.now()-ws.okTime)*This.sendSpeed;
|
||||
var nextMs=(This.sendCurSize+maxSize/3)/This.sampleRate*1000;
|
||||
var delay=Math.floor((nextMs-spMaxMs)/This.sendSpeed);
|
||||
if(delay>0){
|
||||
//传输太快,怕底层识别不过来,降低发送速度
|
||||
CLog("[ASR]延迟"+delay+"ms发送");
|
||||
This.sendWait=setTimeout(function(){
|
||||
This.sendWait=0;
|
||||
This._send();
|
||||
},delay);
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
var needSend=1;
|
||||
var copyBuffers=function(offset,buffers,dist){
|
||||
var size=dist.length;
|
||||
for(var i=0,idx=0;idx<size&&i<buffers.length;){
|
||||
var pcm=buffers[i];
|
||||
if(pcm.length-offset<=size-idx){
|
||||
dist.set(offset==0?pcm:pcm.subarray(offset),idx);
|
||||
idx+=pcm.length-offset;
|
||||
offset=0;
|
||||
buffers.splice(i,1);
|
||||
}else{
|
||||
dist.set(pcm.subarray(offset,offset+(size-idx)),idx);
|
||||
offset+=size-idx;
|
||||
break;
|
||||
};
|
||||
};
|
||||
return offset;
|
||||
};
|
||||
if(This.joinIsOpen){
|
||||
//发送新1分钟的开头重叠5秒数据
|
||||
if(This.joinOffset==-1){
|
||||
//精准定位5秒
|
||||
This.joinSend=0;
|
||||
This.joinOffset=0;
|
||||
This.log("发送上1分钟结尾5秒数据...");
|
||||
var total=0;
|
||||
for(var i=This.joinBuffers.length-1;i>=0;i--){
|
||||
total+=This.joinBuffers[i].length;
|
||||
if(total>=size5s){
|
||||
This.joinBuffers.splice(0, i);
|
||||
This.joinSize=total;
|
||||
This.joinOffset=total-size5s;
|
||||
break;
|
||||
};
|
||||
};
|
||||
};
|
||||
var buffersSize=This.joinSize-This.joinOffset;//缓冲余量
|
||||
var size=Math.min(maxSize,buffersSize);
|
||||
if(size<=0){
|
||||
//重叠5秒数据发送完毕
|
||||
This.log("发送新1分钟数据(重叠"+Math.round(This.joinSend/This.sampleRate*1000)+"ms)...");
|
||||
This.joinBuffers=[];
|
||||
This.joinSize=0;
|
||||
This.joinOffset=-1;
|
||||
This.joinIsOpen=0;
|
||||
This._send();
|
||||
return;
|
||||
};
|
||||
|
||||
//创建块数据,消耗掉buffers
|
||||
var chunk=new Int16Array(size);
|
||||
This.joinSend+=size;
|
||||
This.joinSendTotal+=size;
|
||||
This.joinOffset=copyBuffers(This.joinOffset,This.joinBuffers,chunk);
|
||||
|
||||
This.joinSize=0;
|
||||
for(var i=0;i<This.joinBuffers.length;i++){
|
||||
This.joinSize+=This.joinBuffers[i].length;
|
||||
};
|
||||
}else{
|
||||
var buffersSize=This.pcmTotal-This.pcmSend;//缓冲余量
|
||||
var buffersDur=Math.round(buffersSize/This.sampleRate*1000);
|
||||
var curHasSize=size60s-This.sendCurSize;//当前连接剩余能发送的量
|
||||
var sizeNext=Math.min(maxSize,buffersSize);//不管连接剩余数时本应当发送的数量
|
||||
var size=Math.min(sizeNext,curHasSize);
|
||||
if(This.state==1 && size<Math.min(minSize,curHasSize)){
|
||||
//不够发送一次的,等待新数据
|
||||
return;
|
||||
};
|
||||
var needNew=0;
|
||||
if(curHasSize<=0){
|
||||
//当前连接一分钟已消耗完
|
||||
if(This.state==2 && buffersSize<This.sampleRate*1.2){
|
||||
//剩余的量太少,并且已stop,没必要再新建连接,直接丢弃
|
||||
size=buffersSize;
|
||||
This.log("丢弃结尾"+buffersDur+"ms数据","#999");
|
||||
needSend=0;
|
||||
}else{
|
||||
//开始新1分钟的连接,等到实时回调后再看要不要新建
|
||||
needNew=true;
|
||||
};
|
||||
};
|
||||
//回调看看是否要超时终止掉
|
||||
if(needSend && !processCall(sizeNext)){//用本应当的发送量来计算
|
||||
//超时,终止识别
|
||||
var durS=Math.round(This.asrDuration()/1000);
|
||||
This.log("已主动超时,共识别"+durS+"秒,丢弃缓冲"+buffersDur+"ms,正在终止...");
|
||||
This.wsLock=1;//阻塞住后续调用
|
||||
ws.stopWs(function(){
|
||||
abort("已主动超时,共识别"+durS+"秒,终止识别");
|
||||
},function(err){
|
||||
abort(err);
|
||||
});
|
||||
return;
|
||||
};
|
||||
//开始新1分钟的连接
|
||||
if(needNew){
|
||||
CLog("[ASR]新1分钟接续,当前缓冲"+buffersDur+"ms...");
|
||||
This.wsLock=1;//阻塞住后续调用
|
||||
ws.stopWs(function(){
|
||||
This._token(function(){
|
||||
This.log("新1分钟接续OK,当前缓冲"+buffersDur+"ms",2);
|
||||
This.wsLock=0;
|
||||
This.wsCur=0;//重置当前连接
|
||||
This.sendCurSize=0;
|
||||
|
||||
This.joinIsOpen=1;//新1分钟先发重叠的5秒数据
|
||||
This.joinOffset=-1;
|
||||
|
||||
This._send();
|
||||
},function(err){
|
||||
abort("语音识别新1分钟token接口出错:"+err);
|
||||
});
|
||||
},function(err){
|
||||
abort(err);
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
//创建块数据,消耗掉buffers
|
||||
var chunk=new Int16Array(size);
|
||||
This.pcmOffset=copyBuffers(This.pcmOffset,This.pcmBuffers,chunk);
|
||||
This.pcmSend+=size;
|
||||
|
||||
//写入到下一分钟的头5秒重叠区域中,不管写了多少,写就完了
|
||||
This.joinBuffers.push(chunk);
|
||||
This.joinSize+=size;
|
||||
};
|
||||
|
||||
This.sendCurSize+=chunk.length;
|
||||
This.sendTotal+=chunk.length;
|
||||
if(needSend){
|
||||
try{
|
||||
ws.send(chunk.buffer);
|
||||
}catch(e){CLog("ws.send",1,e);};
|
||||
};
|
||||
|
||||
//不要停
|
||||
This.sendWait=setTimeout(function(){
|
||||
This.sendWait=0;
|
||||
This._send();
|
||||
});//仅退出调用堆栈
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**返回实时结果文本,如果已stop返回的就是最终文本**/
|
||||
,getText:function(){
|
||||
var arr=this.resTxts;
|
||||
var txt="";
|
||||
for(var i=0;i<arr.length;i++){
|
||||
var obj=arr[i];
|
||||
if(obj.fullTxt){
|
||||
txt=obj.fullTxt;
|
||||
}else{
|
||||
var tmp=obj.tempTxt||"";
|
||||
if(obj.okTxt){
|
||||
tmp=obj.okTxt;
|
||||
};
|
||||
//5秒重叠进行模糊拼接
|
||||
if(!txt){
|
||||
txt=tmp;
|
||||
}else{
|
||||
var left=txt.substr(-20);//240字/分
|
||||
var finds=[];
|
||||
for(var x=0,max=Math.min(17,tmp.length-3);x<=max;x++){
|
||||
for(var i0=0;i0<17;i0++){
|
||||
if(left[i0]==tmp[x]){
|
||||
var n=1;
|
||||
for(;n<17;n++){
|
||||
if(left[i0+n]!=tmp[x+n]){
|
||||
break;
|
||||
};
|
||||
};
|
||||
if(n>=3){//3字相同即匹配
|
||||
finds.push({x:x,i0:i0,n:n});
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
finds.sort(function(a,b){
|
||||
var v=b.n-a.n;
|
||||
return v!=0?v:b.i0-a.i0;//越长越好,越靠后越好
|
||||
});
|
||||
var f0=finds[0];
|
||||
if(f0){
|
||||
txt=txt.substr(0,txt.length-left.length+f0.i0);
|
||||
txt+=tmp.substr(f0.x);
|
||||
}else{
|
||||
txt+=tmp;
|
||||
};
|
||||
};
|
||||
//存起来
|
||||
if(obj.okTxt!=null && tmp==obj.okTxt){
|
||||
obj.fullTxt=txt;
|
||||
};
|
||||
};
|
||||
};
|
||||
return txt;
|
||||
}
|
||||
|
||||
//创建新的wss连接
|
||||
,_wsNew:function(sData,id,resTxt,process,connOk,connFail){
|
||||
var uuid=function(){
|
||||
var s=[];
|
||||
for(var i=0,r;i<32;i++){
|
||||
r=Math.floor(Math.random()*16);
|
||||
s.push(String.fromCharCode(r<10?r+48:r-10+97));
|
||||
};
|
||||
return s.join("");
|
||||
};
|
||||
var This=this,set=This.set;
|
||||
CLog("[ASR "+id+"]正在连接...");
|
||||
var url="wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1?token="+sData.token;
|
||||
if(set.compatibleWebSocket){
|
||||
var ws=set.compatibleWebSocket(url);
|
||||
}else{
|
||||
var ws=new WebSocket(url);
|
||||
}
|
||||
|
||||
//ws._s=0 0连接中 1opening 2openOK 3stoping 4closeing -1closed
|
||||
//ws.isStop=0 1已停止识别
|
||||
ws.onclose=function(){
|
||||
if(ws._s==-1)return;
|
||||
var isFail=ws._s!=4;
|
||||
ws._s=-1;
|
||||
This.log("["+id+"]close");
|
||||
|
||||
isFail&&connFail(ws._err||"连接"+id+"已关闭");
|
||||
};
|
||||
ws.onerror=function(e){
|
||||
if(ws._s==-1)return;
|
||||
var msg="网络连接错误";
|
||||
ws._err||(ws._err=msg);
|
||||
This.log("["+id+"]"+msg,1);
|
||||
ws.onclose();
|
||||
};
|
||||
ws.onopen=function(){
|
||||
if(ws._s==-1)return;
|
||||
ws._s=1;
|
||||
CLog("[ASR "+id+"]open");
|
||||
ws._task=uuid();
|
||||
ws.send(JSON.stringify({
|
||||
header:{
|
||||
message_id:uuid()
|
||||
,task_id:ws._task
|
||||
,appkey:sData.appkey
|
||||
|
||||
,namespace:"SpeechRecognizer"
|
||||
,name:"StartRecognition"
|
||||
}
|
||||
,payload:{
|
||||
format:"pcm"
|
||||
,sample_rate:This.sampleRate
|
||||
,enable_intermediate_result:true //返回中间识别结果
|
||||
,enable_punctuation_prediction:true //添加标点
|
||||
,enable_inverse_text_normalization:true //后处理中将数值处理
|
||||
}
|
||||
,context:{ }
|
||||
}));
|
||||
};
|
||||
ws.onmessage=function(e){
|
||||
var data=e.data;
|
||||
var logMsg=true;
|
||||
if(typeof(data)=="string" && data[0]=="{"){
|
||||
data=JSON.parse(data);
|
||||
var header=data.header||{};
|
||||
var payload=data.payload||{};
|
||||
var name=header.name||"";
|
||||
var status=header.status||0;
|
||||
|
||||
var isFail=name=="TaskFailed";
|
||||
var errMsg="";
|
||||
|
||||
//init
|
||||
if(ws._s==1 && (name=="RecognitionStarted" || isFail)){
|
||||
if(isFail){
|
||||
errMsg="连接"+id+"失败["+status+"]"+header.status_text;
|
||||
}else{
|
||||
ws._s=2;
|
||||
This.log("["+id+"]连接OK");
|
||||
ws.okTime=Date.now();
|
||||
connOk();
|
||||
};
|
||||
};
|
||||
//中间结果
|
||||
if(ws._s==2 && (name=="RecognitionResultChanged" || isFail)){
|
||||
if(isFail){
|
||||
errMsg="识别出现错误["+status+"]"+header.status_text;
|
||||
}else{
|
||||
logMsg=!ws._clmsg;
|
||||
ws._clmsg=1;
|
||||
resTxt.tempTxt=payload.result||"";
|
||||
process();
|
||||
};
|
||||
};
|
||||
//stop
|
||||
if(ws._s==3 && (name=="RecognitionCompleted" || isFail)){
|
||||
var txt="";
|
||||
if(isFail){
|
||||
errMsg="停止识别出现错误["+status+"]"+header.status_text;
|
||||
}else{
|
||||
txt=payload.result||"";
|
||||
This.log("["+id+"]最终识别结果:"+txt);
|
||||
};
|
||||
ws.stopCall&&ws.stopCall(txt,errMsg);
|
||||
};
|
||||
|
||||
if(errMsg){
|
||||
This.log("["+id+"]"+errMsg,1);
|
||||
ws._err||(ws._err=errMsg);
|
||||
};
|
||||
};
|
||||
if(logMsg){
|
||||
CLog("[ASR "+id+"]msg",data);
|
||||
};
|
||||
};
|
||||
ws.stopWs=function(True,False){
|
||||
if(ws._s!=2){
|
||||
False(id+"状态不正确["+ws._s+"]");
|
||||
return;
|
||||
};
|
||||
ws._s=3;
|
||||
ws.isStop=1;
|
||||
|
||||
ws.stopCall=function(txt,err){
|
||||
clearTimeout(ws.stopInt);
|
||||
ws.stopCall=0;
|
||||
ws._s=4;
|
||||
ws.close();
|
||||
|
||||
resTxt.okTxt=txt;
|
||||
process();
|
||||
|
||||
if(err){
|
||||
False(err);
|
||||
}else{
|
||||
True();
|
||||
};
|
||||
};
|
||||
ws.stopInt=setTimeout(function(){
|
||||
ws.stopCall&&ws.stopCall("","停止识别返回结果超时");
|
||||
},10000);
|
||||
|
||||
CLog("[ASR "+id+"]send stop");
|
||||
ws.send(JSON.stringify({
|
||||
header:{
|
||||
message_id:uuid()
|
||||
,task_id:ws._task
|
||||
,appkey:sData.appkey
|
||||
|
||||
,namespace:"SpeechRecognizer"
|
||||
,name:"StopRecognition"
|
||||
}
|
||||
}));
|
||||
};
|
||||
if(ws.connect)ws.connect(); //兼容时会有这个方法
|
||||
return ws;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//获得开始识别的token信息
|
||||
,_token:function(True,False){
|
||||
var This=this,set=This.set;
|
||||
if(!set.tokenApi){
|
||||
False("未配置tokenApi");return;
|
||||
};
|
||||
|
||||
(set.apiRequest||DefaultPost)(set.tokenApi,set.apiArgs||{},function(data){
|
||||
if(!data || !data.appkey || !data.token){
|
||||
False("apiRequest回调的数据格式不正确");return;
|
||||
};
|
||||
This.tokenData=data;
|
||||
True();
|
||||
},False);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
//手撸一个ajax
|
||||
function DefaultPost(url,args,success,fail){
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.timeout=20000;
|
||||
xhr.open("POST",url);
|
||||
xhr.onreadystatechange=function(){
|
||||
if(xhr.readyState==4){
|
||||
if(xhr.status==200){
|
||||
try{
|
||||
var o=JSON.parse(xhr.responseText);
|
||||
}catch(e){};
|
||||
|
||||
if(o.c!==0 || !o.v){
|
||||
fail(o.m||"接口返回非预定义json数据");
|
||||
return;
|
||||
};
|
||||
success(o.v);
|
||||
}else{
|
||||
fail("请求失败["+xhr.status+"]");
|
||||
}
|
||||
}
|
||||
};
|
||||
var arr=[];
|
||||
for(var k in args){
|
||||
arr.push(k+"="+encodeURIComponent(args[k]));
|
||||
};
|
||||
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
|
||||
xhr.send(arr.join("&"));
|
||||
};
|
||||
|
||||
function NOOP(){};
|
||||
|
||||
Recorder[ASR_Aliyun_ShortTxt]=ASR_Aliyun_Short;
|
||||
|
||||
|
||||
}));
|
887
public/extensions/buffer_stream.player.js
Normal file
887
public/extensions/buffer_stream.player.js
Normal file
@ -0,0 +1,887 @@
|
||||
/*
|
||||
录音 Recorder扩展,实时播放录音片段文件,把片段文件转换成MediaStream流
|
||||
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
BufferStreamPlayer可以通过input方法一次性输入整个音频文件,或者实时输入音频片段文件,然后播放出来;输入支持格式:pcm、wav、mp3等浏览器支持的音频格式,非pcm格式会自动解码成pcm(播放音质效果比pcm、wav格式差点);输入前输入后都可进行处理要播放的音频,比如:混音、变速、变调;输入的音频会写入到内部的MediaStream流中,完成将连续的音频片段文件转换成流。
|
||||
|
||||
BufferStreamPlayer可以用于:
|
||||
1. Recorder onProcess等实时处理中,将实时处理好的音频片段转直接换成MediaStream,此流可以作为WebRTC的local流发送到对方,或播放出来;
|
||||
2. 接收到的音频片段文件的实时播放,比如:WebSocket接收到的录音片段文件播放、WebRTC remote流(Recorder支持对这种流进行实时处理)实时处理后的播放;
|
||||
3. 单个音频文件的实时播放处理,比如:播放一段音频,并同时进行可视化绘制(其实自己解码+播放绘制比直接调用这个更有趣,但这个省事、配套功能多点)。
|
||||
|
||||
在线测试例子:
|
||||
https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.decode_buffer_stream_player
|
||||
调用示例:
|
||||
var stream=Recorder.BufferStreamPlayer(set)
|
||||
//创建好后第一件事就是start打开流,打开后就会开始播放input输入的音频,set具体配置看下面源码;注意:start需要在用户操作(触摸、点击等)时进行调用,原因参考runningContext配置
|
||||
stream.start(()=>{
|
||||
stream.currentTime;//当前已播放的时长,单位ms,数值变化时会有onUpdateTime事件
|
||||
stream.duration;//已输入的全部数据总时长,单位ms,数值变化时会有onUpdateTime事件;实时模式下意义不大,会比实际播放的长,因为实时播放时卡了就会丢弃部分数据不播放
|
||||
stream.isStop;//是否已停止,调用了stop方法时会设为true
|
||||
stream.isPause;//是否已暂停,调用了pause方法时会设为true
|
||||
stream.isPlayEnd;//已输入的数据是否播放到了结尾(没有可播放的数据了),input后又会变成false;可代表正在缓冲中或播放结束,状态变更时会有onPlayEnd事件
|
||||
|
||||
//如果不要默认的播放,可以设置set.play为false,这种情况下只拿到MediaStream来用
|
||||
stream.getMediaStream() //通过getMediaStream方法得到MediaStream流,此流可以作为WebRTC的local流发送到对方,或者直接拿来赋值给audio.srcObject来播放(和赋值audio.src作用一致);未start时调用此方法将会抛异常
|
||||
|
||||
stream.getAudioSrc() //【已过时】超低版本浏览器中得到MediaStream流的字符串播放地址,可赋值给audio标签的src,直接播放音频;未start时调用此方法将会抛异常;新版本浏览器已停止支持将MediaStream转换成url字符串,调用本方法新浏览器会抛异常,因此在不需要兼容不支持srcObject的超低版本浏览器时,请直接使用getMediaStream然后赋值给auido.srcObject来播放
|
||||
},(errMsg)=>{
|
||||
//start失败,无法播放
|
||||
});
|
||||
|
||||
//随时都能调用input,会等到start成功后播放出来,不停的调用input,就能持续的播放出声音了,需要暂停播放就不要调用input就行了
|
||||
stream.input(anyData); //anyData数据格式 和更多说明,请阅读下面的input方法源码注释
|
||||
stream.clearInput(keepDuration); //清除已输入但还未播放的数据,一般用于非实时模式打断老的播放;返回清除的音频时长,默认会从总时长duration中减去此时长,keepDuration=true时不减去
|
||||
|
||||
//暂停播放,暂停后:实时模式下会丢弃所有input输入的数据(resume时只播放新input的数据),非实时模式下所有input输入的数据会保留到resume时继续播放
|
||||
stream.pause();
|
||||
//恢复播放,实时模式下只会从最新input的数据开始播放,非实时模式下会从暂停的位置继续播放
|
||||
stream.resume();
|
||||
|
||||
//不要播放了就调用stop停止播放,关闭所有资源
|
||||
stream.stop();
|
||||
|
||||
|
||||
|
||||
注意:已知Firefox的AudioBuffer没法动态修改数据,所以对于带有这种特性的浏览器将采用先缓冲后再播放(类似assets/runtime-codes/fragment.playbuffer.js),音质会相对差一点;其他浏览器测试Android、IOS、Chrome无此问题;start方法中有一大段代码给浏览器做了特性检测并进行兼容处理。
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
var BufferStreamPlayer=function(set){
|
||||
return new fn(set);
|
||||
};
|
||||
var BufferStreamPlayerTxt="BufferStreamPlayer";
|
||||
var fn=function(set){
|
||||
var This=this;
|
||||
var o={
|
||||
play:true //要播放声音,设为false不播放,只提供MediaStream
|
||||
,realtime:true /*默认为true实时模式,设为false为非实时模式
|
||||
实时模式:设为 true 或 {maxDelay:300,discardAll:false}配置对象
|
||||
如果有新的input输入数据,但之前输入的数据还未播放完的时长不超过maxDelay时(缓冲播放延迟默认限制在300ms内),如果积压的数据量过大则积压的数据将会被直接丢弃,少量积压会和新数据一起加速播放,最终达到尽快播放新输入的数据的目的;这在网络不流畅卡顿时会发挥很大作用,可有效降低播放延迟;出现加速播放时声音听起来会比较怪异,可配置discardAll=true来关闭此特性,少量积压的数据也直接丢弃,不会加速播放;如果你的音频数据块超过200ms,需要调大maxDelay(取值100-800ms)
|
||||
非实时模式:设为 false
|
||||
连续完整的播放完所有input输入的数据,之前输入的还未播放完又有新input输入会加入队列排队播放,比如用于:一次性同时输入几段音频完整播放
|
||||
*/
|
||||
|
||||
|
||||
//,onInputError:fn(errMsg, inputIndex) //当input输入出错时回调,参数为input第几次调用和错误消息
|
||||
//,onUpdateTime:fn() //已播放时长、总时长更新回调(stop、pause、resume后一定会回调),this.currentTime为已播放时长,this.duration为已输入的全部数据总时长(实时模式下意义不大,会比实际播放的长),单位都是ms
|
||||
//,onPlayEnd:fn() //没有可播放的数据时回调(stop后一定会回调),已输入的数据已全部播放完了,可代表正在缓冲中或播放结束;之后如果继续input输入了新数据,播放完后会再次回调,因此会多次回调;非实时模式一次性输入了数据时,此回调相当于播放完成,可以stop掉,重新创建对象来input数据可达到循环播放效果
|
||||
|
||||
//,decode:false //input输入的数据在调用transform之前是否要进行一次音频解码成pcm [Int16,...]
|
||||
//mp3、wav等都可以设为true、或设为{fadeInOut:true}配置对象,会自动解码成pcm;默认会开启fadeInOut对解码的pcm首尾进行淡入淡出处理,减少爆音(wav等解码后和原始pcm一致的音频,可以把fadeInOut设为false)
|
||||
|
||||
//transform:fn(inputData,sampleRate,True,False)
|
||||
//将input输入的data(如果开启了decode将是解码后的pcm)转换处理成要播放的pcm数据;如果没有解码也没有提供本方法,input的data必须是[Int16,...]并且设置set.sampleRate
|
||||
//inputData:any input方法输入的任意格式数据,只要这个转换函数支持处理;如果开启了decode,此数据为input输入的数据解码后的pcm [Int16,...]
|
||||
//sampleRate:123 如果设置了decode为解码后的采样率,否则为set.sampleRate || null
|
||||
//True(pcm,sampleRate) 回调处理好的pcm数据([Int16,...])和pcm的采样率
|
||||
//False(errMsg) 处理失败回调
|
||||
|
||||
//sampleRate:16000 //可选input输入的数据默认的采样率,当没有设置解码也没有提供transform时应当明确设置采样率
|
||||
|
||||
//runningContext:AudioContext //可选提供一个state为running状态的AudioContext对象(ctx),默认会在start时自动创建一个新的ctx,这个配置的作用请参阅Recorder的runningContext配置
|
||||
};
|
||||
for(var k in set){
|
||||
o[k]=set[k];
|
||||
};
|
||||
This.set=set=o;
|
||||
|
||||
if(!set.onInputError){
|
||||
set.onInputError=function(err,n){ CLog(err,1); };
|
||||
}
|
||||
};
|
||||
fn.prototype=BufferStreamPlayer.prototype={
|
||||
/**【已过时】获取MediaStream的audio播放地址,新版浏览器、未start将会抛异常**/
|
||||
getAudioSrc:function(){
|
||||
CLog($T("0XYC::getAudioSrc方法已过时:请直接使用getMediaStream然后赋值给audio.srcObject,仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"),3);
|
||||
if(!this._src){
|
||||
//新版chrome调用createObjectURL会直接抛异常了 https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#using_object_urls_for_media_streams
|
||||
this._src=(window.URL||webkitURL).createObjectURL(this.getMediaStream());
|
||||
}
|
||||
return this._src;
|
||||
}
|
||||
/**获取MediaStream流对象,未start将会抛异常**/
|
||||
,getMediaStream:function(){
|
||||
if(!this._dest){
|
||||
throw new Error(NoStartMsg());
|
||||
}
|
||||
return this._dest.stream;
|
||||
}
|
||||
|
||||
|
||||
/**打开音频流,打开后就会开始播放input输入的音频;注意:start需要在用户操作(触摸、点击等)时进行调用,原因参考runningContext配置
|
||||
* True() 打开成功回调
|
||||
* False(errMsg) 打开失败回调**/
|
||||
,start:function(True,False){
|
||||
var falseCall=function(msg,noClear){
|
||||
var next=!checkStop();
|
||||
if(!noClear)This._clear();
|
||||
CLog(msg,1);
|
||||
next&&False&&False(msg);
|
||||
};
|
||||
var checkStop=function(){
|
||||
if(This.isStop){
|
||||
CLog($T("6DDt::start被stop终止"),3);
|
||||
return true;
|
||||
};
|
||||
};
|
||||
var This=this,set=This.set,__abTest=This.__abTest;
|
||||
if(This._Tc!=null){
|
||||
falseCall($T("I4h4::{1}多次start",0,BufferStreamPlayerTxt),1);
|
||||
return;
|
||||
}
|
||||
if(!isBrowser){
|
||||
falseCall($T.G("NonBrowser-1",[BufferStreamPlayerTxt]));
|
||||
return;
|
||||
}
|
||||
This._Tc=0;//currentTime 对应的采样数
|
||||
This._Td=0;//duration 对应的采样数
|
||||
|
||||
This.currentTime=0;//当前已播放的时长,单位ms
|
||||
This.duration=0;//已输入的全部数据总时长,单位ms;实时模式下意义不大,会比实际播放的长,因为实时播放时卡了就会丢弃部分数据不播放
|
||||
This.isStop=0;//是否已停止
|
||||
This.isPause=0;//是否已暂停
|
||||
This.isPlayEnd=0;//已输入的数据是否播放到了结尾(没有可播放的数据了),input后又会变成false;可代表正在缓冲中或播放结束
|
||||
|
||||
This.inputN=0;//第n次调用input
|
||||
|
||||
This.inputQueueIdx=0;//input调用队列当前已处理到的位置
|
||||
This.inputQueue=[];//input调用队列,用于纠正执行顺序
|
||||
|
||||
This.bufferSampleRate=0;//audioBuffer的采样率,首次input后就会固定下来
|
||||
This.audioBuffer=0;
|
||||
This.pcmBuffer=[[],[]];//未推入audioBuffer的pcm数据缓冲
|
||||
|
||||
var fail=function(msg){
|
||||
falseCall($T("P6Gs::浏览器不支持打开{1}",0,BufferStreamPlayerTxt)+(msg?": "+msg:""));
|
||||
};
|
||||
|
||||
var ctx=set.runningContext || Recorder.GetContext(true); This._ctx=ctx;
|
||||
var sVal=ctx.state,spEnd=Recorder.CtxSpEnd(sVal);
|
||||
!__abTest&&CLog("start... ctx.state="+sVal+(
|
||||
spEnd?$T("JwDm::(注意:ctx不是running状态,start需要在用户操作(触摸、点击等)时进行调用,否则会尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"):""
|
||||
));
|
||||
|
||||
var support=1;
|
||||
if(!ctx || !ctx.createMediaStreamDestination){
|
||||
support=0;
|
||||
}else{
|
||||
var source=ctx.createBufferSource();
|
||||
if(!source.start || source.onended===undefined){
|
||||
support=0;//createBufferSource版本太低,难兼容
|
||||
}
|
||||
};
|
||||
if(!support){
|
||||
fail("");
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
var end=function(){
|
||||
if(checkStop())return;
|
||||
//创建MediaStream
|
||||
var dest=ctx.createMediaStreamDestination();
|
||||
dest.channelCount=1;
|
||||
This._dest=dest;
|
||||
|
||||
!__abTest&&CLog("start ok");
|
||||
True&&True();
|
||||
|
||||
This._inputProcess();//处理未完成start前的input调用
|
||||
This._updateTime();//更新时间
|
||||
|
||||
//定时在没有input输入时,将未写入buffer的数据写进去
|
||||
if(!badAB){
|
||||
This._writeInt=setInterval(function(){
|
||||
This._writeBuffer();
|
||||
},100);
|
||||
}else{
|
||||
CLog($T("qx6X::此浏览器的AudioBuffer实现不支持动态特性,采用兼容模式"),3);
|
||||
This._writeInt=setInterval(function(){
|
||||
This._writeBad();
|
||||
},10);//定时调用进行数据写入播放
|
||||
}
|
||||
};
|
||||
var abTest=function(){
|
||||
//浏览器实现检测,已知Firefox的AudioBuffer没法在_writeBuffer中动态修改数据;检测方法:直接新开一个,输入一段测试数据,看看能不能拿到流中的数据
|
||||
var testStream=BufferStreamPlayer({ play:false,sampleRate:8000,runningContext:ctx });
|
||||
testStream.__abTest=1; var testRec;
|
||||
testStream.start(function(){
|
||||
testRec=Recorder({
|
||||
type:"unknown"
|
||||
,sourceStream:testStream.getMediaStream()
|
||||
,runningContext:ctx
|
||||
,onProcess:function(buffers){
|
||||
var bf=buffers[buffers.length-1],all0=1;
|
||||
for(var i=0;i<bf.length;i++){
|
||||
if(bf[i]!=0){ all0=0; break; }
|
||||
}
|
||||
if(all0 && buffers.length<5){
|
||||
return;//再等等看,最长约等500ms
|
||||
}
|
||||
testRec.close();
|
||||
testStream.stop();
|
||||
|
||||
if(testInt){ clearTimeout(testInt); testInt=0;
|
||||
//全部是0就是浏览器不行,要缓冲一次性播放进行兼容
|
||||
badAB=all0;
|
||||
BufferStreamPlayer.BadAudioBuffer=badAB;
|
||||
end();
|
||||
}
|
||||
}
|
||||
});
|
||||
testRec.open(function(){
|
||||
testRec.start();
|
||||
},function(msg){
|
||||
testStream.stop(); fail(msg);
|
||||
});
|
||||
},fail);
|
||||
//超时没有回调
|
||||
var testInt=setTimeout(function(){
|
||||
testInt=0; testStream.stop(); testRec&&testRec.close();
|
||||
fail($T("cdOx::环境检测超时"));
|
||||
},1500);
|
||||
//随机生成1秒的数据,rec有一次回调即可
|
||||
var data=new Int16Array(8000);
|
||||
for(var i=0;i<8000;i++){
|
||||
data[i]=~~(Math.random()*0x7fff*2-0x7fff);
|
||||
}
|
||||
testStream.input(data);
|
||||
};
|
||||
|
||||
var badAB=BufferStreamPlayer.BadAudioBuffer;
|
||||
var ctxNext=function(){
|
||||
if(__abTest || badAB!=null){
|
||||
setTimeout(end); //应当setTimeout一下强转成异步,统一调用代码时的行为
|
||||
}else{
|
||||
abTest();
|
||||
};
|
||||
};
|
||||
var tag="AudioContext resume: ";
|
||||
Recorder.ResumeCtx(ctx,function(runC){
|
||||
runC&&CLog(tag+"wait...");
|
||||
return !This.isStop;
|
||||
},function(runC){
|
||||
runC&&CLog(tag+ctx.state);
|
||||
ctxNext();
|
||||
},function(err){ //比较少见,可能没有影响
|
||||
CLog(tag+ctx.state+" "+$T("S2Bu::可能无法播放:{1}",0,err),1);
|
||||
ctxNext();
|
||||
});
|
||||
}
|
||||
,_clear:function(){
|
||||
var This=this;
|
||||
This.isStop=1;
|
||||
clearInterval(This._writeInt);
|
||||
This.inputQueue=0;
|
||||
|
||||
if(This._src){
|
||||
(window.URL||webkitURL).revokeObjectURL(This._src);
|
||||
This._src=0;
|
||||
}
|
||||
if(This._dest){
|
||||
Recorder.StopS_(This._dest.stream);
|
||||
This._dest=0;
|
||||
}
|
||||
if(!This.set.runningContext && This._ctx){
|
||||
Recorder.CloseNewCtx(This._ctx);
|
||||
}
|
||||
This._ctx=0;
|
||||
|
||||
var source=This.bufferSource;
|
||||
if(source){
|
||||
source.disconnect();
|
||||
source.stop();
|
||||
}
|
||||
This.bufferSource=0;
|
||||
This.audioBuffer=0;
|
||||
}
|
||||
/**停止播放,关闭所有资源**/
|
||||
,stop:function(){
|
||||
var This=this;
|
||||
This._clear();
|
||||
|
||||
!This.__abTest&&CLog("stop");
|
||||
This._playEnd(1);
|
||||
}
|
||||
/**暂停播放,暂停后:实时模式下会丢弃所有input输入的数据(resume时只播放新input的数据),非实时模式下所有input输入的数据会保留到resume时继续播放**/
|
||||
,pause:function(){
|
||||
CLog("pause");
|
||||
this.isPause=1;
|
||||
this._updateTime(1);
|
||||
}
|
||||
/**恢复播放,实时模式下只会从最新input的数据开始播放,非实时模式下会从暂停的位置继续播放**/
|
||||
,resume:function(){
|
||||
var This=this,tag="resume",tag3=tag+"(wait ctx)";
|
||||
CLog(tag);
|
||||
This.isPause=0;
|
||||
This._updateTime(1);
|
||||
|
||||
var ctx=This._ctx;
|
||||
if(ctx){ //AudioContext如果被暂停,尽量恢复
|
||||
Recorder.ResumeCtx(ctx,function(runC){
|
||||
runC&&CLog(tag3+"...");
|
||||
return !This.isStop && !This.isPause;
|
||||
},function(runC){
|
||||
runC&&CLog(tag3+ctx.state);
|
||||
},function(err){
|
||||
CLog(tag3+ctx.state+"[err]"+err,1);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//当前输入的数据播放到结尾时触发回调,stop时永远会触发回调
|
||||
,_playEnd:function(stop){
|
||||
var This=this,startTime=This._PNs,call=This.set.onPlayEnd;
|
||||
if(stop || !This.isPause){//暂停播到结尾不算
|
||||
if(stop || !This.isPlayEnd){
|
||||
if(stop || (startTime && Date.now()-startTime>500)){//已停止或者延迟确认成功
|
||||
This._PNs=0;
|
||||
This.isPlayEnd=1;
|
||||
call&&call();
|
||||
This._updateTime(1);
|
||||
}else if(!startTime){//刚检测到的没有数据了,开始延迟确认
|
||||
This._PNs=Date.now();
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
//有数据播放时,取消已到结尾状态
|
||||
,_playLive:function(){
|
||||
var This=this;
|
||||
This.isPlayEnd=0;
|
||||
This._PNs=0;
|
||||
}
|
||||
//时间更新时触发回调,没有更新时不会触发回调
|
||||
,_updateTime:function(must){
|
||||
var This=this,sampleRate=This.bufferSampleRate||9e9,call=This.set.onUpdateTime;
|
||||
This.currentTime=Math.round(This._Tc/sampleRate*1000);
|
||||
This.duration=Math.round(This._Td/sampleRate*1000);
|
||||
|
||||
var s=""+This.currentTime+This.duration;
|
||||
if(must || This._UTs!=s){
|
||||
This._UTs=s;
|
||||
call&&call();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**输入任意格式的音频数据,未完成start前调用会等到start成功后生效
|
||||
anyData: any 具体类型取决于:
|
||||
set.decode为false时:
|
||||
未提供set.transform,数据必须是pcm[Int16,...],此时的set必须提供sampleRate;
|
||||
提供了set.transform,数据为transform方法支持的任意格式。
|
||||
set.decode为true时:
|
||||
数据必须是ArrayBuffer,会自动解码成pcm[Int16,...];注意输入的每一片数据都应该是完整的一个音频片段文件,否则可能会解码失败;注意ArrayBuffer对象是Transferable object,参与解码后此对象将不可用,因为内存数据已被转移到了解码线程,可通过 stream.input(arrayBuffer.slice(0)) 形式复制一份再解码就没有这个问题了。
|
||||
|
||||
关于anyData的二进制长度:
|
||||
如果是提供的pcm、wav格式数据,数据长度对播放无太大影响,很短的数据也能很好的连续播放。
|
||||
如果是提供的mp3这种必须解码才能获得pcm的数据,数据应当尽量长点,测试发现片段有300ms以上解码后能很好的连续播放,低于100ms解码后可能会有明显的杂音,更低的可能会解码失败;当片段确实太小时,可以将本来会多次input调用的数据缓冲起来,等数据量达到了300ms再来调用一次input,能比较显著的改善播放音质。
|
||||
**/
|
||||
,input:function(anyData){
|
||||
var This=this,set=This.set;
|
||||
var inputN=++This.inputN;
|
||||
if(!This.inputQueue){
|
||||
throw new Error(NoStartMsg());
|
||||
}
|
||||
|
||||
var decSet=set.decode;
|
||||
if(decSet){
|
||||
//先解码
|
||||
DecodeAudio(anyData, function(data){
|
||||
if(!This.inputQueue)return;//stop了
|
||||
if(decSet.fadeInOut==null || decSet.fadeInOut){
|
||||
FadeInOut(data.data, data.sampleRate);//解码后的数据进行一下淡入淡出处理,减少爆音
|
||||
}
|
||||
This._input2(inputN, data.data, data.sampleRate);
|
||||
},function(err){
|
||||
This._inputErr(err, inputN);
|
||||
});
|
||||
}else{
|
||||
This._input2(inputN, anyData, set.sampleRate);
|
||||
}
|
||||
}
|
||||
//transform处理
|
||||
,_input2:function(inputN, anyData, sampleRate){
|
||||
var This=this,set=This.set;
|
||||
|
||||
if(set.transform){
|
||||
set.transform(anyData, sampleRate, function(pcm, sampleRate2){
|
||||
if(!This.inputQueue)return;//stop了
|
||||
|
||||
sampleRate=sampleRate2||sampleRate;
|
||||
This._input3(inputN, pcm, sampleRate);
|
||||
},function(err){
|
||||
This._inputErr(err, inputN);
|
||||
});
|
||||
}else{
|
||||
This._input3(inputN, anyData, sampleRate);
|
||||
}
|
||||
}
|
||||
//转换好的pcm加入input队列,纠正调用顺序,未start时等待
|
||||
,_input3:function(inputN, pcm, sampleRate){
|
||||
var This=this;
|
||||
|
||||
if(!pcm || !pcm.subarray){
|
||||
This._inputErr($T("ZfGG::input调用失败:非pcm[Int16,...]输入时,必须解码或者使用transform转换"), inputN);
|
||||
return;
|
||||
}
|
||||
if(!sampleRate){
|
||||
This._inputErr($T("N4ke::input调用失败:未提供sampleRate"), inputN);
|
||||
return;
|
||||
}
|
||||
if(This.bufferSampleRate && This.bufferSampleRate!=sampleRate){
|
||||
This._inputErr($T("IHZd::input调用失败:data的sampleRate={1}和之前的={2}不同",0,sampleRate,This.bufferSampleRate), inputN);
|
||||
return;
|
||||
}
|
||||
if(!This.bufferSampleRate){
|
||||
This.bufferSampleRate=sampleRate;//首次处理后,固定下来,后续的每次输入都是相同的
|
||||
}
|
||||
|
||||
//加入队列,纠正input执行顺序,解码、transform均有可能会导致顺序不一致
|
||||
if(inputN>This.inputQueueIdx){ //clearInput移动了队列位置的丢弃
|
||||
This.inputQueue[inputN]=pcm;
|
||||
}
|
||||
|
||||
if(This._dest){//已start,可以开始处理队列
|
||||
This._inputProcess();
|
||||
}
|
||||
}
|
||||
,_inputErr:function(errMsg, inputN){
|
||||
if(!this.inputQueue) return;//stop了
|
||||
this.inputQueue[inputN]=1;//出错了,队列里面也要占个位
|
||||
this.set.onInputError(errMsg, inputN);
|
||||
}
|
||||
//处理input队列
|
||||
,_inputProcess:function(){
|
||||
var This=this;
|
||||
if(!This.bufferSampleRate){
|
||||
return;
|
||||
}
|
||||
|
||||
var queue=This.inputQueue;
|
||||
for(var i=This.inputQueueIdx+1;i<queue.length;i++){ //inputN是从1开始,所以+1
|
||||
var pcm=queue[i];
|
||||
if(pcm==1){
|
||||
This.inputQueueIdx=i;//跳过出错的input
|
||||
continue;
|
||||
}
|
||||
if(!pcm){
|
||||
return;//之前的input还未进入本方法,退出等待
|
||||
}
|
||||
|
||||
This.inputQueueIdx=i;
|
||||
queue[i]=null;
|
||||
|
||||
//推入缓冲,最多两个元素 [堆积的,新的]
|
||||
var pcms=This.pcmBuffer;
|
||||
var pcm0=pcms[0],pcm1=pcms[1];
|
||||
if(pcm0.length){
|
||||
if(pcm1.length){
|
||||
var tmp=new Int16Array(pcm0.length+pcm1.length);
|
||||
tmp.set(pcm0);
|
||||
tmp.set(pcm1,pcm0.length);
|
||||
pcms[0]=tmp;
|
||||
}
|
||||
}else{
|
||||
pcms[0]=pcm1;
|
||||
}
|
||||
pcms[1]=pcm;
|
||||
|
||||
This._Td+=pcm.length;//更新已输入总时长
|
||||
This._updateTime();
|
||||
This._playLive();//有播放数据了
|
||||
}
|
||||
|
||||
if(!BufferStreamPlayer.BadAudioBuffer){
|
||||
if(!This.audioBuffer){
|
||||
This._createBuffer(true);
|
||||
}else{
|
||||
This._writeBuffer();
|
||||
}
|
||||
}else{
|
||||
This._writeBad();
|
||||
}
|
||||
}
|
||||
|
||||
/**清除已输入但还未播放的数据,一般用于非实时模式打断老的播放;返回清除的音频时长,默认会从总时长duration中减去此时长,keepDuration时不减去*/
|
||||
,clearInput:function(keepDuration){
|
||||
var This=this, sampleRate=This.bufferSampleRate, size=0;
|
||||
if(This.inputQueue){//未stop
|
||||
This.inputQueueIdx=This.inputN;//队列位置移到结尾
|
||||
|
||||
var pcms=This.pcmBuffer;
|
||||
size=pcms[0].length+pcms[1].length;
|
||||
This._subClear();
|
||||
if(!keepDuration) This._Td-=size;//减掉已输入总时长
|
||||
This._updateTime(1);
|
||||
}
|
||||
var dur = size? Math.round(size/sampleRate*1000) : 0;
|
||||
CLog("clearInput "+dur+"ms "+size);
|
||||
return dur;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/****************正常的播放处理****************/
|
||||
//创建播放buffer
|
||||
,_createBuffer:function(init){
|
||||
var This=this,set=This.set;
|
||||
if(!init && !This.audioBuffer){
|
||||
return;
|
||||
}
|
||||
|
||||
var ctx=This._ctx;
|
||||
var sampleRate=This.bufferSampleRate;
|
||||
var bufferSize=sampleRate*(set.bufferSecond||60);//建一个可以持续播放60秒的buffer,循环写入数据播放,大点好简单省事
|
||||
var buffer=ctx.createBuffer(1, bufferSize,sampleRate);
|
||||
|
||||
var source=ctx.createBufferSource();
|
||||
source.channelCount=1;
|
||||
source.buffer=buffer;
|
||||
source.connect(This._dest);
|
||||
if(set.play){//播放出声音
|
||||
source.connect(ctx.destination);
|
||||
}
|
||||
source.onended=function(){
|
||||
source.disconnect();
|
||||
source.stop();
|
||||
|
||||
This._createBuffer();//重新创建buffer
|
||||
};
|
||||
source.start();//古董 source.noteOn(0) 不支持onended 放弃支持
|
||||
|
||||
This.bufferSource=source;
|
||||
This.audioBuffer=buffer;
|
||||
This.audioBufferIdx=0;
|
||||
This._createBufferTime=Date.now();
|
||||
|
||||
This._writeBuffer();
|
||||
}
|
||||
,_writeBuffer:function(){
|
||||
var This=this,set=This.set;
|
||||
var buffer=This.audioBuffer;
|
||||
var sampleRate=This.bufferSampleRate;
|
||||
var oldAudioBufferIdx=This.audioBufferIdx;
|
||||
if(!buffer){
|
||||
return;
|
||||
}
|
||||
|
||||
//计算已播放的量,可能已播放过头了,卡了没有数据
|
||||
var playSize=Math.floor((Date.now()-This._createBufferTime)/1000*sampleRate);
|
||||
if(This.audioBufferIdx+0.005*sampleRate<playSize){//5ms动态区间
|
||||
This.audioBufferIdx=playSize;//将写入位置修正到当前播放位置
|
||||
}
|
||||
//写进去了,但还未被播放的量
|
||||
var wnSize=Math.max(0, This.audioBufferIdx-playSize);
|
||||
|
||||
//这次最大能写入多少;限制到800ms,包括写入了还未播放的
|
||||
var maxSize=buffer.length-This.audioBufferIdx;
|
||||
maxSize=Math.min(maxSize, ~~(0.8*sampleRate)-wnSize);
|
||||
if(maxSize<1){//写不下了,退出
|
||||
return;
|
||||
}
|
||||
|
||||
if(This._subPause()){//暂停了,不消费缓冲数据
|
||||
return;
|
||||
};
|
||||
var pcms=This.pcmBuffer;
|
||||
var pcm0=pcms[0],pcm1=pcms[1],pcm1Len=pcm1.length;
|
||||
if(pcm0.length+pcm1Len==0){//无可用数据,退出
|
||||
This._playEnd();//无可播放数据回调
|
||||
return;
|
||||
};
|
||||
This._playLive();//有播放数据了
|
||||
|
||||
var pcmSize=0,speed=1;
|
||||
var realMode=set.realtime;
|
||||
while(realMode){
|
||||
//************实时模式************
|
||||
//尽量同步播放,避免过大延迟,但始终保持延迟150ms播放新数据,这样每次添加进新数据都是接到还未播放到的最后面,减少引入的杂音,减少网络波动的影响
|
||||
var delaySecond=0.15;
|
||||
|
||||
//计算当前堆积的量
|
||||
var dSize=wnSize+pcm0.length;
|
||||
var dMax=(realMode.maxDelay||300)/1000 *sampleRate;
|
||||
|
||||
//堆积的在300ms内按正常播放
|
||||
if(dSize<dMax){
|
||||
//至少要延迟播放新数据
|
||||
var d150Size=Math.floor(delaySecond*sampleRate-dSize-pcm1Len);
|
||||
if(oldAudioBufferIdx==0 && d150Size>0){
|
||||
//开头加上少了的延迟
|
||||
This.audioBufferIdx=Math.max(This.audioBufferIdx, d150Size);
|
||||
}
|
||||
|
||||
realMode=false;//切换成顺序播放
|
||||
break;
|
||||
}
|
||||
//堆积的太多,配置为全丢弃
|
||||
if(realMode.discardAll){
|
||||
if(dSize>dMax*1.333){//超过400ms,取200ms正常播放,300ms中位数
|
||||
pcm0=This._cutPcm0(Math.round(dMax*0.666-wnSize-pcm1Len));
|
||||
}
|
||||
realMode=false;//切换成顺序播放
|
||||
break;
|
||||
}
|
||||
|
||||
//堆积的太多,要加速播放了,最多播放积压最后3秒的量,超过的直接丢弃
|
||||
pcm0=This._cutPcm0(3*sampleRate-wnSize-pcm1Len);
|
||||
|
||||
speed=1.6;//倍速,重采样
|
||||
//计算要截取出来量
|
||||
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1Len)/speed));
|
||||
break;
|
||||
}
|
||||
if(!realMode){
|
||||
//*******按顺序取数据播放*********
|
||||
//计算要截取出来量
|
||||
pcmSize=Math.min(maxSize, pcm0.length+pcm1Len);
|
||||
}
|
||||
if(!pcmSize){
|
||||
return;
|
||||
}
|
||||
|
||||
//截取数据并写入到audioBuffer中
|
||||
This.audioBufferIdx=This._subWrite(buffer,pcmSize,This.audioBufferIdx,speed);
|
||||
}
|
||||
|
||||
|
||||
/****************兼容播放处理,播放音质略微差点****************/
|
||||
,_writeBad:function(){
|
||||
var This=this,set=This.set;
|
||||
var buffer=This.audioBuffer;
|
||||
var sampleRate=This.bufferSampleRate;
|
||||
var ctx=This._ctx;
|
||||
|
||||
//正在播放,5ms不能结束就等待播放完,定时器是10ms
|
||||
if(buffer){
|
||||
var ms=buffer.length/sampleRate*1000;
|
||||
if(Date.now()-This._createBufferTime<ms-5){
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//这次最大能写入多少;限制到800ms
|
||||
var maxSize=~~(0.8*sampleRate);
|
||||
var st=set.PlayBufferDisable?0:sampleRate/1000*300;//缓冲播放,不然间隔太短接续爆音明显
|
||||
|
||||
if(This._subPause()){//暂停了,不消费缓冲数据
|
||||
return;
|
||||
};
|
||||
var pcms=This.pcmBuffer;
|
||||
var pcm0=pcms[0],pcm1=pcms[1],pcm1Len=pcm1.length;
|
||||
var allSize=pcm0.length+pcm1Len;
|
||||
if(allSize==0 || allSize<st){//无可用数据 不够缓冲量,退出
|
||||
This._playEnd();//无可播放数据回调,最后一丁点会始终等缓冲满导致卡住
|
||||
return;
|
||||
};
|
||||
This._playLive();//有播放数据了
|
||||
|
||||
var pcmSize=0,speed=1;
|
||||
var realMode=set.realtime;
|
||||
while(realMode){
|
||||
//************实时模式************
|
||||
//计算当前堆积的量
|
||||
var dSize=pcm0.length;
|
||||
var dMax=(realMode.maxDelay||300)/1000 *sampleRate;
|
||||
|
||||
//堆积的在300ms内按正常播放
|
||||
if(dSize<dMax){
|
||||
realMode=false;//切换成顺序播放
|
||||
break;
|
||||
}
|
||||
//堆积的太多,配置为全丢弃
|
||||
if(realMode.discardAll){
|
||||
if(dSize>dMax*1.333){//超过400ms,取200ms正常播放,300ms中位数
|
||||
pcm0=This._cutPcm0(Math.round(dMax*0.666-pcm1Len));
|
||||
}
|
||||
realMode=false;//切换成顺序播放
|
||||
break;
|
||||
}
|
||||
|
||||
//堆积的太多,要加速播放了,最多播放积压最后3秒的量,超过的直接丢弃
|
||||
pcm0=This._cutPcm0(3*sampleRate-pcm1Len);
|
||||
|
||||
speed=1.6;//倍速,重采样
|
||||
//计算要截取出来量
|
||||
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1Len)/speed));
|
||||
break;
|
||||
}
|
||||
if(!realMode){
|
||||
//*******按顺序取数据播放*********
|
||||
//计算要截取出来量
|
||||
pcmSize=Math.min(maxSize, pcm0.length+pcm1Len);
|
||||
}
|
||||
if(!pcmSize){
|
||||
return;
|
||||
}
|
||||
|
||||
//新建buffer,一次性完整播放当前的数据
|
||||
buffer=ctx.createBuffer(1,pcmSize,sampleRate);
|
||||
|
||||
//截取数据并写入到audioBuffer中
|
||||
This._subWrite(buffer,pcmSize,0,speed);
|
||||
|
||||
//首尾进行1ms的淡入淡出 大幅减弱爆音
|
||||
FadeInOut(buffer.getChannelData(0), sampleRate);
|
||||
|
||||
var source=ctx.createBufferSource();
|
||||
source.channelCount=1;
|
||||
source.buffer=buffer;
|
||||
source.connect(This._dest);
|
||||
if(set.play){//播放出声音
|
||||
source.connect(ctx.destination);
|
||||
}
|
||||
source.start();//古董 source.noteOn(0) 不支持onended 放弃支持
|
||||
|
||||
This.bufferSource=source;
|
||||
This.audioBuffer=buffer;
|
||||
This._createBufferTime=Date.now();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
,_cutPcm0:function(pcmNs){//保留堆积的数据到指定的时长数量
|
||||
var pcms=this.pcmBuffer,pcm0=pcms[0];
|
||||
if(pcmNs<0)pcmNs=0;
|
||||
if(pcm0.length>pcmNs){//丢弃超过秒数的
|
||||
var size=pcm0.length-pcmNs, dur=Math.round(size/this.bufferSampleRate*1000);
|
||||
pcm0=pcm0.subarray(size);
|
||||
pcms[0]=pcm0;
|
||||
CLog($T("L8sC::延迟过大,已丢弃{1}ms {2}",0,dur,size),3);
|
||||
}
|
||||
return pcm0;
|
||||
}
|
||||
,_subPause:function(){//暂停了,就不要消费掉缓冲数据了,等待resume再来消费
|
||||
var This=this;
|
||||
if(!This.isPause){
|
||||
return 0;
|
||||
};
|
||||
if(This.set.realtime){//实时模式,丢弃所有未消费的数据,resume时从最新input的数据开始播放
|
||||
This._subClear();
|
||||
};
|
||||
return 1;
|
||||
}
|
||||
,_subClear:function(){ //清除缓冲数据
|
||||
this.pcmBuffer=[[],[]];
|
||||
}
|
||||
,_subWrite:function(buffer, pcmSize, offset, speed){
|
||||
var This=this;
|
||||
var pcms=This.pcmBuffer;
|
||||
var pcm0=pcms[0],pcm1=pcms[1];
|
||||
|
||||
//截取数据
|
||||
var pcm=new Int16Array(pcmSize);
|
||||
var i=0,n=0;
|
||||
for(var j=0;n<pcmSize && j<pcm0.length;){//简单重采样
|
||||
pcm[n++]=pcm0[i];
|
||||
j+=speed; i=Math.round(j);
|
||||
}
|
||||
if(i>=pcm0.length){//堆积的消耗完了
|
||||
pcm0=new Int16Array(0);
|
||||
|
||||
for(j=0,i=0;n<pcmSize && j<pcm1.length;){
|
||||
pcm[n++]=pcm1[i];
|
||||
j+=speed; i=Math.round(j);
|
||||
}
|
||||
if(i>=pcm1.length){
|
||||
pcm1=new Int16Array(0);
|
||||
}else{
|
||||
pcm1=pcm1.subarray(i);
|
||||
}
|
||||
pcms[1]=pcm1;
|
||||
}else{
|
||||
pcm0=pcm0.subarray(i);
|
||||
}
|
||||
pcms[0]=pcm0;
|
||||
|
||||
|
||||
//写入到audioBuffer中
|
||||
var channel=buffer.getChannelData(0);
|
||||
for(var i=0;i<pcmSize;i++,offset++){
|
||||
channel[offset]=pcm[i]/0x7FFF;
|
||||
}
|
||||
|
||||
This._Tc+=pcmSize;//更新已播放时长
|
||||
This._updateTime();
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var NoStartMsg=function(){
|
||||
return $T("TZPq::{1}未调用start方法",0,BufferStreamPlayerTxt);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**pcm数据进行首尾1ms淡入淡出处理,播放时可以大幅减弱爆音**/
|
||||
var FadeInOut=BufferStreamPlayer.FadeInOut=function(arr,sampleRate){
|
||||
var sd=sampleRate/1000*1;//浮点数,arr是Int16或者Float32
|
||||
for(var i=0;i<sd;i++){
|
||||
arr[i]*=i/sd;
|
||||
}
|
||||
for(var l=arr.length,i=~~(l-sd);i<l;i++){
|
||||
arr[i]*=(l-i)/sd;
|
||||
}
|
||||
};
|
||||
|
||||
/**解码音频文件成pcm**/
|
||||
var DecodeAudio=BufferStreamPlayer.DecodeAudio=function(arrayBuffer,True,False){
|
||||
var ctx=Recorder.GetContext();
|
||||
if(!ctx){//强制激活Recorder.Ctx 不支持大概率也不支持解码
|
||||
False&&False($T("iCFC::浏览器不支持音频解码"));
|
||||
return;
|
||||
};
|
||||
if(!arrayBuffer || !(arrayBuffer instanceof ArrayBuffer)){
|
||||
False&&False($T("wE2k::音频解码数据必须是ArrayBuffer"));
|
||||
return;//非ArrayBuffer 有日志但不抛异常 不会走回调
|
||||
};
|
||||
|
||||
ctx.decodeAudioData(arrayBuffer,function(raw){
|
||||
var src=raw.getChannelData(0);
|
||||
var sampleRate=raw.sampleRate;
|
||||
|
||||
var pcm=new Int16Array(src.length);
|
||||
for(var i=0;i<src.length;i++){//floatTo16BitPCM
|
||||
var s=Math.max(-1,Math.min(1,src[i]));
|
||||
s=s<0?s*0x8000:s*0x7FFF;
|
||||
pcm[i]=s;
|
||||
};
|
||||
|
||||
True&&True({
|
||||
sampleRate:sampleRate
|
||||
,duration:Math.round(src.length/sampleRate*1000)
|
||||
,data:pcm
|
||||
});
|
||||
},function(e){
|
||||
False&&False($T("mOaT::音频解码失败:{1}",0,e&&e.message||"-"));
|
||||
});
|
||||
};
|
||||
|
||||
var CLog=function(){
|
||||
var v=arguments; v[0]="["+BufferStreamPlayerTxt+"]"+v[0];
|
||||
Recorder.CLog.apply(null,v);
|
||||
};
|
||||
Recorder[BufferStreamPlayerTxt]=BufferStreamPlayer;
|
||||
|
||||
|
||||
}));
|
372
public/extensions/create-audio.nmn2pcm.js
Normal file
372
public/extensions/create-audio.nmn2pcm.js
Normal file
@ -0,0 +1,372 @@
|
||||
/***
|
||||
简单用 正弦波、方波、锯齿波、三角波 函数生成一段音乐简谱的pcm数据,主要用于测试时提供音频数据。本可音频生成插件可以移植到其他语言环境,如需定制可联系作者
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
此插件在线生成测试:assets/runtime-codes/test.create-audio.nmn2pcm.js
|
||||
|
||||
var pcmData=Recorder.NMN2PCM(set);
|
||||
set配置:{
|
||||
texts:""|["",""] 简谱格式化文本,如果格式不符合要求,将会抛异常
|
||||
sampleRate: 生成pcm的采样率,默认48000;取值不能过低,否则会削除高音
|
||||
timbre: 音色,默认2.0(使用音符对应频率的一个倍频),取值>=1.0
|
||||
meterDuration: 一拍时长,毫秒,默认600ms
|
||||
muteDuration: 音符之间的静默,毫秒,0时无静默,默认meterDur/4(最大50ms)
|
||||
beginDuration: 开头的静默时长,毫秒,0时无静默,默认为200ms
|
||||
endDuration: 结尾的静默时长,毫秒,0时无静默,默认为200ms
|
||||
|
||||
volume: 音量,默认0.3,取值范围0.0-1.0(最大值1)
|
||||
waveType: 波形发生器类型,默认"sine",取值:sine(正弦波)、square(方波,volume应当减半)、sawtooth(锯齿波)、triangle(三角波)
|
||||
}
|
||||
|
||||
texts格式:单个文本,或文本数组
|
||||
- 四分音符(一拍):低音: 1.-7. 中音: 1-7 高音: 1'-7' 休止符(静音):0
|
||||
- 音符后面用 "." 表示低音(尽量改用".":".." 倍低音,"..." 超低音)
|
||||
- 音符后面用 "'" 表示高音(尽量改用"'":"''" 倍高音,"'''" 超高音)
|
||||
- 音符之间用 "|" 或 " " 分隔一拍
|
||||
- 一拍里面多个音符用 "," 分隔,每个音按权重分配这一拍的时长占比,如:“6,7”为一拍,6、7各占1/2拍,相当于八分音符
|
||||
|
||||
- 音符后面用 "-" 表示二分音符,简单计算为1+1=2拍时长,几个-就加几拍
|
||||
- 音符后面用 "_" 表示八分音符;两两在一拍里面的音符可以免写_,自动会按1/2分配;一拍里面只有一个音时这拍会被简单计算为1/2=0.5拍;其他情况计算会按权重分配这一拍的时长(复杂),如:“6,7_”为1/2+1/2/2=0.75拍(“6*,7_”才是(1+0.5)/2+1/2/2=1拍),其中6权重1分配1/2=0.5拍,7权重0.5分配1/2/2=0.25拍;多加一个"_"就多除个2:“6_,7_”是1/2+1/2=1拍(等同于“6,7”可免写_);“6__,7__”是1/2/2+1/2/2=0.5拍;只要权重加起来是整数就算作完整的1拍
|
||||
- 音符后面用 "*" 表示1+0.5=1.5拍,多出来的1/2计算和_相同(复杂),"**"两个表示加0.25
|
||||
|
||||
- 可以使用 "S"(sine) "Q"(square) "A"(sawtooth) "T"(triangle) 来切换后续波形发生器类型(按一拍来书写,但不占用时长),类型后面可以接 "(2.0)" 来设置音色,接 "[0.5]" 来设置音量(为set.volume*0.5);特殊值 "R"(reset) 可重置类型成set配置值,如果R后面没有接音色或音量也会被重置;比如:"1 2|A(4.0)[0.6] 3 4 R|5 6",其中12 56使用set配置的类型和音色音量,34使用锯齿波、音色4.0、音量0.18=0.3*0.6
|
||||
|
||||
- 如果同时有多个音,必须提供数组格式,每个音单独提供一个完整简谱(必须同步对齐)
|
||||
|
||||
返回结果:{
|
||||
pcm: Int16Array,pcm数据
|
||||
duration: 123 pcm的时长,单位毫秒
|
||||
set: {...} 使用的set配置
|
||||
warns: [] 不适合抛异常的提示消息
|
||||
}
|
||||
|
||||
Recorder.NMN2PCM.GetExamples() 可获取内置的简谱
|
||||
***/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
var NMN2PCM=function(set){
|
||||
var texts=set.texts||[]; if(typeof(texts)=="string") texts=[texts];
|
||||
var setSR=set.sampleRate, sampleRate=setSR; if(!sampleRate || sampleRate<1)sampleRate=48000;
|
||||
var meterDur=set.meterDuration||600;
|
||||
var timbre=set.timbre||2; if(timbre<1)timbre=1;
|
||||
|
||||
var volume=set.volume; if(volume==null)volume=0.3;
|
||||
volume=Math.max(0,volume); volume=Math.min(1,volume);
|
||||
|
||||
var waveType=set.waveType||"";
|
||||
if(",sine,square,sawtooth,triangle,".indexOf(","+waveType+",")==-1)waveType="";
|
||||
waveType=waveType||"sine";
|
||||
|
||||
var muteDur=set.muteDuration;
|
||||
if(muteDur==null || muteDur<0){
|
||||
muteDur=meterDur/4; if(muteDur>50)muteDur=50;
|
||||
}
|
||||
var mute0=new Int16Array(sampleRate*muteDur/1000);
|
||||
|
||||
var beginDur=set.beginDuration;
|
||||
if(beginDur==null || beginDur<0) beginDur=200;
|
||||
var beginMute=new Int16Array(sampleRate*beginDur/1000);
|
||||
var endDur=set.endDuration;
|
||||
if(endDur==null || endDur<0) endDur=200;
|
||||
var endMute=new Int16Array(sampleRate*endDur/1000);
|
||||
|
||||
//生成C调频率 A=440 国际标准音
|
||||
var s=function(s){ return 440/Math.pow(2,s/12) };
|
||||
var Freqs=[s(9),s(7),s(5),s(4),s(2),s(0),s(-2)];
|
||||
var FreqMP={};
|
||||
for(var i=1;i<=7;i++){
|
||||
var v=Freqs[i-1];
|
||||
FreqMP[i+"..."]=v/8;
|
||||
FreqMP[i+".."]=v/4;
|
||||
FreqMP[i+"."]=v/2;
|
||||
FreqMP[i]=v;
|
||||
FreqMP[i+"'"]=v*2;
|
||||
FreqMP[i+"''"]=v*4;
|
||||
FreqMP[i+"'''"]=v*8;
|
||||
}
|
||||
|
||||
var tracks=[],freqMax=0,freqMin=90000;
|
||||
for(var iT=0;setSR!=-1 && iT<texts.length;iT++){
|
||||
var meters=texts[iT].split(/[\s\|]+/);
|
||||
var buffers=[],size=0,wType=waveType,wTimbre=timbre,wVol=volume;
|
||||
for(var i0=0;i0<meters.length;i0++){
|
||||
var txt0=meters[i0]; if(!txt0)continue;
|
||||
var v0=txt0.charCodeAt(0);
|
||||
if(v0<48 || v0>55){//不是0-7,切换波形或音色
|
||||
var m=/^(\w)(?:\((.+)\)|\[(.+)\])*$/.exec(txt0)||[],mT=m[1];
|
||||
var m=/\((.+)\)/.exec(txt0)||[],mTb=m[1];
|
||||
var m=/\[(.+)\]/.exec(txt0)||[],mVol=m[1];
|
||||
if(mT=="R"){ wType=waveType;wTimbre=timbre;wVol=volume; }
|
||||
else if(mT=="S") wType="sine";
|
||||
else if(mT=="Q") wType="square";
|
||||
else if(mT=="A") wType="sawtooth";
|
||||
else if(mT=="T") wType="triangle";
|
||||
else mT="";
|
||||
if(!mT||mTb&&!+mTb||mVol&&!+mVol)throw new Error("Invalid: "+txt0);
|
||||
if(mTb)wTimbre=+mTb;
|
||||
if(mVol)wVol=volume*mVol;
|
||||
continue;
|
||||
}
|
||||
var ys=txt0.split(",");//一拍里面的音符
|
||||
var durTotal=meterDur; //一拍的时长,如果里面有+,代表多拍
|
||||
var bTotal=0,hasG=0,hasX=0;
|
||||
for(var i2=0;i2<ys.length;i2++){//先计算出每个音符的占用时长比例
|
||||
var vs=ys[i2].split("");
|
||||
var o={ y:vs[0],b:1,t:wType,tb:wTimbre,vol:wVol }; ys[i2]=o;
|
||||
for(var i3=1;i3<vs.length;i3++){
|
||||
var v=vs[i3];
|
||||
if(v=="'") o.y+="'";
|
||||
else if(v==".") o.y+=".";
|
||||
else if(v=="-"){ o.b+=1; durTotal+=meterDur; }
|
||||
else if(v=="_"){ o.b/=2; hasG=1; }
|
||||
else if(v=="*" && !hasX){ o.b+=0.5; hasX=0.5;
|
||||
if(vs[i3+1]=="*"){ o.b-=0.25; hasX=0.25; i3++; } }
|
||||
else throw new Error($T("3RBa::符号[{1}]无效:{2}",0,v,txt0));
|
||||
}
|
||||
bTotal+=o.b;
|
||||
}
|
||||
if(bTotal%1>0){
|
||||
if(hasG){//"_"不够数量,减掉时间
|
||||
durTotal*=bTotal/Math.ceil(bTotal);
|
||||
}else if(hasX){//"*"加上1/2|1/4拍的时间
|
||||
durTotal+=meterDur*hasX;
|
||||
}
|
||||
}
|
||||
durTotal-=ys.length*muteDur;//减掉中间的静默
|
||||
for(var i2=0;i2<ys.length;i2++){//生成每个音符的pcm
|
||||
var o=ys[i2],wType=o.t,wTimbre=o.tb,wVol=o.vol,freq=FreqMP[o.y]||0;
|
||||
if(!freq && o.y!="0") throw new Error($T("U212::音符[{1}]无效:{2}",0,o.y,txt0));
|
||||
freq=freq*wTimbre;
|
||||
var dur=durTotal*o.b/bTotal;
|
||||
var pcm=new Int16Array(Math.round(dur/1000*sampleRate));
|
||||
if(freq){
|
||||
freqMax=Math.max(freqMax,freq);
|
||||
freqMin=Math.min(freqMin,freq);
|
||||
//不同波形算法取自 https://github.com/cristovao-trevisan/wave-generator/blob/master/index.js
|
||||
if(wType=="sine"){//正弦波
|
||||
var V=(2 * Math.PI) * freq / sampleRate;
|
||||
for(var i=0;i<pcm.length;i++){
|
||||
var v=wVol*Math.sin(V * i);
|
||||
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
|
||||
}
|
||||
}else if(wType=="square"){//方波
|
||||
var V=sampleRate / freq;
|
||||
for(var i=0;i<pcm.length;i++){
|
||||
var v=wVol*((i % V) < (V / 2) ? 1 : -1);
|
||||
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
|
||||
}
|
||||
}else if(wType=="sawtooth"){//锯齿波
|
||||
var V=sampleRate / freq;
|
||||
for(var i=0;i<pcm.length;i++){
|
||||
var v=wVol*(-1 + 2 * (i % V) / V);
|
||||
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
|
||||
}
|
||||
}else if(wType=="triangle"){//三角波
|
||||
var V=sampleRate / freq;
|
||||
for(var i=0;i<pcm.length;i++){
|
||||
var Vi = (i + V / 4) % V;
|
||||
var v=wVol*(Vi<V/2?(-1+4*Vi/V):(3-4*Vi/V));
|
||||
pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF;
|
||||
}
|
||||
}
|
||||
var pcmDur4=~~(pcm.length/sampleRate*1000/4)||1;
|
||||
FadeInOut(pcm,sampleRate,Math.min(pcmDur4, 10));
|
||||
}
|
||||
|
||||
var mute=mute0; if(!buffers.length)mute=beginMute;
|
||||
buffers.push(mute); size+=mute.length;
|
||||
|
||||
buffers.push(pcm); size+=pcm.length;
|
||||
}
|
||||
}
|
||||
if(size>0){
|
||||
buffers.push(endMute); size+=endMute.length;
|
||||
tracks.push({buffers:buffers,size:size});
|
||||
}
|
||||
}
|
||||
tracks.sort(function(a,b){return b.size-a.size});
|
||||
|
||||
var pcm=new Int16Array(tracks[0]&&tracks[0].size||0);
|
||||
for(var iT=0;iT<tracks.length;iT++){
|
||||
var o=tracks[iT],buffers=o.buffers,size=o.size;
|
||||
if(iT==0){
|
||||
for(var i=0,offset=0;i<buffers.length;i++){
|
||||
var buf=buffers[i];
|
||||
pcm.set(buf,offset);
|
||||
offset+=buf.length;
|
||||
}
|
||||
}else{
|
||||
var diffMs=(pcm.length-size)/sampleRate*1000;
|
||||
if(diffMs>10){//10毫秒误差
|
||||
throw new Error($T("7qAD::多个音时必须对齐,相差{1}ms",0,diffMs));
|
||||
};
|
||||
for(var i=0,offset=0;i<buffers.length;i++){
|
||||
var buf=buffers[i];
|
||||
for(var j=0;j<buf.length;j++){
|
||||
var data_mix,data1=pcm[offset],data2=buf[j];
|
||||
|
||||
//简单混音算法 https://blog.csdn.net/dancing_night/article/details/53080819
|
||||
if(data1<0 && data2<0){
|
||||
data_mix = data1+data2 - (data1 * data2 / -0x7FFF);
|
||||
}else{
|
||||
data_mix = data1+data2 - (data1 * data2 / 0x7FFF);
|
||||
};
|
||||
|
||||
pcm[offset++]=data_mix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dur=Math.round(pcm.length/sampleRate*1000);
|
||||
var Warns=[],minSR=~~(freqMax*2);
|
||||
if(freqMax && sampleRate<minSR){
|
||||
var msg="sampleRate["+sampleRate+"] should be greater than "+minSR;
|
||||
Warns.push(msg); Recorder.CLog("NMN2PCM: "+msg,3);
|
||||
}
|
||||
|
||||
return {pcm:pcm, duration:dur, warns:Warns, set:{
|
||||
texts:texts, sampleRate:sampleRate, timbre:timbre, meterDuration:meterDur
|
||||
,muteDuration:muteDur, beginDuration:beginDur, endDuration:endDur
|
||||
,volume:volume,waveType:waveType
|
||||
}};
|
||||
};
|
||||
|
||||
|
||||
/**pcm数据进行首尾1ms淡入淡出处理,播放时可以大幅减弱爆音**/
|
||||
var FadeInOut=NMN2PCM.FadeInOut=function(arr,sampleRate,dur){
|
||||
var sd=sampleRate/1000*(dur||1);//浮点数,arr是Int16或者Float32
|
||||
for(var i=0;i<sd;i++){
|
||||
arr[i]*=i/sd;
|
||||
}
|
||||
for(var l=arr.length,i=~~(l-sd);i<l;i++){
|
||||
arr[i]*=(l-i)/sd;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/***内置部分简谱*****/
|
||||
NMN2PCM.GetExamples=function(){ return {
|
||||
|
||||
DFH:{//前3句,https://www.hnchemeng.com/liux/201807/68393.html
|
||||
name:"东方红"
|
||||
,get:function(sampleRate){
|
||||
return NMN2PCM({ //https://www.bilibili.com/video/BV1VW4y1v7nY?p=2
|
||||
sampleRate:sampleRate
|
||||
,meterDuration:1000
|
||||
,timbre:3
|
||||
,texts:"5 5,6|2-|1 1,6.|2-|5 5|6,1' 6,5|1 1,6.|2-"
|
||||
});
|
||||
}
|
||||
}
|
||||
,HappyBirthday:{//4句,https://www.zaoxu.com/jjsh/bkdq/310228.html
|
||||
name:$T("QGsW::祝你生日快乐")
|
||||
,get:function(sampleRate){
|
||||
return NMN2PCM({
|
||||
sampleRate:sampleRate
|
||||
,meterDuration:450
|
||||
,timbre:4
|
||||
,waveType:"triangle", volume:0.15
|
||||
,texts:"5.,5. 6. 5.|1 7.-|5.,5. 6. 5.|2 1-|5.,5. 5 3|1 7. 6.|4*,4_ 3 1|2 1-"
|
||||
});
|
||||
}
|
||||
}
|
||||
,LHC:{//节选一段,https://www.qinyipu.com/jianpu/jianpudaquan/41703.html
|
||||
name:"兰花草(洒水版)"
|
||||
,get:function(sampleRate){
|
||||
return NMN2PCM({
|
||||
sampleRate:sampleRate
|
||||
,meterDuration:650
|
||||
,timbre:4
|
||||
,texts:"6.,3 3,3|3* 2_|1*,2_ 1,7.|6.-|6,6 6,6|6* 5_|3_,5_,5 5,4|3-|3,3_,6_ 6,5|3* 2_|1*,2_ 1,7.|6. 3.|3.,1 1,7.|6.* 2__,3__|2*,1_ 7._,7._,5.|6.-"
|
||||
});
|
||||
}
|
||||
}
|
||||
,ForElise:{//节选一段,https://www.qinyipu.com/jianpu/chunyinle/3023.html
|
||||
name:$T("emJR::致爱丽丝")
|
||||
,get:function(sampleRate){
|
||||
return NMN2PCM({
|
||||
sampleRate:sampleRate
|
||||
,meterDuration:550
|
||||
,muteDuration:20
|
||||
,timbre:6
|
||||
,texts:"3',2'|3',2' 3',7 2',1'|"
|
||||
+"6 0,1 3,6|7 0,3 5,7|1' 0 3',2'|"
|
||||
+"3',2' 3',7 2',1'|6 0,1 3,6|7 0,3 1',7|"
|
||||
+"6 0,7 1',2'|3' 0,5 4',3'|2' 0,4 3',2'|1' 0,3 2',1'|"
|
||||
+"7"
|
||||
});
|
||||
}
|
||||
}
|
||||
,Canon_Right:{//节选一段,https://www.cangqiang.com.cn/d/32153.html
|
||||
name:$T("GsYy::卡农-右手简谱")
|
||||
,get:function(sampleRate){
|
||||
return NMN2PCM({
|
||||
sampleRate:sampleRate
|
||||
,meterDuration:700
|
||||
,texts:"1',7 1',3 5 6,7|"
|
||||
+"1' 3' 5',3' 5',6'|4',3' 2',4' 3',2' 1',7| 7 1',2'|"
|
||||
+"5',3'_,4'_ 5',3'_,4'_ 5',5,6,7 1',2',3',4'|3',1'_,2'_ 3',3_,4_ 5,6,5,4 5,1',7,1'|"
|
||||
+"6,1'_,7_ 6,5_,4_ 5,4,3,4 5,6,7,1'|6,1'_,7_ 1',7_,6_ 7,6,7,1' 2'_,1'_,7|1'-"
|
||||
});
|
||||
}
|
||||
}
|
||||
,Canon:{//开头一段,https://www.kanpula.com/jianpu/21316.html
|
||||
name:$T("bSFZ::卡农")
|
||||
,get:function(sampleRate){
|
||||
var txt1="",txt2="",txt3="",txt4="";
|
||||
//(1)
|
||||
txt1+="3'---|2'---|1'---|7---|";
|
||||
txt2+="1'---|7---|6---|5---|";
|
||||
txt3+="5---|5---|3---|3---|";
|
||||
txt4+="R[0.3] 1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|";
|
||||
//(5)
|
||||
txt1+="6---|5---|6---|7---|";
|
||||
txt2+="4---|3---|4---|5---|";
|
||||
txt3+="1---|1---|1---|2---|";
|
||||
txt4+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5.. 2. 5. 7.|";
|
||||
//(9)
|
||||
txt1+="3'---|2'---|1'---|7---|";
|
||||
txt2+="1'---|7---|6---|5---|";
|
||||
txt3+="5---|5---|3---|3-- 5'|";
|
||||
txt4+="1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|";
|
||||
//(13)
|
||||
txt1+="4' 3' 2' 4'|3' 2' 1' 5|6- 6 1'|7 1' 2'-|";
|
||||
txt2+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5.. 2. 5. 7.|";
|
||||
txt3+="0---|0---|0---|0---|";
|
||||
txt4+="0---|0---|0---|0---|";
|
||||
//(17)
|
||||
txt1+="3',5 1'_ 5' 5_ 3'|3' 4' 3' 2'|1',3 6_ 3' 3_ 1'|1' 2' 1' 7|";
|
||||
txt2+="1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|";
|
||||
txt3+="0---|0---|0---|0---|";
|
||||
txt4+="0---|0---|0---|0---|";
|
||||
//(21)
|
||||
txt1+="6,1 4_ 1' 1_ 6|5,1 3_ 1' 1_ 5|6,1 4_ 1' 1_ 6|7 7 1' 2'|";
|
||||
txt2+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5..,5. 5..,5. 6..,6. 6..,6.|";
|
||||
txt3+="0---|0---|0---|0---|";
|
||||
txt4+="0---|0---|0---|0---|";
|
||||
|
||||
return NMN2PCM({
|
||||
sampleRate:sampleRate
|
||||
,meterDuration:500
|
||||
,texts:[txt1,txt2,txt3,txt4]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Recorder.NMN2PCM=NMN2PCM;
|
||||
|
||||
}));
|
268
public/extensions/dtmf.decode.js
Normal file
268
public/extensions/dtmf.decode.js
Normal file
@ -0,0 +1,268 @@
|
||||
/*
|
||||
录音 Recorder扩展,DTMF(电话拨号按键信号)解码器,解码得到按键值
|
||||
使用本扩展需要引入lib.fft.js支持
|
||||
|
||||
本扩展识别DTMF按键准确度高,误识别率低,支持识别120ms以上按键间隔+30ms以上的按键音,纯js实现易于移植
|
||||
|
||||
使用场景:电话录音软解,软电话实时提取DTMF按键信号等
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
参数:
|
||||
pcmData:[Int16,...] pcm一维数组,原则上一次处理的数据量不要超过10秒,太长的数据应当分段延时处理
|
||||
sampleRate: 123 pcm的采样率
|
||||
prevChunk: null || {} 上次的返回值,用于连续识别
|
||||
|
||||
返回:
|
||||
chunk:{
|
||||
keys:[keyItem,...] 识别到的按键,如果未识别到数组长度为0
|
||||
keyItem:{
|
||||
key:"" //按键值 0-9 #*
|
||||
time:123 //所在的时间位置,ms
|
||||
}
|
||||
|
||||
//以下用于下次接续识别
|
||||
lastIs:"" "":mute {}:match 结尾处是什么
|
||||
lastCheckCount:0 结尾如果是key,此时的检查次数
|
||||
prevIs:"" "":null {}:match 上次疑似检测到了什么
|
||||
totalLen:0 总采样数,相对4khz
|
||||
pcm:[Int16,...] 4khz pcm数据
|
||||
checkFactor:3 信号检查因子,取值1,2,3,默认为3不支持低于32ms的按键音检测,当需要检测时可以设为2,当信号更恶劣时设为1,这样将会减少检查的次数,导致错误识别率变高
|
||||
debug:false 是否开启调试日志
|
||||
}
|
||||
*/
|
||||
Recorder.DTMF_Decode=function(pcmData,sampleRate,prevChunk){
|
||||
prevChunk||(prevChunk={});
|
||||
var lastIs=prevChunk.lastIs||"";
|
||||
var lastCheckCount=prevChunk.lastCheckCount==null?99:prevChunk.lastCheckCount;
|
||||
var prevIs=prevChunk.prevIs||"";
|
||||
var totalLen=prevChunk.totalLen||0;
|
||||
var prevPcm=prevChunk.pcm;
|
||||
var checkFactor=prevChunk.checkFactor||0;
|
||||
var debug=prevChunk.debug;
|
||||
|
||||
var keys=[];
|
||||
|
||||
if(!Recorder.LibFFT){
|
||||
throw new Error($T.G("NeedImport-2",["DTMF_Decode","src/extensions/lib.fft.js"]));
|
||||
};
|
||||
var bufferSize=256;//小一点每次处理的时长不会太长,也不要太小影响分辨率
|
||||
var fft=Recorder.LibFFT(bufferSize);
|
||||
|
||||
|
||||
/****初始值计算****/
|
||||
var windowSize=bufferSize/4;//滑动窗口大小,取值为4的原因:64/4=16ms,16ms*(3-1)=32ms,保证3次取值判断有效性
|
||||
var checkCount=checkFactor||3;//只有3次连续窗口内计算结果相同判定为有效信号或间隔
|
||||
var muteCount=3;//两个信号间的最小间隔,3个窗口大小
|
||||
var startTotal=totalLen;
|
||||
|
||||
/****将采样率降低到4khz,单次fft处理1000/(4000/256)=64ms,分辨率4000/256=15.625hz,允许连续dtmf信号间隔128ms****/
|
||||
var stepFloat=sampleRate/4000;
|
||||
|
||||
var newSize=Math.floor(pcmData.length/stepFloat);
|
||||
totalLen+=newSize;
|
||||
var pos=0;
|
||||
if(prevPcm&&prevPcm.length>bufferSize){//接上上次的数据,继续滑动
|
||||
pos=windowSize*(checkCount+1);
|
||||
newSize+=pos;
|
||||
startTotal-=pos;
|
||||
};
|
||||
var arr=new Int16Array(newSize);
|
||||
if(pos){
|
||||
arr.set(prevPcm.subarray(prevPcm.length-pos));//接上上次的数据,继续滑动
|
||||
};
|
||||
|
||||
for(var idxFloat=0;idxFloat<pcmData.length;pos++,idxFloat+=stepFloat){
|
||||
//简单抽样
|
||||
arr[pos]=pcmData[Math.round(idxFloat)];
|
||||
};
|
||||
pcmData=arr;
|
||||
sampleRate=4000;
|
||||
|
||||
|
||||
var freqStep=sampleRate/bufferSize;//分辨率
|
||||
var logMin=20;//粗略计算信号强度最小值,此值是先给0再根据下面的Math.log(fv)多次【测试】(下面一个log)出来的
|
||||
|
||||
|
||||
/****循环处理所有数据,识别出所有按键信号****/
|
||||
for(var i0=0; i0+bufferSize<=pcmData.length; i0+=windowSize){
|
||||
var arr=pcmData.subarray(i0,i0+bufferSize);
|
||||
var freqs=fft.transform(arr);
|
||||
var freqPs=[];
|
||||
|
||||
var fv0=0,p0=0,v0=0,vi0=0, fv1=0,p1=0,v1=0,vi1=0;//查找高群和低群
|
||||
for(var i2=0;i2<freqs.length;i2++){
|
||||
var fv=freqs[i2];
|
||||
var p=Math.log(fv);//粗略计算信号强度
|
||||
freqPs.push(p);
|
||||
var v=(i2+1)*freqStep;
|
||||
if(p>logMin){
|
||||
if(fv>fv0 && v<1050){
|
||||
fv0=fv;
|
||||
p0=p;
|
||||
v0=v;
|
||||
vi0=i2;
|
||||
}else if(fv>fv1 && v>1050){
|
||||
fv1=fv;
|
||||
p1=p;
|
||||
v1=v;
|
||||
vi1=i2;
|
||||
};
|
||||
};
|
||||
};
|
||||
var pv0 =-1, pv1=-1;
|
||||
if(v0>600 && v1<1700 && Math.abs(p0-p1)<2.5){//高低频的幅度相差不能太大,此值是先给个大值再多次【测试】(下面一个log)得出来的
|
||||
//波形匹配度:两个峰值之间应当是深V型曲线,如果出现大幅杂波,可以直接排除掉
|
||||
var isV=1;
|
||||
//先找出谷底
|
||||
var pMin=p0,minI=0;
|
||||
for(var i2=vi0;i2<vi1;i2++){
|
||||
var v=freqPs[i2];
|
||||
if(v && v<pMin){//0不作数
|
||||
pMin=v;
|
||||
minI=i2;
|
||||
};
|
||||
};
|
||||
var xMax=(p0-pMin)*0.5//允许幅度变化最大值
|
||||
//V左侧,下降段
|
||||
var curMin=p0;
|
||||
for(var i2=vi0;isV&&i2<minI;i2++){
|
||||
var v=freqPs[i2];
|
||||
if(v<=curMin){
|
||||
curMin=v;
|
||||
}else if(v-curMin>xMax){
|
||||
isV=0;//下降段检测到过度上升
|
||||
};
|
||||
};
|
||||
//V右侧,上升段
|
||||
var curMax=pMin;
|
||||
for(var i2=minI;isV&&i2<vi1;i2++){
|
||||
var v=freqPs[i2];
|
||||
if(v>=curMax){
|
||||
curMax=v;
|
||||
}else if(curMax-v>xMax){
|
||||
isV=0;//上升段检测到过度下降
|
||||
};
|
||||
};
|
||||
|
||||
if(isV){
|
||||
pv0=FindIndex(v0, DTMF_Freqs[0], freqStep);
|
||||
pv1=FindIndex(v1, DTMF_Freqs[1], freqStep);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
var key="";
|
||||
if (pv0 >= 0 && pv1 >= 0) {
|
||||
key = DTMF_Chars[pv0][pv1];
|
||||
if(debug)console.log(key,Math.round((startTotal+i0)/sampleRate*1000),p0.toFixed(2),p1.toFixed(2),Math.abs(p0-p1).toFixed(2)); //【测试】得出数值
|
||||
|
||||
if(lastIs){
|
||||
if(lastIs.key==key){//有效,增加校验次数
|
||||
lastCheckCount++;
|
||||
}else{//异常数据,恢复间隔计数
|
||||
key="";
|
||||
lastCheckCount=lastIs.old+lastCheckCount;
|
||||
};
|
||||
}else{
|
||||
//没有连续的信号,检查是否在100ms内有检测到信号,当中间是断开的那种
|
||||
if(prevIs && prevIs.old2 && prevIs.key==key){
|
||||
if(startTotal+i0-prevIs.start<100*sampleRate/1000){
|
||||
lastIs=prevIs;
|
||||
lastCheckCount=prevIs.old2+1;
|
||||
if(debug)console.warn("接续了开叉的信号"+lastCheckCount);
|
||||
};
|
||||
};
|
||||
if(!lastIs){
|
||||
if(lastCheckCount>=muteCount){//间隔够了,开始按键识别计数
|
||||
lastIs={key:key,old:lastCheckCount,old2:lastCheckCount,start:startTotal+i0,pcms:[],use:0};
|
||||
lastCheckCount=1;
|
||||
}else{//上次识别以来间隔不够,重置间隔计数
|
||||
key="";
|
||||
lastCheckCount=0;
|
||||
};
|
||||
};
|
||||
};
|
||||
}else{
|
||||
if(lastIs){//下一个,恢复间隔计数
|
||||
lastIs.old2=lastCheckCount;
|
||||
lastCheckCount=lastIs.old+lastCheckCount;
|
||||
};
|
||||
};
|
||||
|
||||
if(key){
|
||||
if(debug)lastIs.pcms.push(arr);
|
||||
//按键有效,并且未push过
|
||||
if(lastCheckCount>=checkCount && !lastIs.use){
|
||||
lastIs.use=1;
|
||||
keys.push({
|
||||
key:key
|
||||
,time:Math.round(lastIs.start/sampleRate*1000)
|
||||
});
|
||||
};
|
||||
//重置间隔数据
|
||||
if(lastIs.use){
|
||||
if(debug)console.log(key+"有效按键",lastIs);
|
||||
lastIs.old=0;
|
||||
lastIs.old2=0;
|
||||
lastCheckCount=0;
|
||||
};
|
||||
}else{
|
||||
//未发现按键
|
||||
if(lastIs){
|
||||
if(debug)console.log(lastIs) //测试,输出疑似key
|
||||
prevIs=lastIs;
|
||||
};
|
||||
lastIs="";
|
||||
lastCheckCount++;
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
keys:keys
|
||||
|
||||
,lastIs:lastIs
|
||||
,lastCheckCount:lastCheckCount
|
||||
,prevIs:prevIs
|
||||
,totalLen:totalLen
|
||||
,pcm:pcmData
|
||||
,checkFactor:checkFactor
|
||||
,debug:debug
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
var DTMF_Freqs = [
|
||||
[697, 770, 852, 941],
|
||||
[1209, 1336, 1477, 1633]
|
||||
];
|
||||
var DTMF_Chars = [
|
||||
["1", "2", "3", "A"],
|
||||
["4", "5", "6", "B"],
|
||||
["7", "8", "9", "C"],
|
||||
["*", "0", "#", "D"],
|
||||
];
|
||||
var FindIndex=function(freq, freqs, freqStep){
|
||||
var idx=-1,idxb=1000;
|
||||
for(var i=0;i<freqs.length;i++){
|
||||
var xb=Math.abs(freqs[i]-freq);
|
||||
if(idxb>xb){
|
||||
idxb=xb;
|
||||
if(xb<freqStep*2){//最多2个分辨率内误差
|
||||
idx=i;
|
||||
};
|
||||
};
|
||||
};
|
||||
return idx;
|
||||
};
|
||||
|
||||
}));
|
196
public/extensions/dtmf.encode.js
Normal file
196
public/extensions/dtmf.encode.js
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
录音 Recorder扩展,DTMF(电话拨号按键信号)编码生成器,生成按键对应的音频PCM信号
|
||||
|
||||
本扩展分两个功能:
|
||||
DTMF_Encode
|
||||
DTMF_EncodeMix
|
||||
|
||||
本扩展生成信号代码、原理简单粗暴,纯js实现易于移植,0依赖
|
||||
|
||||
使用场景:DTMF按键信号生成,软电话实时发送DTMF按键信号等
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
本方法用来生成单个按键信号pcm数据,属于底层方法,要混合多个按键信号到别的pcm中请用封装好的DTMF_EncodeMix方法
|
||||
|
||||
参数:
|
||||
key: 单个按键0-9#*
|
||||
sampleRate:123 要生成的pcm采样率
|
||||
duration:100 按键音持续时间
|
||||
mute:50 按键音前后静音时长
|
||||
返回:
|
||||
pcm:[Int16,...],生成单个按键信号
|
||||
**/
|
||||
Recorder.DTMF_Encode=function(key,sampleRate,duration,mute){
|
||||
var durSize=Math.floor(sampleRate*(duration||100)/1000);
|
||||
var muteSize=Math.floor(sampleRate*(mute==null?50:mute)/1000);
|
||||
var pcm0=new Int16Array(durSize+muteSize*2);
|
||||
var pcm1=new Int16Array(durSize+muteSize*2);
|
||||
|
||||
// https://github.com/watilde/node-dtfm/blob/master/encode.js
|
||||
var f0=DTMF_Freqs[key][0];
|
||||
var f1=DTMF_Freqs[key][1];
|
||||
var vol=0.3;
|
||||
for(var i=0;i<durSize;i++){
|
||||
var v0=vol*Math.sin((2 * Math.PI) * f0 * (i / sampleRate));
|
||||
var v1=vol*Math.sin((2 * Math.PI) * f1 * (i / sampleRate));
|
||||
pcm0[i+muteSize]=Math.max(-1,Math.min(1,v0))*0x7FFF;
|
||||
pcm1[i+muteSize]=Math.max(-1,Math.min(1,v1))*0x7FFF;
|
||||
};
|
||||
|
||||
//简单叠加 低群 和 高群 信号
|
||||
Mix(pcm0,0,pcm1,0);
|
||||
return pcm0;
|
||||
};
|
||||
|
||||
|
||||
/**返回EncodeMix对象,将输入的按键信号混合到持续输入的pcm流中,当.mix(inputPcms)提供的太短的pcm会无法完整放下一个完整的按键信号,所以需要不停调用.mix(inputPcms)进行混合**/
|
||||
Recorder.DTMF_EncodeMix=function(set){
|
||||
return new EncodeMix(set);
|
||||
};
|
||||
var EncodeMix=function(set){
|
||||
var This=this;
|
||||
This.set={
|
||||
duration:100 //按键信号持续时间 ms,最小值为30ms
|
||||
,mute:25 //按键音前后静音时长 ms,取值为0也是可以的
|
||||
,interval:200 //两次按键信号间隔时长 ms,间隔内包含了duration+mute*2,最小值为120ms
|
||||
};
|
||||
for(var k in set){
|
||||
This.set[k]=set[k];
|
||||
};
|
||||
|
||||
This.keys="";
|
||||
This.idx=0;
|
||||
This.state={keyIdx:-1,skip:0};
|
||||
};
|
||||
EncodeMix.prototype={
|
||||
/** 添加一个按键或多个按键 "0" "123#*",后面慢慢通过mix方法混合到pcm中,无返回值 **/
|
||||
add:function(keys){
|
||||
this.keys+=keys;
|
||||
}
|
||||
/** 将已添加的按键信号混合到pcm中,pcms:[[Int16,...],...]二维数组,sampleRate:pcm的采样率,index:pcms第一维开始索引,将从这个pcm开始混合。
|
||||
返回混合状态对象。
|
||||
注意:调用本方法会修改pcms中的内容,因此混合结果就在pcms内。 **/
|
||||
,mix:function(pcms,sampleRate,index){
|
||||
index||(index=0);
|
||||
var This=this,set=This.set;
|
||||
var newEncodes=[];
|
||||
|
||||
var state=This.state;
|
||||
var pcmPos=0;
|
||||
loop:
|
||||
for(var i0=index;i0<pcms.length;i0++){
|
||||
var pcm=pcms[i0];
|
||||
|
||||
var key=This.keys.charAt(This.idx);
|
||||
if(!key){//没有需要处理的按键,把间隔消耗掉
|
||||
state.skip=Math.max(0, state.skip-pcm.length);
|
||||
} else while(key){
|
||||
//按键间隔处理
|
||||
if(state.skip){
|
||||
var op=pcm.length-pcmPos;
|
||||
if(op<=state.skip){
|
||||
state.skip-=op;
|
||||
pcmPos=0;
|
||||
continue loop;
|
||||
};
|
||||
pcmPos+=state.skip;
|
||||
state.skip=0;
|
||||
};
|
||||
|
||||
var keyPcm=state.keyPcm;
|
||||
|
||||
//这个key已经混合过,看看有没有剩余的信号
|
||||
if(state.keyIdx==This.idx){
|
||||
if(state.cur>=keyPcm.length){
|
||||
state.keyIdx=-1;
|
||||
};
|
||||
};
|
||||
//新的key,生成信号
|
||||
if(state.keyIdx!=This.idx){
|
||||
keyPcm=Recorder.DTMF_Encode(key,sampleRate,set.duration,set.mute);
|
||||
state.keyIdx=This.idx;
|
||||
state.cur=0;
|
||||
state.keyPcm=keyPcm;
|
||||
|
||||
newEncodes.push({
|
||||
key:key
|
||||
,data:keyPcm
|
||||
});
|
||||
};
|
||||
|
||||
//将keyPcm混合到当前pcm中,实际是替换逻辑
|
||||
var res=Mix(pcm,pcmPos,keyPcm,state.cur,true);
|
||||
state.cur=res.cur;
|
||||
pcmPos=res.last;
|
||||
|
||||
//下一个按键
|
||||
if(res.cur>=keyPcm.length){
|
||||
This.idx++;
|
||||
key=This.keys.charAt(This.idx);
|
||||
state.skip=Math.floor(sampleRate*(set.interval-set.duration-set.mute*2)/1000);
|
||||
};
|
||||
|
||||
//当前pcm的位置已消耗完
|
||||
if(res.last>=pcm.length){
|
||||
pcmPos=0;
|
||||
continue loop;//下一个pcm
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
newEncodes:newEncodes //本次混合新生成的按键信号列表 [{key:"*",data:[Int16,...]},...],如果没有产生新信号将为空数组
|
||||
,hasNext:This.idx<This.keys.length //是否还有未混合完的信号
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
//teach.realtime.mix_multiple 抄过来的简单混合算法
|
||||
var Mix=function(buffer,pos1,add,pos2,mute){
|
||||
for(var j=pos1,cur=pos2;;j++,cur++){
|
||||
if(j>=buffer.length || cur>=add.length){
|
||||
return {
|
||||
last:j
|
||||
,cur:cur
|
||||
};
|
||||
};
|
||||
|
||||
if(mute){
|
||||
buffer[j]=0;//置为0即为静音
|
||||
};
|
||||
var data_mix,data1=buffer[j],data2=add[cur];
|
||||
|
||||
//简单混音算法 https://blog.csdn.net/dancing_night/article/details/53080819
|
||||
if(data1<0 && data2<0){
|
||||
data_mix = data1+data2 - (data1 * data2 / -0x7FFF);
|
||||
}else{
|
||||
data_mix = data1+data2 - (data1 * data2 / 0x7FFF);
|
||||
};
|
||||
|
||||
buffer[j]=data_mix;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
var DTMF_Freqs={
|
||||
'1': [697, 1209] ,'2': [697, 1336] ,'3': [697, 1477] ,'A': [697, 1633]
|
||||
,'4': [770, 1209] ,'5': [770, 1336] ,'6': [770, 1477] ,'B': [770, 1633]
|
||||
,'7': [852, 1209] ,'8': [852, 1336] ,'9': [852, 1477] ,'C': [852, 1633]
|
||||
,'*': [941, 1209] ,'0': [941, 1336] ,'#': [941, 1477] ,'D': [941, 1633]
|
||||
};
|
||||
|
||||
|
||||
}));
|
377
public/extensions/frequency.histogram.view.js
Normal file
377
public/extensions/frequency.histogram.view.js
Normal file
@ -0,0 +1,377 @@
|
||||
/*
|
||||
录音 Recorder扩展,频率直方图显示
|
||||
使用本扩展需要引入src/extensions/lib.fft.js支持,直方图特意优化主要显示0-5khz语音部分(线性),其他高频显示区域较小,不适合用来展示音乐频谱,可通过配置fullFreq来恢复成完整的线性频谱,或自行修改源码修改成倍频程频谱(伯德图、对数频谱);本可视化插件可以移植到其他语言环境,如需定制可联系作者
|
||||
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
本扩展核心算法主要参考了Java开源库jmp123 版本0.3 的代码:
|
||||
https://www.iteye.com/topic/851459
|
||||
https://sourceforge.net/projects/jmp123/files/
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
var FrequencyHistogramView=function(set){
|
||||
return new fn(set);
|
||||
};
|
||||
var ViewTxt="FrequencyHistogramView";
|
||||
var fn=function(set){
|
||||
var This=this;
|
||||
var o={
|
||||
/*
|
||||
elem:"css selector" //自动显示到dom,并以此dom大小为显示大小
|
||||
//或者配置显示大小,手动把frequencyObj.elem显示到别的地方
|
||||
,width:0 //显示宽度
|
||||
,height:0 //显示高度
|
||||
|
||||
H5环境以上配置二选一
|
||||
|
||||
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象,需支持getContext("2d"),支持设置width、height,支持drawImage(canvas,...)
|
||||
,width:0 //canvas显示宽度
|
||||
,height:0 //canvas显示高度
|
||||
非H5环境使用以上配置
|
||||
*/
|
||||
|
||||
scale:2 //缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
|
||||
|
||||
,fps:20 //绘制帧率,不可过高
|
||||
|
||||
,lineCount:30 //直方图柱子数量,数量的多少对性能影响不大,密集运算集中在FFT算法中
|
||||
,widthRatio:0.6 //柱子线条宽度占比,为所有柱子占用整个视图宽度的比例,剩下的空白区域均匀插入柱子中间;默认值也基本相当于一根柱子占0.6,一根空白占0.4;设为1不留空白,当视图不足容下所有柱子时也不留空白
|
||||
,spaceWidth:0 //柱子间空白固定基础宽度,柱子宽度自适应,当不为0时widthRatio无效,当视图不足容下所有柱子时将不会留空白,允许为负数,让柱子发生重叠
|
||||
,minHeight:0 //柱子保留基础高度,position不为±1时应该保留点高度
|
||||
,position:-1 //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,小数为百分比
|
||||
,mirrorEnable:false //是否启用镜像,如果启用,视图宽度会分成左右两块,右边这块进行绘制,左边这块进行镜像(以中间这根柱子的中心进行镜像)
|
||||
|
||||
,stripeEnable:true //是否启用柱子顶上的峰值小横条,position不是-1时应当关闭,否则会很丑
|
||||
,stripeHeight:3 //峰值小横条基础高度
|
||||
,stripeMargin:6 //峰值小横条和柱子保持的基础距离
|
||||
|
||||
,fallDuration:1000 //柱子从最顶上下降到最底部最长时间ms
|
||||
,stripeFallDuration:3500 //峰值小横条从最顶上下降到底部最长时间ms
|
||||
|
||||
//柱子颜色配置:[位置,css颜色,...] 位置: 取值0.0-1.0之间
|
||||
,linear:[0,"rgba(0,187,17,1)",0.5,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"]
|
||||
//峰值小横条渐变颜色配置,取值格式和linear一致,留空为柱子的渐变颜色
|
||||
,stripeLinear:null
|
||||
|
||||
,shadowBlur:0 //柱子阴影基础大小,设为0不显示阴影,如果柱子数量太多时请勿开启,非常影响性能
|
||||
,shadowColor:"#bbb" //柱子阴影颜色
|
||||
,stripeShadowBlur:-1 //峰值小横条阴影基础大小,设为0不显示阴影,-1为柱子的大小,如果柱子数量太多时请勿开启,非常影响性能
|
||||
,stripeShadowColor:"" //峰值小横条阴影颜色,留空为柱子的阴影颜色
|
||||
|
||||
,fullFreq:false //是否要绘制所有频率;默认false主要绘制5khz以下的频率,高频部分占比很少,此时不同的采样率对频谱显示几乎没有影响;设为true后不同采样率下显示的频谱是不一样的,因为 最大频率=采样率/2 会有差异
|
||||
//当发生绘制时会回调此方法,参数为当前绘制的频率数据和采样率,可实现多个直方图同时绘制,只消耗一个input输入和计算时间
|
||||
,onDraw:function(frequencyData,sampleRate){}
|
||||
};
|
||||
for(var k in set){
|
||||
o[k]=set[k];
|
||||
};
|
||||
This.set=set=o;
|
||||
|
||||
var cCanvas="compatibleCanvas";
|
||||
if(set[cCanvas]){
|
||||
var canvas=This.canvas=set[cCanvas];
|
||||
}else{
|
||||
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
|
||||
var elem=set.elem;
|
||||
if(elem){
|
||||
if(typeof(elem)=="string"){
|
||||
elem=document.querySelector(elem);
|
||||
}else if(elem.length){
|
||||
elem=elem[0];
|
||||
};
|
||||
};
|
||||
if(elem){
|
||||
set.width=elem.offsetWidth;
|
||||
set.height=elem.offsetHeight;
|
||||
};
|
||||
|
||||
var thisElem=This.elem=document.createElement("div");
|
||||
thisElem.style.fontSize=0;
|
||||
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
|
||||
|
||||
var canvas=This.canvas=thisElem.querySelector("canvas");
|
||||
|
||||
if(elem){
|
||||
elem.innerHTML="";
|
||||
elem.appendChild(thisElem);
|
||||
};
|
||||
};
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var height=set.height*scale;
|
||||
if(!width || !height){
|
||||
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
|
||||
};
|
||||
|
||||
canvas.width=width;
|
||||
canvas.height=height;
|
||||
var ctx=This.ctx=canvas.getContext("2d");
|
||||
|
||||
if(!Recorder.LibFFT){
|
||||
throw new Error($T.G("NeedImport-2",[ViewTxt,"src/extensions/lib.fft.js"]));
|
||||
};
|
||||
This.fft=Recorder.LibFFT(1024);
|
||||
|
||||
//柱子所在高度
|
||||
This.lastH=[];
|
||||
//峰值小横条所在高度
|
||||
This.stripesH=[];
|
||||
};
|
||||
fn.prototype=FrequencyHistogramView.prototype={
|
||||
genLinear:function(ctx,colors,from,to){
|
||||
var rtv=ctx.createLinearGradient(0,from,0,to);
|
||||
for(var i=0;i<colors.length;){
|
||||
rtv.addColorStop(colors[i++],colors[i++]);
|
||||
};
|
||||
return rtv;
|
||||
}
|
||||
,input:function(pcmData,powerLevel,sampleRate){
|
||||
var This=this;
|
||||
This.sampleRate=sampleRate;
|
||||
This.pcmData=pcmData;
|
||||
This.pcmPos=0;
|
||||
|
||||
This.inputTime=Date.now();
|
||||
This.schedule();
|
||||
}
|
||||
,schedule:function(){
|
||||
var This=this,set=This.set;
|
||||
var interval=Math.floor(1000/set.fps);
|
||||
if(!This.timer){
|
||||
This.timer=setInterval(function(){
|
||||
This.schedule();
|
||||
},interval);
|
||||
};
|
||||
|
||||
var now=Date.now();
|
||||
var drawTime=This.drawTime||0;
|
||||
if(now-This.inputTime>set.stripeFallDuration*1.3){
|
||||
//超时没有输入,顶部横条已全部落下,干掉定时器
|
||||
clearInterval(This.timer);
|
||||
This.timer=0;
|
||||
|
||||
This.lastH=[];//重置高度再绘制一次,避免定时不准没到底就停了
|
||||
This.stripesH=[];
|
||||
This.draw(null,This.sampleRate);
|
||||
return;
|
||||
};
|
||||
if(now-drawTime<interval){
|
||||
//没到间隔时间,不绘制
|
||||
return;
|
||||
};
|
||||
This.drawTime=now;
|
||||
|
||||
//调用FFT计算频率数据
|
||||
var bufferSize=This.fft.bufferSize;
|
||||
var pcm=This.pcmData;
|
||||
var pos=This.pcmPos;
|
||||
var arr=new Int16Array(bufferSize);
|
||||
for(var i=0;i<bufferSize&&pos<pcm.length;i++,pos++){
|
||||
arr[i]=pcm[pos];
|
||||
};
|
||||
This.pcmPos=pos;
|
||||
|
||||
var frequencyData=This.fft.transform(arr);
|
||||
|
||||
//推入绘制
|
||||
This.draw(frequencyData,This.sampleRate);
|
||||
}
|
||||
,draw:function(frequencyData,sampleRate){
|
||||
var This=this,set=This.set;
|
||||
var ctx=This.ctx;
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var height=set.height*scale;
|
||||
var lineCount=set.lineCount;
|
||||
var bufferSize=This.fft.bufferSize;
|
||||
|
||||
|
||||
//计算高度位置
|
||||
var position=set.position;
|
||||
var posAbs=Math.abs(set.position);
|
||||
var originY=position==1?0:height;//y轴原点
|
||||
var heightY=height;//最高的一边高度
|
||||
if(posAbs<1){
|
||||
heightY=heightY/2;
|
||||
originY=heightY;
|
||||
heightY=Math.floor(heightY*(1+posAbs));
|
||||
originY=Math.floor(position>0?originY*(1-posAbs):originY*(1+posAbs));
|
||||
};
|
||||
|
||||
var lastH=This.lastH;
|
||||
var stripesH=This.stripesH;
|
||||
var speed=Math.ceil(heightY/(set.fallDuration/(1000/set.fps)));
|
||||
var stripeSpeed=Math.ceil(heightY/(set.stripeFallDuration/(1000/set.fps)));
|
||||
var stripeMargin=set.stripeMargin*scale;
|
||||
|
||||
var Y0=1 << (Math.round(Math.log(bufferSize)/Math.log(2) + 3) << 1);
|
||||
var logY0 = Math.log(Y0)/Math.log(10);
|
||||
var dBmax=20*Math.log(0x7fff)/Math.log(10);
|
||||
|
||||
var fftSize=bufferSize/2,fftSize5k=fftSize;
|
||||
if(!set.fullFreq){//非绘制所有频率时,计算5khz所在位置,8000采样率及以下最高只有4khz
|
||||
fftSize5k=Math.min(fftSize,Math.floor(fftSize*5000/(sampleRate/2)));
|
||||
}
|
||||
var isFullFreq=fftSize5k==fftSize;
|
||||
var line80=isFullFreq?lineCount:Math.round(lineCount*0.8);//80%的柱子位置
|
||||
var fftSizeStep1=fftSize5k/line80;
|
||||
var fftSizeStep2=isFullFreq?0:(fftSize-fftSize5k)/(lineCount-line80);
|
||||
var fftIdx=0;
|
||||
for(var i=0;i<lineCount;i++){
|
||||
// !fullFreq 时不采用jmp123的非线性划分频段,录音语音并不适用于音乐的频率,应当弱化高频部分
|
||||
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。
|
||||
var start=Math.ceil(fftIdx);
|
||||
if(i<line80){
|
||||
//5khz以下
|
||||
fftIdx+=fftSizeStep1;
|
||||
}else{
|
||||
//5khz以上
|
||||
fftIdx+=fftSizeStep2;
|
||||
};
|
||||
var end=Math.ceil(fftIdx); if(end==start)end++;
|
||||
end=Math.min(end,fftSize);
|
||||
|
||||
|
||||
//参考AudioGUI.java .drawHistogram方法
|
||||
|
||||
//查找当前频段的最大"幅值"
|
||||
var maxAmp=0;
|
||||
if(frequencyData){
|
||||
for (var j=start; j<end; j++) {
|
||||
maxAmp=Math.max(maxAmp,Math.abs(frequencyData[j]));
|
||||
};
|
||||
};
|
||||
|
||||
//计算音量
|
||||
var dB= (maxAmp > Y0) ? Math.floor((Math.log(maxAmp)/Math.log(10) - logY0) * 17) : 0;
|
||||
var h=heightY*Math.min(dB/dBmax,1);
|
||||
|
||||
//使柱子匀速下降
|
||||
lastH[i]=(lastH[i]||0)-speed;
|
||||
if(h<lastH[i]){h=lastH[i];};
|
||||
if(h<0){h=0;};
|
||||
lastH[i]=h;
|
||||
|
||||
var shi=stripesH[i]||0;
|
||||
if(h&&h+stripeMargin>shi) {
|
||||
stripesH[i]=h+stripeMargin;
|
||||
}else{
|
||||
//使峰值小横条匀速度下落
|
||||
var sh =shi-stripeSpeed;
|
||||
if(sh < 0){sh = 0;};
|
||||
stripesH[i] = sh;
|
||||
};
|
||||
};
|
||||
|
||||
//开始绘制图形
|
||||
ctx.clearRect(0,0,width,height);
|
||||
|
||||
var linear1=This.genLinear(ctx,set.linear,originY,originY-heightY);//上半部分的填充
|
||||
var stripeLinear1=set.stripeLinear&&This.genLinear(ctx,set.stripeLinear,originY,originY-heightY)||linear1;//上半部分的峰值小横条填充
|
||||
|
||||
var linear2=This.genLinear(ctx,set.linear,originY,originY+heightY);//下半部分的填充
|
||||
var stripeLinear2=set.stripeLinear&&This.genLinear(ctx,set.stripeLinear,originY,originY+heightY)||linear2;//上半部分的峰值小横条填充
|
||||
|
||||
//计算柱子间距
|
||||
var mirrorEnable=set.mirrorEnable;
|
||||
var mirrorCount=mirrorEnable?lineCount*2-1:lineCount;//镜像柱子数量翻一倍-1根
|
||||
|
||||
var widthRatio=set.widthRatio;
|
||||
var spaceWidth=set.spaceWidth*scale;
|
||||
if(spaceWidth!=0){
|
||||
widthRatio=(width-spaceWidth*(mirrorCount+1))/width;
|
||||
};
|
||||
|
||||
for(var i=0;i<2;i++){
|
||||
var lineFloat=Math.max(1*scale,(width*widthRatio)/mirrorCount);//柱子宽度至少1个单位
|
||||
var lineWN=Math.floor(lineFloat),lineWF=lineFloat-lineWN;//提取出小数部分
|
||||
var spaceFloat=(width-mirrorCount*lineFloat)/(mirrorCount+1);//均匀间隔,首尾都留空,可能为负数,柱子将发生重叠
|
||||
if(spaceFloat>0 && spaceFloat<1){
|
||||
widthRatio=1; spaceFloat=0; //不够一个像素,丢弃不绘制间隔,重新计算
|
||||
}else break;
|
||||
};
|
||||
|
||||
//绘制
|
||||
var minHeight=set.minHeight*scale;
|
||||
var XFloat=mirrorEnable?(width-lineWN)/2-spaceFloat:0;//镜像时,中间柱子位于正中心
|
||||
for(var iMirror=0;iMirror<2;iMirror++){
|
||||
if(iMirror){ ctx.save(); ctx.scale(-1,1); }
|
||||
var xMirror=iMirror?width:0; //绘制镜像部分,不用drawImage(canvas)进行镜像绘制,提升兼容性(iOS微信小程序bug https://developers.weixin.qq.com/community/develop/doc/000aaca2148dc8a235a0fb8c66b000)
|
||||
|
||||
//绘制柱子
|
||||
ctx.shadowBlur=set.shadowBlur*scale;
|
||||
ctx.shadowColor=set.shadowColor;
|
||||
for(var i=0,xFloat=XFloat,wFloat=0,x,y,w,h;i<lineCount;i++){
|
||||
xFloat+=spaceFloat;
|
||||
x=Math.floor(xFloat)-xMirror;
|
||||
w=lineWN; wFloat+=lineWF; if(wFloat>=1){ w++; wFloat--; } //小数凑够1像素
|
||||
h=Math.max(lastH[i],minHeight);
|
||||
|
||||
//绘制上半部分
|
||||
if(originY!=0){
|
||||
y=originY-h;
|
||||
ctx.fillStyle=linear1;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
};
|
||||
//绘制下半部分
|
||||
if(originY!=height){
|
||||
ctx.fillStyle=linear2;
|
||||
ctx.fillRect(x, originY, w, h);
|
||||
};
|
||||
|
||||
xFloat+=w;
|
||||
};
|
||||
|
||||
//绘制柱子顶上峰值小横条
|
||||
if(set.stripeEnable){
|
||||
var stripeShadowBlur=set.stripeShadowBlur;
|
||||
ctx.shadowBlur=(stripeShadowBlur==-1?set.shadowBlur:stripeShadowBlur)*scale;
|
||||
ctx.shadowColor=set.stripeShadowColor||set.shadowColor;
|
||||
var stripeHeight=set.stripeHeight*scale;
|
||||
for(var i=0,xFloat=XFloat,wFloat=0,x,y,w,h;i<lineCount;i++){
|
||||
xFloat+=spaceFloat;
|
||||
x=Math.floor(xFloat)-xMirror;
|
||||
w=lineWN; wFloat+=lineWF; if(wFloat>=1){ w++; wFloat--; } //小数凑够1像素
|
||||
h=stripesH[i];
|
||||
|
||||
//绘制上半部分
|
||||
if(originY!=0){
|
||||
y=originY-h-stripeHeight;
|
||||
if(y<0){y=0;};
|
||||
ctx.fillStyle=stripeLinear1;
|
||||
ctx.fillRect(x, y, w, stripeHeight);
|
||||
};
|
||||
//绘制下半部分
|
||||
if(originY!=height){
|
||||
y=originY+h;
|
||||
if(y+stripeHeight>height){
|
||||
y=height-stripeHeight;
|
||||
};
|
||||
ctx.fillStyle=stripeLinear2;
|
||||
ctx.fillRect(x, y, w, stripeHeight);
|
||||
};
|
||||
|
||||
xFloat+=w;
|
||||
};
|
||||
};
|
||||
|
||||
if(iMirror){ ctx.restore(); }
|
||||
if(!mirrorEnable) break;
|
||||
};
|
||||
|
||||
if(frequencyData){
|
||||
set.onDraw(frequencyData,sampleRate);
|
||||
};
|
||||
}
|
||||
};
|
||||
Recorder[ViewTxt]=FrequencyHistogramView;
|
||||
|
||||
|
||||
}));
|
118
public/extensions/lib.fft.js
Normal file
118
public/extensions/lib.fft.js
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
时域转频域,快速傅里叶变换(FFT)
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
var fft=Recorder.LibFFT(bufferSize)
|
||||
bufferSize取值2的n次方
|
||||
|
||||
fft.bufferSize 实际采用的bufferSize
|
||||
fft.transform(inBuffer)
|
||||
inBuffer:[Int16,...] 数组长度必须是bufferSize
|
||||
返回[Float64(Long),...],长度为bufferSize/2
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
从FFT.java 移植,Java开源库:jmp123 版本0.3
|
||||
https://www.iteye.com/topic/851459
|
||||
https://sourceforge.net/projects/jmp123/files/
|
||||
*/
|
||||
Recorder.LibFFT=function(bufferSize){
|
||||
var FFT_N_LOG,FFT_N,MINY;
|
||||
var real, imag, sintable, costable;
|
||||
var bitReverse;
|
||||
|
||||
var FFT_Fn=function(bufferSize) {//bufferSize只能取值2的n次方
|
||||
FFT_N_LOG=Math.round(Math.log(bufferSize)/Math.log(2));
|
||||
FFT_N = 1 << FFT_N_LOG;
|
||||
MINY = ((FFT_N << 2) * Math.sqrt(2));
|
||||
|
||||
real = [];
|
||||
imag = [];
|
||||
sintable = [0];
|
||||
costable = [0];
|
||||
bitReverse = [];
|
||||
|
||||
var i, j, k, reve;
|
||||
for (i = 0; i < FFT_N; i++) {
|
||||
k = i;
|
||||
for (j = 0, reve = 0; j != FFT_N_LOG; j++) {
|
||||
reve <<= 1;
|
||||
reve |= (k & 1);
|
||||
k >>>= 1;
|
||||
}
|
||||
bitReverse[i] = reve;
|
||||
}
|
||||
|
||||
var theta, dt = 2 * Math.PI / FFT_N;
|
||||
for (i = (FFT_N >> 1) - 1; i > 0; i--) {
|
||||
theta = i * dt;
|
||||
costable[i] = Math.cos(theta);
|
||||
sintable[i] = Math.sin(theta);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
用于频谱显示的快速傅里叶变换
|
||||
inBuffer 输入FFT_N个实数,返回 FFT_N/2个输出值(复数模的平方)。
|
||||
*/
|
||||
var getModulus=function(inBuffer) {
|
||||
var i, j, k, ir, j0 = 1, idx = FFT_N_LOG - 1;
|
||||
var cosv, sinv, tmpr, tmpi;
|
||||
for (i = 0; i != FFT_N; i++) {
|
||||
real[i] = inBuffer[bitReverse[i]];
|
||||
imag[i] = 0;
|
||||
}
|
||||
|
||||
for (i = FFT_N_LOG; i != 0; i--) {
|
||||
for (j = 0; j != j0; j++) {
|
||||
cosv = costable[j << idx];
|
||||
sinv = sintable[j << idx];
|
||||
for (k = j; k < FFT_N; k += j0 << 1) {
|
||||
ir = k + j0;
|
||||
tmpr = cosv * real[ir] - sinv * imag[ir];
|
||||
tmpi = cosv * imag[ir] + sinv * real[ir];
|
||||
real[ir] = real[k] - tmpr;
|
||||
imag[ir] = imag[k] - tmpi;
|
||||
real[k] += tmpr;
|
||||
imag[k] += tmpi;
|
||||
}
|
||||
}
|
||||
j0 <<= 1;
|
||||
idx--;
|
||||
}
|
||||
|
||||
j = FFT_N >> 1;
|
||||
var outBuffer=new Float64Array(j);
|
||||
/*
|
||||
* 输出模的平方:
|
||||
* for(i = 1; i <= j; i++)
|
||||
* inBuffer[i-1] = real[i] * real[i] + imag[i] * imag[i];
|
||||
*
|
||||
* 如果FFT只用于频谱显示,可以"淘汰"幅值较小的而减少浮点乘法运算. MINY的值
|
||||
* 和Spectrum.Y0,Spectrum.logY0对应.
|
||||
*/
|
||||
sinv = MINY;
|
||||
cosv = -MINY;
|
||||
for (i = j; i != 0; i--) {
|
||||
tmpr = real[i];
|
||||
tmpi = imag[i];
|
||||
if (tmpr > cosv && tmpr < sinv && tmpi > cosv && tmpi < sinv)
|
||||
outBuffer[i - 1] = 0;
|
||||
else
|
||||
outBuffer[i - 1] = Math.round(tmpr * tmpr + tmpi * tmpi);
|
||||
}
|
||||
return outBuffer;
|
||||
}
|
||||
|
||||
FFT_Fn(bufferSize);
|
||||
return {transform:getModulus,bufferSize:FFT_N};
|
||||
};
|
||||
|
||||
}));
|
1155
public/extensions/sonic.js
Normal file
1155
public/extensions/sonic.js
Normal file
File diff suppressed because it is too large
Load Diff
278
public/extensions/wavesurfer.view.js
Normal file
278
public/extensions/wavesurfer.view.js
Normal file
@ -0,0 +1,278 @@
|
||||
/*
|
||||
录音 Recorder扩展,音频可视化波形显示
|
||||
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
外观和名称来源于:
|
||||
https://github.com/katspaugh/wavesurfer.js https://github.com/collab-project/videojs-record
|
||||
|
||||
本扩展的波形绘制直接简单的使用PCM的采样数值大小来进行线条的绘制,同一段音频绘制出的波形和Audition内显示的波形外观上几乎没有差异。
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
var WaveSurferView=function(set){
|
||||
return new fn(set);
|
||||
};
|
||||
var ViewTxt="WaveSurferView";
|
||||
var fn=function(set){
|
||||
var This=this;
|
||||
var o={
|
||||
/*
|
||||
elem:"css selector" //自动显示到dom,并以此dom大小为显示大小
|
||||
//或者配置显示大小,手动把surferObj.elem显示到别的地方
|
||||
,width:0 //显示宽度
|
||||
,height:0 //显示高度
|
||||
|
||||
H5环境以上配置二选一
|
||||
|
||||
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象,需支持getContext("2d"),支持设置width、height,支持drawImage(canvas,...)
|
||||
,compatibleCanvas_2x: CanvasObject //提供一个宽度是compatibleCanvas的2倍canvas对象
|
||||
,width:0 //canvas显示宽度
|
||||
,height:0 //canvas显示高度
|
||||
非H5环境使用以上配置
|
||||
*/
|
||||
|
||||
scale:2 //缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
|
||||
|
||||
,fps:50 //绘制帧率,不可过高,50-60fps运动性质动画明显会流畅舒适,实际显示帧率达不到这个值也并无太大影响
|
||||
|
||||
,duration:2500 //当前视图窗口内最大绘制的波形的持续时间,此处决定了移动速率
|
||||
,direction:1 //波形前进方向,取值:1由左往右,-1由右往左
|
||||
,position:0 //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,小数为百分比
|
||||
|
||||
,centerHeight:1 //中线基础粗细,如果为0不绘制中线,position=±1时应当设为0
|
||||
|
||||
//波形颜色配置:[位置,css颜色,...] 位置: 取值0.0-1.0之间
|
||||
,linear:[0,"rgba(0,187,17,1)",0.7,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"]
|
||||
,centerColor:"" //中线css颜色,留空取波形第一个渐变颜色
|
||||
};
|
||||
for(var k in set){
|
||||
o[k]=set[k];
|
||||
};
|
||||
This.set=set=o;
|
||||
|
||||
var cCanvas="compatibleCanvas";
|
||||
if(set[cCanvas]){
|
||||
var canvas=This.canvas=set[cCanvas];
|
||||
var canvas2=This.canvas2=set[cCanvas+"_2x"];
|
||||
}else{
|
||||
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
|
||||
var elem=set.elem;
|
||||
if(elem){
|
||||
if(typeof(elem)=="string"){
|
||||
elem=document.querySelector(elem);
|
||||
}else if(elem.length){
|
||||
elem=elem[0];
|
||||
};
|
||||
};
|
||||
if(elem){
|
||||
set.width=elem.offsetWidth;
|
||||
set.height=elem.offsetHeight;
|
||||
};
|
||||
|
||||
var thisElem=This.elem=document.createElement("div");
|
||||
thisElem.style.fontSize=0;
|
||||
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
|
||||
|
||||
var canvas=This.canvas=thisElem.querySelector("canvas");
|
||||
var canvas2=This.canvas2=document.createElement("canvas");
|
||||
|
||||
if(elem){
|
||||
elem.innerHTML="";
|
||||
elem.appendChild(thisElem);
|
||||
};
|
||||
};
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var height=set.height*scale;
|
||||
if(!width || !height){
|
||||
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
|
||||
};
|
||||
|
||||
canvas.width=width;
|
||||
canvas.height=height;
|
||||
var ctx=This.ctx=canvas.getContext("2d");
|
||||
|
||||
canvas2.width=width*2;//卷轴,后台绘制画布能容纳两块窗口内容,进行无缝滚动
|
||||
canvas2.height=height;
|
||||
var ctx2=This.ctx2=canvas2.getContext("2d");
|
||||
|
||||
This.x=0;
|
||||
};
|
||||
fn.prototype=WaveSurferView.prototype={
|
||||
genLinear:function(ctx,colors,from,to){
|
||||
var rtv=ctx.createLinearGradient(0,from,0,to);
|
||||
for(var i=0;i<colors.length;){
|
||||
rtv.addColorStop(colors[i++],colors[i++]);
|
||||
};
|
||||
return rtv;
|
||||
}
|
||||
,input:function(pcmData,powerLevel,sampleRate){
|
||||
var This=this;
|
||||
This.sampleRate=sampleRate;
|
||||
This.pcmData=pcmData;
|
||||
This.pcmPos=0;
|
||||
|
||||
This.inputTime=Date.now();
|
||||
This.schedule();
|
||||
}
|
||||
,schedule:function(){
|
||||
var This=this,set=This.set;
|
||||
var interval=Math.floor(1000/set.fps);
|
||||
if(!This.timer){
|
||||
This.timer=setInterval(function(){
|
||||
This.schedule();
|
||||
},interval);
|
||||
};
|
||||
|
||||
var now=Date.now();
|
||||
var drawTime=This.drawTime||0;
|
||||
if(now-drawTime<interval){
|
||||
//没到间隔时间,不绘制
|
||||
return;
|
||||
};
|
||||
This.drawTime=now;
|
||||
|
||||
//切分当前需要的绘制数据
|
||||
var bufferSize=This.sampleRate/set.fps;
|
||||
var pcm=This.pcmData;
|
||||
var pos=This.pcmPos;
|
||||
var arr=new Int16Array(Math.min(bufferSize,pcm.length-pos));
|
||||
for(var i=0;i<arr.length;i++,pos++){
|
||||
arr[i]=pcm[pos];
|
||||
};
|
||||
This.pcmPos=pos;
|
||||
|
||||
//推入绘制
|
||||
if(arr.length){
|
||||
This.draw(arr,This.sampleRate);
|
||||
}else{
|
||||
if(now-This.inputTime>1300){
|
||||
//超时没有输入,干掉定时器
|
||||
clearInterval(This.timer);
|
||||
This.timer=0;
|
||||
};
|
||||
};
|
||||
}
|
||||
,draw:function(pcmData,sampleRate){
|
||||
var This=this,set=This.set;
|
||||
var ctx=This.ctx2;
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var width2=width*2;
|
||||
var height=set.height*scale;
|
||||
var lineWidth=1*scale;//一条线占用1个单位长度
|
||||
|
||||
//计算高度位置
|
||||
var position=set.position;
|
||||
var posAbs=Math.abs(set.position);
|
||||
var originY=position==1?0:height;//y轴原点
|
||||
var heightY=height;//最高的一边高度
|
||||
if(posAbs<1){
|
||||
heightY=heightY/2;
|
||||
originY=heightY;
|
||||
heightY=Math.floor(heightY*(1+posAbs));
|
||||
originY=Math.floor(position>0?originY*(1-posAbs):originY*(1+posAbs));
|
||||
};
|
||||
|
||||
//计算绘制占用长度
|
||||
var pcmDuration=pcmData.length*1000/sampleRate;
|
||||
var pcmWidth=pcmDuration*width/set.duration;
|
||||
pcmWidth+=This.drawLoss||0;
|
||||
var pointCount=0;
|
||||
if(pcmWidth<lineWidth){
|
||||
This.drawLoss=pcmWidth;
|
||||
//pointCount=0; 不够一根不绘制
|
||||
}else{
|
||||
This.drawLoss=0;
|
||||
pointCount=Math.floor(pcmWidth/lineWidth);
|
||||
};
|
||||
|
||||
//***后台卷轴连续绘制***
|
||||
var linear1=This.genLinear(ctx,set.linear,originY,originY-heightY);//上半部分的填充
|
||||
var linear2=This.genLinear(ctx,set.linear,originY,originY+heightY);//下半部分的填充
|
||||
|
||||
var x=This.x;
|
||||
var step=pcmData.length/pointCount;
|
||||
for(var i=0,idx=0;i<pointCount;i++){
|
||||
var j=Math.floor(idx);
|
||||
var end=Math.floor(idx+step);
|
||||
idx+=step;
|
||||
|
||||
//寻找区间内最大值
|
||||
var max=0;
|
||||
for(;j<end;j++){
|
||||
max=Math.max(max,Math.abs(pcmData[j]));
|
||||
};
|
||||
|
||||
//计算高度
|
||||
var h=heightY*Math.min(1,max/0x7fff);
|
||||
|
||||
//绘制当前线条,不管方向,从x:0往x:max方向画就是了
|
||||
//绘制上半部分
|
||||
if(originY!=0){
|
||||
ctx.fillStyle=linear1;
|
||||
ctx.fillRect(x, originY-h, lineWidth, h);
|
||||
};
|
||||
//绘制下半部分
|
||||
if(originY!=height){
|
||||
ctx.fillStyle=linear2;
|
||||
ctx.fillRect(x, originY, lineWidth, h);
|
||||
};
|
||||
|
||||
x+=lineWidth;
|
||||
//超过卷轴宽度,移动画布第二个窗口内容到第一个窗口
|
||||
if(x>=width2){
|
||||
ctx.clearRect(0,0,width,height);
|
||||
ctx.drawImage(This.canvas2,width,0,width,height,0,0,width,height);
|
||||
ctx.clearRect(width,0,width,height);
|
||||
x=width;
|
||||
};
|
||||
};
|
||||
This.x=x;
|
||||
|
||||
//***画回到显示区域***
|
||||
ctx=This.ctx;
|
||||
ctx.clearRect(0,0,width,height);
|
||||
|
||||
//绘制一条中线
|
||||
var centerHeight=set.centerHeight*scale;
|
||||
if(centerHeight){
|
||||
var y=originY-Math.floor(centerHeight/2);
|
||||
y=Math.max(y,0);
|
||||
y=Math.min(y,height-centerHeight);
|
||||
|
||||
ctx.fillStyle=set.centerColor||set.linear[1];
|
||||
ctx.fillRect(0, y, width, centerHeight);
|
||||
};
|
||||
|
||||
//画回画布
|
||||
var srcX=0,srcW=x,destX=0;
|
||||
if(srcW>width){
|
||||
srcX=srcW-width;
|
||||
srcW=width;
|
||||
}else{
|
||||
destX=width-srcW;
|
||||
};
|
||||
|
||||
var direction=set.direction;
|
||||
if(direction==-1){//由右往左
|
||||
ctx.drawImage(This.canvas2,srcX,0,srcW,height,destX,0,srcW,height);
|
||||
}else{//由左往右
|
||||
ctx.save();
|
||||
ctx.scale(-1,1);
|
||||
ctx.drawImage(This.canvas2,srcX,0,srcW,height,-width+destX,0,srcW,height);
|
||||
ctx.restore();
|
||||
};
|
||||
}
|
||||
};
|
||||
Recorder[ViewTxt]=WaveSurferView;
|
||||
|
||||
|
||||
}));
|
229
public/extensions/waveview.js
Normal file
229
public/extensions/waveview.js
Normal file
@ -0,0 +1,229 @@
|
||||
/*
|
||||
录音 Recorder扩展,动态波形显示
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
var rec=win.Recorder,ni=rec.i18n;
|
||||
factory(rec,ni,ni.$T,browser);
|
||||
}(function(Recorder,i18n,$T,isBrowser){
|
||||
"use strict";
|
||||
|
||||
var WaveView=function(set){
|
||||
return new fn(set);
|
||||
};
|
||||
var ViewTxt="WaveView";
|
||||
var fn=function(set){
|
||||
var This=this;
|
||||
var o={
|
||||
/*
|
||||
elem:"css selector" //自动显示到dom,并以此dom大小为显示大小
|
||||
//或者配置显示大小,手动把waveviewObj.elem显示到别的地方
|
||||
,width:0 //显示宽度
|
||||
,height:0 //显示高度
|
||||
|
||||
H5环境以上配置二选一
|
||||
|
||||
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象,需支持getContext("2d"),支持设置width、height,支持drawImage(canvas,...)
|
||||
,width:0 //canvas显示宽度
|
||||
,height:0 //canvas显示高度
|
||||
非H5环境使用以上配置
|
||||
*/
|
||||
|
||||
scale:2 //缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
|
||||
,speed:9 //移动速度系数,越大越快
|
||||
,phase:21.8 //相位,调整了速度后,调整这个值得到一个看起来舒服的波形
|
||||
|
||||
,fps:20 //绘制帧率,调整后也需调整phase值
|
||||
,keep:true //当停止了input输入时,是否保持波形,设为false停止后将变成一条线
|
||||
|
||||
,lineWidth:3 //线条基础粗细
|
||||
|
||||
//渐变色配置:[位置,css颜色,...] 位置: 取值0.0-1.0之间
|
||||
,linear1:[0,"rgba(150,96,238,1)",0.2,"rgba(170,79,249,1)",1,"rgba(53,199,253,1)"] //线条渐变色1,从左到右
|
||||
,linear2:[0,"rgba(209,130,255,0.6)",1,"rgba(53,199,255,0.6)"] //线条渐变色2,从左到右
|
||||
,linearBg:[0,"rgba(255,255,255,0.2)",1,"rgba(54,197,252,0.2)"] //背景渐变色,从上到下
|
||||
};
|
||||
for(var k in set){
|
||||
o[k]=set[k];
|
||||
};
|
||||
This.set=set=o;
|
||||
|
||||
var cCanvas="compatibleCanvas";
|
||||
if(set[cCanvas]){
|
||||
var canvas=This.canvas=set[cCanvas];
|
||||
}else{
|
||||
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
|
||||
var elem=set.elem;
|
||||
if(elem){
|
||||
if(typeof(elem)=="string"){
|
||||
elem=document.querySelector(elem);
|
||||
}else if(elem.length){
|
||||
elem=elem[0];
|
||||
};
|
||||
};
|
||||
if(elem){
|
||||
set.width=elem.offsetWidth;
|
||||
set.height=elem.offsetHeight;
|
||||
};
|
||||
|
||||
var thisElem=This.elem=document.createElement("div");
|
||||
thisElem.style.fontSize=0;
|
||||
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
|
||||
|
||||
var canvas=This.canvas=thisElem.querySelector("canvas");
|
||||
|
||||
if(elem){
|
||||
elem.innerHTML="";
|
||||
elem.appendChild(thisElem);
|
||||
};
|
||||
};
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var height=set.height*scale;
|
||||
if(!width || !height){
|
||||
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
|
||||
};
|
||||
|
||||
canvas.width=width;
|
||||
canvas.height=height;
|
||||
var ctx=This.ctx=canvas.getContext("2d");
|
||||
|
||||
This.linear1=This.genLinear(ctx,width,set.linear1);
|
||||
This.linear2=This.genLinear(ctx,width,set.linear2);
|
||||
This.linearBg=This.genLinear(ctx,height,set.linearBg,true);
|
||||
|
||||
This._phase=0;
|
||||
};
|
||||
fn.prototype=WaveView.prototype={
|
||||
genLinear:function(ctx,size,colors,top){
|
||||
var rtv=ctx.createLinearGradient(0,0,top?0:size,top?size:0);
|
||||
for(var i=0;i<colors.length;){
|
||||
rtv.addColorStop(colors[i++],colors[i++]);
|
||||
};
|
||||
return rtv;
|
||||
}
|
||||
,genPath:function(frequency,amplitude,phase){
|
||||
//曲线生成算法参考 https://github.com/HaloMartin/MCVoiceWave/blob/f6dc28975fbe0f7fc6cc4dbc2e61b0aa5574e9bc/MCVoiceWave/MCVoiceWaveView.m#L268
|
||||
var rtv=[];
|
||||
var This=this,set=This.set;
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var maxAmplitude=set.height*scale/2;
|
||||
|
||||
for(var x=0;x<=width;x+=scale) {
|
||||
var scaling=(1+Math.cos(Math.PI+(x/width)*2*Math.PI))/2;
|
||||
var y=scaling*maxAmplitude*amplitude*Math.sin(2*Math.PI*(x/width)*frequency+phase)+maxAmplitude;
|
||||
rtv.push(y);
|
||||
}
|
||||
return rtv;
|
||||
}
|
||||
,input:function(pcmData,powerLevel,sampleRate){
|
||||
var This=this;
|
||||
This.sampleRate=sampleRate;
|
||||
This.pcmData=pcmData;
|
||||
This.pcmPos=0;
|
||||
|
||||
This.inputTime=Date.now();
|
||||
This.schedule();
|
||||
}
|
||||
,schedule:function(){
|
||||
var This=this,set=This.set;
|
||||
var interval=Math.floor(1000/set.fps);
|
||||
if(!This.timer){
|
||||
This.timer=setInterval(function(){
|
||||
This.schedule();
|
||||
},interval);
|
||||
};
|
||||
|
||||
var now=Date.now();
|
||||
var drawTime=This.drawTime||0;
|
||||
if(now-drawTime<interval){
|
||||
//没到间隔时间,不绘制
|
||||
return;
|
||||
};
|
||||
This.drawTime=now;
|
||||
|
||||
//切分当前需要的绘制数据
|
||||
var bufferSize=This.sampleRate/set.fps;
|
||||
var pcm=This.pcmData;
|
||||
var pos=This.pcmPos;
|
||||
var len=Math.max(0, Math.min(bufferSize,pcm.length-pos));
|
||||
var sum=0;
|
||||
for(var i=0;i<len;i++,pos++){
|
||||
sum+=Math.abs(pcm[pos]);
|
||||
};
|
||||
This.pcmPos=pos;
|
||||
|
||||
//推入绘制
|
||||
if(len || !set.keep){
|
||||
This.draw(Recorder.PowerLevel(sum, len));
|
||||
}
|
||||
if(!len && now-This.inputTime>1300){
|
||||
//超时没有输入,干掉定时器
|
||||
clearInterval(This.timer);
|
||||
This.timer=0;
|
||||
}
|
||||
}
|
||||
,draw:function(powerLevel){
|
||||
var This=this,set=This.set;
|
||||
var ctx=This.ctx;
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
var height=set.height*scale;
|
||||
|
||||
var speedx=set.speed/set.fps;
|
||||
var phase=This._phase-=speedx;//位移速度
|
||||
var phase2=phase+speedx*set.phase;
|
||||
var amplitude=powerLevel/100;
|
||||
var path1=This.genPath(2,amplitude,phase);
|
||||
var path2=This.genPath(1.8,amplitude,phase2);
|
||||
|
||||
//开始绘制图形
|
||||
ctx.clearRect(0,0,width,height);
|
||||
|
||||
//绘制包围背景
|
||||
ctx.beginPath();
|
||||
for(var i=0,x=0;x<=width;i++,x+=scale) {
|
||||
if (x==0) {
|
||||
ctx.moveTo(x,path1[i]);
|
||||
}else {
|
||||
ctx.lineTo(x,path1[i]);
|
||||
};
|
||||
};
|
||||
i--;
|
||||
for(var x=width-1;x>=0;i--,x-=scale) {
|
||||
ctx.lineTo(x,path2[i]);
|
||||
};
|
||||
ctx.closePath();
|
||||
ctx.fillStyle=This.linearBg;
|
||||
ctx.fill();
|
||||
|
||||
//绘制线
|
||||
This.drawPath(path2,This.linear2);
|
||||
This.drawPath(path1,This.linear1);
|
||||
}
|
||||
,drawPath:function(path,linear){
|
||||
var This=this,set=This.set;
|
||||
var ctx=This.ctx;
|
||||
var scale=set.scale;
|
||||
var width=set.width*scale;
|
||||
|
||||
ctx.beginPath();
|
||||
for(var i=0,x=0;x<=width;i++,x+=scale) {
|
||||
if (x==0) {
|
||||
ctx.moveTo(x,path[i]);
|
||||
}else {
|
||||
ctx.lineTo(x,path[i]);
|
||||
};
|
||||
};
|
||||
ctx.lineWidth=set.lineWidth*scale;
|
||||
ctx.strokeStyle=linear;
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
Recorder[ViewTxt]=WaveView;
|
||||
|
||||
|
||||
}));
|
935
public/i18n/Template.js
Normal file
935
public/i18n/Template.js
Normal file
@ -0,0 +1,935 @@
|
||||
/*
|
||||
Recorder i18n/Template.js
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
Usage: Recorder.i18n.lang="Your-Language-Name" or "your-language"
|
||||
|
||||
Desc: This file is a language translation template file. After copying and renaming, translate the text into the corresponding language. 此文件为语言翻译模板文件,复制并改名后,将文本翻译成对应语言即可。
|
||||
|
||||
注意:请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成,只允许在字符串中填写翻译后的文本,请勿改变代码结构;翻译的文本如果需要明确的空值,请填写"=Empty";文本中的变量用{n}表示(n代表第几个变量),所有变量必须都出现至少一次,如果不要某变量用{n!}表示
|
||||
|
||||
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
factory(win.Recorder,browser);
|
||||
}(function(Recorder,isBrowser){
|
||||
"use strict";
|
||||
var i18n=Recorder.i18n;
|
||||
|
||||
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-1 End @@
|
||||
|
||||
//@@Exec i18n.lang="Your-Language-Name";
|
||||
Recorder.CLog('Import Recorder i18n lang="Your-Language-Name"');
|
||||
|
||||
i18n.alias["Your-Language-Name"]="your-language";
|
||||
|
||||
var putSet={lang:"your-language"};
|
||||
|
||||
i18n.data["rtl$your-language"]=false;
|
||||
i18n.data["desc$your-language"]="This file is a language translation template file. After copying and renaming, translate the text into the corresponding language. 此文件为语言翻译模板文件,复制并改名后,将文本翻译成对应语言即可。";
|
||||
//@@Exec i18n.GenerateDisplayEnglish=true;
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=recorder-core.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@en="Duplicate import {1}"
|
||||
//@@Put0
|
||||
"K8zP:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="剩{1}个GetContext未close"
|
||||
//@@en="There are {1} GetContext unclosed"
|
||||
,"mSxV:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用,否则将在rec.start时尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
//@@en=" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
|
||||
,"nMIy:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="Stream的采样率{1}不等于{2},将进行采样率转换(注意:音质不会变好甚至可能变差),主要在移动端未禁用回声消除时会产生此现象,浏览器有回声消除时可能只会返回16k采样率的音频数据,"
|
||||
//@@en="The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
|
||||
,"eS8i:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="。由于{1}内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启{1}。"
|
||||
//@@en=". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
|
||||
,"ZGlf:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="Connect采用老的{1},"
|
||||
//@@en="Connect uses the old {1}, "
|
||||
,"7TU0:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="但已设置{1}尝试启用{2}"
|
||||
//@@en="But {1} is set trying to enable {2}"
|
||||
,"JwCL:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="可设置{1}尝试启用{2}"
|
||||
//@@en="Can set {1} try to enable {2}"
|
||||
,"VGjB:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}未返回任何音频,恢复使用{2}"
|
||||
//@@en="{1} did not return any audio, reverting to {2}"
|
||||
,"MxX1:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
//@@en="{1} redundant callback"
|
||||
,"XUap:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
|
||||
//@@en="Connect uses {1}, set {2} to restore old-fashioned {3}"
|
||||
,"yOta:"+ //args: {1}-{3}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="(此浏览器不支持{1})"
|
||||
//@@en=" (This browser does not support {1}) "
|
||||
,"VwPd:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}未返回任何音频,降级使用{2}"
|
||||
//@@en="{1} did not return any audio, downgrade to {2}"
|
||||
,"vHnb:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
//@@en="{1} redundant callback"
|
||||
,"O9P7:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
|
||||
//@@en="Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
|
||||
,"LMEm:"+ //args: {1}-{4}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}的filter采样率变了,重设滤波"
|
||||
//@@en="The filter sampleRate of {1} has changed, reset the filter"
|
||||
,"d48C:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}似乎传入了未重置chunk {2}"
|
||||
//@@en="{1} seems to have passed in an unreset chunk {2}"
|
||||
,"tlbC:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}和{2}必须是数值"
|
||||
//@@en="{1} and {2} must be number"
|
||||
,"VtS4:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="录音open失败:"
|
||||
//@@en="Recording open failed: "
|
||||
,"5tWi:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="open被取消"
|
||||
//@@en="open cancelled"
|
||||
,"dFm8:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="open被中断"
|
||||
//@@en="open interrupted"
|
||||
,"VtJO:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",可尝试使用RecordApp解决方案"
|
||||
//@@en=", you can try to use the RecordApp solution "
|
||||
,"EMJq:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="不能录音:"
|
||||
//@@en="Cannot record: "
|
||||
,"A5bm:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="不支持此浏览器从流中获取录音"
|
||||
//@@en="This browser does not support obtaining recordings from stream"
|
||||
,"1iU7:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="从流中打开录音失败:"
|
||||
//@@en="Failed to open recording from stream: "
|
||||
,"BTW2:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="无权录音(跨域,请尝试给iframe添加麦克风访问策略,如{1})"
|
||||
//@@en="No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
|
||||
,"Nclz:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",无可用麦克风"
|
||||
//@@en=", no microphone available"
|
||||
,"jBa9:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="用户拒绝了录音权限"
|
||||
//@@en="User denied recording permission"
|
||||
,"gyO5:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="浏览器禁止不安全页面录音,可开启https解决"
|
||||
//@@en="Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
|
||||
,"oWNo:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="此浏览器不支持录音"
|
||||
//@@en="This browser does not support recording"
|
||||
,"COxc:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="发现同时多次调用open"
|
||||
//@@en="It was found that open was called multiple times at the same time"
|
||||
,"upb8:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="录音功能无效:无音频流"
|
||||
//@@en="Invalid recording: no audio stream"
|
||||
,"Q1GA:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",将尝试禁用回声消除后重试"
|
||||
//@@en=", will try to disable echoCancellation and try again"
|
||||
,"KxE2:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="请求录音权限错误"
|
||||
//@@en="Error requesting recording permission"
|
||||
,"xEQR:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="无法录音:"
|
||||
//@@en="Unable to record: "
|
||||
,"bDOG:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
|
||||
//@@en="Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
|
||||
,"IjL3:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除,移动端未禁用回声消除时可能会降低系统播放音量(关闭录音后可恢复)和仅提供16k采样率的音频流(不需要回声消除时可明确配置成禁用来获得48k高音质的流),请参阅文档中{2}配置"
|
||||
//@@en=", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
|
||||
,"RiWe:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="close被忽略(因为同时open了多个rec,只有最后一个会真正close)"
|
||||
//@@en="close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
|
||||
,"hWVz:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="忽略"
|
||||
//@@en="ignore"
|
||||
,"UHvm:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="不支持{1}架构"
|
||||
//@@en="{1} architecture not supported"
|
||||
,"Essp:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
|
||||
//@@en="{1} type does not support setting takeoffEncodeChunk"
|
||||
,"2XBl:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="(未加载编码器)"
|
||||
//@@en="(Encoder not loaded)"
|
||||
,"LG7e:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}环境不支持实时处理"
|
||||
//@@en="{1} environment does not support real-time processing"
|
||||
,"7uMV:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="补偿{1}ms"
|
||||
//@@en="Compensation {1}ms"
|
||||
,"4Kfd:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未补偿{1}ms"
|
||||
//@@en="Uncompensated {1}ms"
|
||||
,"bM5i:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="回调出错是不允许的,需保证不会抛异常"
|
||||
//@@en="Callback error is not allowed, you need to ensure that no exception will be thrown"
|
||||
,"gFUF:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="低性能,耗时{1}ms"
|
||||
//@@en="Low performance, took {1}ms"
|
||||
,"2ghS:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未进入异步前不能清除buffers"
|
||||
//@@en="Buffers cannot be cleared before entering async"
|
||||
,"ufqH:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="start失败:未open"
|
||||
//@@en="start failed: not open"
|
||||
,"6WmN:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="start 开始录音,"
|
||||
//@@en="start recording, "
|
||||
,"kLDN:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="start被中断"
|
||||
//@@en="start was interrupted"
|
||||
,"Bp2y:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",可能无法录音:"
|
||||
//@@en=", may fail to record: "
|
||||
,"upkE:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="stop 和start时差:"
|
||||
//@@en="Stop and start time difference: "
|
||||
,"Xq4s:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="补偿:"
|
||||
//@@en="compensate: "
|
||||
,"3CQP:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
//@@en="Failed to stop recording: "
|
||||
,"u8JG:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",请设置{1}"
|
||||
//@@en=", please set {1}"
|
||||
,"1skY:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
|
||||
//@@en="Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
|
||||
,"Wv7l:"+ //args: {1}-{3}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}编码器返回的不是{2}"
|
||||
//@@en="{1} encoder returned not {2}"
|
||||
,"Vkbd:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
|
||||
//@@en="After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
|
||||
,"QWnr:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="生成的{1}无效"
|
||||
//@@en="Invalid generated {1}"
|
||||
,"Sz2H:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"wf9t:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh=",开始录音前无用户交互导致AudioContext未运行"
|
||||
//@@en=", No user interaction before starting recording, resulting in AudioContext not running"
|
||||
,"Dl2c:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未采集到录音"
|
||||
//@@en="Recording not captured"
|
||||
,"Ltz3:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
|
||||
//@@en="The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
|
||||
,"xGuI:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="录音错误:"
|
||||
//@@en="Recording error: "
|
||||
,"AxOH:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="音频buffers被释放"
|
||||
//@@en="Audio buffers are released"
|
||||
,"xkKd:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="采样:{1} 花:{2}ms"
|
||||
//@@en="Sampled: {1}, took: {2}ms"
|
||||
,"CxeT:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="非浏览器环境,不支持{1}"
|
||||
//@@en="Non-browser environment, does not support {1}"
|
||||
,"NonBrowser-1:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="参数错误:{1}"
|
||||
//@@en="Illegal argument: {1}"
|
||||
,"IllegalArgs-1:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="调用{1}需要先导入{2}"
|
||||
//@@en="Calling {1} needs to import {2} first"
|
||||
,"NeedImport-2:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="不支持:{1}"
|
||||
//@@en="Not support: {1}"
|
||||
,"NotSupport-1:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="覆盖导入{1}"
|
||||
//@@en="Override import {1}"
|
||||
,"8HO5:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=recorder-core.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-amr.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="AMR-NB(NarrowBand),采样率设置无效(只提供8000hz),比特率范围:{1}(默认12.2kbps),一帧20ms、{2}字节;浏览器一般不支持播放amr格式,可用Recorder.amr2wav()转码成wav播放"
|
||||
//@@en="AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
|
||||
//@@Put0
|
||||
"b2mN:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
|
||||
//@@en="AMR Info: does not match the set {1}, has been updated to {2}"
|
||||
,"tQBv:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
//@@en="Data sampleRate lower than {1}"
|
||||
,"q12D:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"TxjV:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据,只有首次回调数据(首帧)包含AMR头;在合并成AMR文件时,如果没有把首帧数据包含进去,则必须在文件开头添加上AMR头:Recorder.AMR.AMR_HEADER(转成二进制),否则无法播放"
|
||||
//@@en="takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
|
||||
,"Q7p7:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,amr实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
|
||||
,"6o9Z:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="amr worker剩{1}个未stop"
|
||||
//@@en="amr worker left {1} unstopped"
|
||||
,"yYWs:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="amr编码器未start"
|
||||
//@@en="amr encoder not started"
|
||||
,"jOi8:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-amr.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-ogg.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="Ogg Vorbis,比特率取值16-100kbps,采样率取值无限制"
|
||||
//@@en="Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
|
||||
//@@Put0
|
||||
"O8Gn:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"5si6:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据,Ogg由数据页组成,一页包含多帧音频数据(含几秒的音频,一页数据无法单独解码和播放),此编码器每次输出都是完整的一页数据,因此实时性会比较低;在合并成完整ogg文件时,必须将输出的所有数据合并到一起,否则可能无法播放,不支持截取中间一部分单独解码和播放"
|
||||
//@@en="takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
|
||||
,"R8yz:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,OggVorbis实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
|
||||
,"hB9D:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="ogg worker剩{1}个未stop"
|
||||
//@@en="There are {1} unstopped ogg workers"
|
||||
,"oTiy:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="ogg编码器未start"
|
||||
//@@en="ogg encoder not started"
|
||||
,"dIpw:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-ogg.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-webm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="此浏览器不支持进行webm编码,未实现MediaRecorder"
|
||||
//@@en="This browser does not support webm encoding, MediaRecorder is not implemented"
|
||||
//@@Put0
|
||||
"L49q:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="只有比较新的浏览器支持,压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法,只能按照类似边播放边收听形式把数据导入到MediaRecorder,有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小,但音频文件中的比特率并非设定比特率,采样率由于是我们自己采样的,到这个编码器随他怎么搞"
|
||||
//@@en="Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
|
||||
,"tsTW:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="此浏览器不支持把录音转成webm格式"
|
||||
//@@en="This browser does not support converting recordings to webm format"
|
||||
,"aG4z:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="转码webm出错:{1}"
|
||||
//@@en="Error encoding webm: {1}"
|
||||
,"PIX0:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-webm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/g711x.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1};{2}音频文件无法直接播放,可用Recorder.{2}2wav()转码成wav播放;采样率比特率设置无效,固定为8000hz采样率、16位,每个采样压缩成8位存储,音频文件大小为8000字节/秒;如需任意采样率支持,请使用Recorder.{2}_encode()方法"
|
||||
//@@en="{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
|
||||
//@@Put0
|
||||
"d8YX:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
//@@en="Data sampleRate lower than {1}"
|
||||
,"29UK:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}编码器未start"
|
||||
//@@en="{1} encoder not started"
|
||||
,"quVJ:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/g711x.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/mp3.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="采样率范围:{1};比特率范围:{2}(不同比特率支持的采样率范围不同,小于32kbps时采样率需小于32000)"
|
||||
//@@en="sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
|
||||
//@@Put0
|
||||
"Zm7L:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}不在mp3支持的取值范围:{2}"
|
||||
//@@en="{1} is not in the value range supported by mp3: {2}"
|
||||
,"eGB9:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围:{3}"
|
||||
//@@en="sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
|
||||
,"zLTa:"+ //args: {1}-{3}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"yhUs:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,mp3实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
|
||||
,"k9PT:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="mp3 worker剩{1}个未stop"
|
||||
//@@en="There are {1} unstopped mp3 workers left"
|
||||
,"fT6M:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="mp3编码器未start"
|
||||
//@@en="mp3 encoder not started"
|
||||
,"mPxH:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="和设置的不匹配{1},已更新成{2}"
|
||||
//@@en="Does not match the set {1}, has been updated to {2}"
|
||||
,"uY9i:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="Fix移除{1}帧"
|
||||
//@@en="Fix remove {1} frame"
|
||||
,"iMSm:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="移除帧数过多"
|
||||
//@@en="Remove too many frames"
|
||||
,"b9zm:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/mp3.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="pcm为未封装的原始音频数据,pcm音频文件无法直接播放,可用Recorder.pcm2wav()转码成wav播放;支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
//@@en="pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
|
||||
//@@Put0
|
||||
"fWsN:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
|
||||
//@@en="PCM Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
,"uMUJ:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="pcm2wav必须提供sampleRate和bitRate"
|
||||
//@@en="pcm2wav must provide sampleRate and bitRate"
|
||||
,"KmRz:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="pcm编码器未start"
|
||||
//@@en="pcm encoder not started"
|
||||
,"sDkA:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/wav.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="支持位数8位、16位(填在比特率里面),采样率取值无限制;此编码器仅在pcm数据前加了一个44字节的wav头,编码出来的16位wav文件去掉开头的44字节即可得到pcm(注:其他wav编码器可能不是44字节)"
|
||||
//@@en="Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
|
||||
//@@Put0
|
||||
"gPSE:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
|
||||
//@@en="WAV Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
,"wyw9:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/wav.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="getAudioSrc方法已过时:请直接使用getMediaStream然后赋值给audio.srcObject,仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
|
||||
//@@en="The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
|
||||
//@@Put0
|
||||
"0XYC:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="start被stop终止"
|
||||
//@@en="start is terminated by stop"
|
||||
,"6DDt:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}多次start"
|
||||
//@@en="{1} repeat start"
|
||||
,"I4h4:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="浏览器不支持打开{1}"
|
||||
//@@en="The browser does not support opening {1}"
|
||||
,"P6Gs:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,start需要在用户操作(触摸、点击等)时进行调用,否则会尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
//@@en=" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
|
||||
,"JwDm:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="此浏览器的AudioBuffer实现不支持动态特性,采用兼容模式"
|
||||
//@@en="The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
|
||||
,"qx6X:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="环境检测超时"
|
||||
//@@en="Environment detection timeout"
|
||||
,"cdOx:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="可能无法播放:{1}"
|
||||
//@@en="Could not play: {1}"
|
||||
,"S2Bu:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="input调用失败:非pcm[Int16,...]输入时,必须解码或者使用transform转换"
|
||||
//@@en="input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
|
||||
,"ZfGG:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="input调用失败:未提供sampleRate"
|
||||
//@@en="input call failed: sampleRate not provided"
|
||||
,"N4ke:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="input调用失败:data的sampleRate={1}和之前的={2}不同"
|
||||
//@@en="input call failed: sampleRate={1} of data is different from previous={2}"
|
||||
,"IHZd:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="延迟过大,已丢弃{1}ms {2}"
|
||||
//@@en="The delay is too large, {1}ms has been discarded, {2}"
|
||||
,"L8sC:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="{1}未调用start方法"
|
||||
//@@en="{1} did not call the start method"
|
||||
,"TZPq:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="浏览器不支持音频解码"
|
||||
//@@en="Browser does not support audio decoding"
|
||||
,"iCFC:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="音频解码数据必须是ArrayBuffer"
|
||||
//@@en="Audio decoding data must be ArrayBuffer"
|
||||
,"wE2k:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="音频解码失败:{1}"
|
||||
//@@en="Audio decoding failed: {1}"
|
||||
,"mOaT:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/buffer_stream.player.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="符号[{1}]无效:{2}"
|
||||
//@@en="Invalid symbol [{1}]: {2}"
|
||||
//@@Put0
|
||||
"3RBa:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="音符[{1}]无效:{2}"
|
||||
//@@en="Invalid note [{1}]: {2}"
|
||||
,"U212:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="多个音时必须对齐,相差{1}ms"
|
||||
//@@en="Multiple tones must be aligned, with a difference of {1}ms"
|
||||
,"7qAD:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="祝你生日快乐"
|
||||
//@@en="Happy Birthday to You"
|
||||
,"QGsW:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="致爱丽丝"
|
||||
//@@en="For Elise"
|
||||
,"emJR:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="卡农-右手简谱"
|
||||
//@@en="Canon - Right Hand Notation"
|
||||
,"GsYy:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="卡农"
|
||||
//@@en="Canon"
|
||||
,"bSFZ:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/sonic.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,不支持调用Sonic.Async"
|
||||
//@@en="The current environment does not support Web Worker and does not support calling Sonic.Async"
|
||||
//@@Put0
|
||||
"Ikdz:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="sonic worker剩{1}个未flush"
|
||||
//@@en="There are {1} unflushed sonic workers left"
|
||||
,"IC5Y:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/sonic.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app-native-support.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
|
||||
//@@en="The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
|
||||
//@@Put0
|
||||
"WWoj:"+ //args: {1}-{3}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未开始录音,但收到Native PCM数据"
|
||||
//@@en="Recording does not start, but Native PCM data is received"
|
||||
,"rCAM:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="检测到跨域iframe,NativeRecordReceivePCM无法注入到顶层,已监听postMessage转发兼容传输数据,请自行实现将top层接收到数据转发到本iframe(不限层),不然无法接收到录音数据"
|
||||
//@@en="A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
|
||||
,"t2OF:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"Z2y2:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app-native-support.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@en="Duplicate import {1}"
|
||||
//@@Put0
|
||||
"uXtA:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
|
||||
//@@en="Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
|
||||
,"kIBu:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="重复注册{1}"
|
||||
//@@en="Duplicate registration {1}"
|
||||
,"ha2K:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="仅清理资源"
|
||||
//@@en="Clean resources only"
|
||||
,"wpTL:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"bpvP:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前环境不支持实时回调,无法进行{1}"
|
||||
//@@en="The current environment does not support real-time callback and cannot be performed {1}"
|
||||
,"fLJD:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="录音权限请求失败:"
|
||||
//@@en="Recording permission request failed: "
|
||||
,"YnzX:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="需先调用{1}"
|
||||
//@@en="Need to call {1} first"
|
||||
,"nwKR:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
|
||||
//@@en="This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
|
||||
,"citA:"+ //args: {1}-{2}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="开始录音失败:"
|
||||
//@@en="Failed to start recording: "
|
||||
,"ecp9:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="不能录音:"
|
||||
//@@en="Cannot record: "
|
||||
,"EKmS:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="已开始录音"
|
||||
//@@en="Recording started"
|
||||
,"k7Qo:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
//@@en="Failed to stop recording: "
|
||||
,"Douz:"+ //no args
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="和Start时差:{1}ms"
|
||||
//@@en="Time difference from Start: {1}ms"
|
||||
,"wqSH:"+ //args: {1}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
|
||||
//@@en="Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
|
||||
,"g3VX:"+ //args: {1}-{4}
|
||||
"" /** TODO: translate to your-language **/
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app.js ***************
|
||||
|
||||
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-2 End @@
|
||||
|
||||
}));
|
782
public/i18n/en-US.js
Normal file
782
public/i18n/en-US.js
Normal file
@ -0,0 +1,782 @@
|
||||
/*
|
||||
Recorder i18n/en-US.js
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
Usage: Recorder.i18n.lang="en-US" or "en"
|
||||
|
||||
Desc: English, 英语。This translation mainly comes from: google translation + Baidu translation, translated from Chinese to English. 此翻译主要来自:google翻译+百度翻译,由中文翻译成英文。
|
||||
|
||||
注意:请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成,只允许在字符串中填写翻译后的文本,请勿改变代码结构;翻译的文本如果需要明确的空值,请填写"=Empty";文本中的变量用{n}表示(n代表第几个变量),所有变量必须都出现至少一次,如果不要某变量用{n!}表示
|
||||
|
||||
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
factory(win.Recorder,browser);
|
||||
}(function(Recorder,isBrowser){
|
||||
"use strict";
|
||||
var i18n=Recorder.i18n;
|
||||
|
||||
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-1 End @@
|
||||
|
||||
//@@Exec i18n.lang="en-US";
|
||||
Recorder.CLog('Import Recorder i18n lang="en-US"');
|
||||
|
||||
i18n.alias["en-US"]="en";
|
||||
|
||||
var putSet={lang:"en"};
|
||||
|
||||
i18n.data["rtl$en"]=false;
|
||||
i18n.data["desc$en"]="English, 英语。This translation mainly comes from: google translation + Baidu translation, translated from Chinese to English. 此翻译主要来自:google翻译+百度翻译,由中文翻译成英文。";
|
||||
//@@Exec i18n.GenerateDisplayEnglish=false;
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=recorder-core.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@Put0
|
||||
"K8zP:"+ //args: {1}
|
||||
"Duplicate import {1}"
|
||||
|
||||
//@@zh="剩{1}个GetContext未close"
|
||||
,"mSxV:"+ //args: {1}
|
||||
"There are {1} GetContext unclosed"
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用,否则将在rec.start时尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
,"nMIy:"+ //no args
|
||||
" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
|
||||
|
||||
//@@zh="Stream的采样率{1}不等于{2},将进行采样率转换(注意:音质不会变好甚至可能变差),主要在移动端未禁用回声消除时会产生此现象,浏览器有回声消除时可能只会返回16k采样率的音频数据,"
|
||||
,"eS8i:"+ //args: {1}-{2}
|
||||
"The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
|
||||
|
||||
//@@zh="。由于{1}内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启{1}。"
|
||||
,"ZGlf:"+ //args: {1}
|
||||
". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
|
||||
|
||||
//@@zh="Connect采用老的{1},"
|
||||
,"7TU0:"+ //args: {1}
|
||||
"Connect uses the old {1}, "
|
||||
|
||||
//@@zh="但已设置{1}尝试启用{2}"
|
||||
,"JwCL:"+ //args: {1}-{2}
|
||||
"But {1} is set trying to enable {2}"
|
||||
|
||||
//@@zh="可设置{1}尝试启用{2}"
|
||||
,"VGjB:"+ //args: {1}-{2}
|
||||
"Can set {1} try to enable {2}"
|
||||
|
||||
//@@zh="{1}未返回任何音频,恢复使用{2}"
|
||||
,"MxX1:"+ //args: {1}-{2}
|
||||
"{1} did not return any audio, reverting to {2}"
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
,"XUap:"+ //args: {1}
|
||||
"{1} redundant callback"
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
|
||||
,"yOta:"+ //args: {1}-{3}
|
||||
"Connect uses {1}, set {2} to restore old-fashioned {3}"
|
||||
|
||||
//@@zh="(此浏览器不支持{1})"
|
||||
,"VwPd:"+ //args: {1}
|
||||
" (This browser does not support {1}) "
|
||||
|
||||
//@@zh="{1}未返回任何音频,降级使用{2}"
|
||||
,"vHnb:"+ //args: {1}-{2}
|
||||
"{1} did not return any audio, downgrade to {2}"
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
,"O9P7:"+ //args: {1}
|
||||
"{1} redundant callback"
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
|
||||
,"LMEm:"+ //args: {1}-{4}
|
||||
"Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
|
||||
|
||||
//@@zh="{1}的filter采样率变了,重设滤波"
|
||||
,"d48C:"+ //args: {1}
|
||||
"The filter sampleRate of {1} has changed, reset the filter"
|
||||
|
||||
//@@zh="{1}似乎传入了未重置chunk {2}"
|
||||
,"tlbC:"+ //args: {1}-{2}
|
||||
"{1} seems to have passed in an unreset chunk {2}"
|
||||
|
||||
//@@zh="{1}和{2}必须是数值"
|
||||
,"VtS4:"+ //args: {1}-{2}
|
||||
"{1} and {2} must be number"
|
||||
|
||||
//@@zh="录音open失败:"
|
||||
,"5tWi:"+ //no args
|
||||
"Recording open failed: "
|
||||
|
||||
//@@zh="open被取消"
|
||||
,"dFm8:"+ //no args
|
||||
"open cancelled"
|
||||
|
||||
//@@zh="open被中断"
|
||||
,"VtJO:"+ //no args
|
||||
"open interrupted"
|
||||
|
||||
//@@zh=",可尝试使用RecordApp解决方案"
|
||||
,"EMJq:"+ //no args
|
||||
", you can try to use the RecordApp solution "
|
||||
|
||||
//@@zh="不能录音:"
|
||||
,"A5bm:"+ //no args
|
||||
"Cannot record: "
|
||||
|
||||
//@@zh="不支持此浏览器从流中获取录音"
|
||||
,"1iU7:"+ //no args
|
||||
"This browser does not support obtaining recordings from stream"
|
||||
|
||||
//@@zh="从流中打开录音失败:"
|
||||
,"BTW2:"+ //no args
|
||||
"Failed to open recording from stream: "
|
||||
|
||||
//@@zh="无权录音(跨域,请尝试给iframe添加麦克风访问策略,如{1})"
|
||||
,"Nclz:"+ //args: {1}
|
||||
"No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
|
||||
|
||||
//@@zh=",无可用麦克风"
|
||||
,"jBa9:"+ //no args
|
||||
", no microphone available"
|
||||
|
||||
//@@zh="用户拒绝了录音权限"
|
||||
,"gyO5:"+ //no args
|
||||
"User denied recording permission"
|
||||
|
||||
//@@zh="浏览器禁止不安全页面录音,可开启https解决"
|
||||
,"oWNo:"+ //no args
|
||||
"Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
|
||||
|
||||
//@@zh="此浏览器不支持录音"
|
||||
,"COxc:"+ //no args
|
||||
"This browser does not support recording"
|
||||
|
||||
//@@zh="发现同时多次调用open"
|
||||
,"upb8:"+ //no args
|
||||
"It was found that open was called multiple times at the same time"
|
||||
|
||||
//@@zh="录音功能无效:无音频流"
|
||||
,"Q1GA:"+ //no args
|
||||
"Invalid recording: no audio stream"
|
||||
|
||||
//@@zh=",将尝试禁用回声消除后重试"
|
||||
,"KxE2:"+ //no args
|
||||
", will try to disable echoCancellation and try again"
|
||||
|
||||
//@@zh="请求录音权限错误"
|
||||
,"xEQR:"+ //no args
|
||||
"Error requesting recording permission"
|
||||
|
||||
//@@zh="无法录音:"
|
||||
,"bDOG:"+ //no args
|
||||
"Unable to record: "
|
||||
|
||||
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
|
||||
,"IjL3:"+ //args: {1}
|
||||
"Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
|
||||
|
||||
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除,移动端未禁用回声消除时可能会降低系统播放音量(关闭录音后可恢复)和仅提供16k采样率的音频流(不需要回声消除时可明确配置成禁用来获得48k高音质的流),请参阅文档中{2}配置"
|
||||
,"RiWe:"+ //args: {1}-{2}
|
||||
", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
|
||||
|
||||
//@@zh="close被忽略(因为同时open了多个rec,只有最后一个会真正close)"
|
||||
,"hWVz:"+ //no args
|
||||
"close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
|
||||
|
||||
//@@zh="忽略"
|
||||
,"UHvm:"+ //no args
|
||||
"ignore"
|
||||
|
||||
//@@zh="不支持{1}架构"
|
||||
,"Essp:"+ //args: {1}
|
||||
"{1} architecture not supported"
|
||||
|
||||
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
|
||||
,"2XBl:"+ //args: {1}
|
||||
"{1} type does not support setting takeoffEncodeChunk"
|
||||
|
||||
//@@zh="(未加载编码器)"
|
||||
,"LG7e:"+ //no args
|
||||
"(Encoder not loaded)"
|
||||
|
||||
//@@zh="{1}环境不支持实时处理"
|
||||
,"7uMV:"+ //args: {1}
|
||||
"{1} environment does not support real-time processing"
|
||||
|
||||
//@@zh="补偿{1}ms"
|
||||
,"4Kfd:"+ //args: {1}
|
||||
"Compensation {1}ms"
|
||||
|
||||
//@@zh="未补偿{1}ms"
|
||||
,"bM5i:"+ //args: {1}
|
||||
"Uncompensated {1}ms"
|
||||
|
||||
//@@zh="回调出错是不允许的,需保证不会抛异常"
|
||||
,"gFUF:"+ //no args
|
||||
"Callback error is not allowed, you need to ensure that no exception will be thrown"
|
||||
|
||||
//@@zh="低性能,耗时{1}ms"
|
||||
,"2ghS:"+ //args: {1}
|
||||
"Low performance, took {1}ms"
|
||||
|
||||
//@@zh="未进入异步前不能清除buffers"
|
||||
,"ufqH:"+ //no args
|
||||
"Buffers cannot be cleared before entering async"
|
||||
|
||||
//@@zh="start失败:未open"
|
||||
,"6WmN:"+ //no args
|
||||
"start failed: not open"
|
||||
|
||||
//@@zh="start 开始录音,"
|
||||
,"kLDN:"+ //no args
|
||||
"start recording, "
|
||||
|
||||
//@@zh="start被中断"
|
||||
,"Bp2y:"+ //no args
|
||||
"start was interrupted"
|
||||
|
||||
//@@zh=",可能无法录音:"
|
||||
,"upkE:"+ //no args
|
||||
", may fail to record: "
|
||||
|
||||
//@@zh="stop 和start时差:"
|
||||
,"Xq4s:"+ //no args
|
||||
"Stop and start time difference: "
|
||||
|
||||
//@@zh="补偿:"
|
||||
,"3CQP:"+ //no args
|
||||
"compensate: "
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
,"u8JG:"+ //no args
|
||||
"Failed to stop recording: "
|
||||
|
||||
//@@zh=",请设置{1}"
|
||||
,"1skY:"+ //args: {1}
|
||||
", please set {1}"
|
||||
|
||||
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
|
||||
,"Wv7l:"+ //args: {1}-{3}
|
||||
"Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
|
||||
|
||||
//@@zh="{1}编码器返回的不是{2}"
|
||||
,"Vkbd:"+ //args: {1}-{2}
|
||||
"{1} encoder returned not {2}"
|
||||
|
||||
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
|
||||
,"QWnr:"+ //no args
|
||||
"After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
|
||||
|
||||
//@@zh="生成的{1}无效"
|
||||
,"Sz2H:"+ //args: {1}
|
||||
"Invalid generated {1}"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
,"wf9t:"+ //no args
|
||||
"Recording not started"
|
||||
|
||||
//@@zh=",开始录音前无用户交互导致AudioContext未运行"
|
||||
,"Dl2c:"+ //no args
|
||||
", No user interaction before starting recording, resulting in AudioContext not running"
|
||||
|
||||
//@@zh="未采集到录音"
|
||||
,"Ltz3:"+ //no args
|
||||
"Recording not captured"
|
||||
|
||||
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
|
||||
,"xGuI:"+ //args: {1}-{2}
|
||||
"The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
|
||||
|
||||
//@@zh="录音错误:"
|
||||
,"AxOH:"+ //no args
|
||||
"Recording error: "
|
||||
|
||||
//@@zh="音频buffers被释放"
|
||||
,"xkKd:"+ //no args
|
||||
"Audio buffers are released"
|
||||
|
||||
//@@zh="采样:{1} 花:{2}ms"
|
||||
,"CxeT:"+ //args: {1}-{2}
|
||||
"Sampled: {1}, took: {2}ms"
|
||||
|
||||
//@@zh="非浏览器环境,不支持{1}"
|
||||
,"NonBrowser-1:"+ //args: {1}
|
||||
"Non-browser environment, does not support {1}"
|
||||
|
||||
//@@zh="参数错误:{1}"
|
||||
,"IllegalArgs-1:"+ //args: {1}
|
||||
"Illegal argument: {1}"
|
||||
|
||||
//@@zh="调用{1}需要先导入{2}"
|
||||
,"NeedImport-2:"+ //args: {1}-{2}
|
||||
"Calling {1} needs to import {2} first"
|
||||
|
||||
//@@zh="不支持:{1}"
|
||||
,"NotSupport-1:"+ //args: {1}
|
||||
"Not support: {1}"
|
||||
|
||||
//@@zh="覆盖导入{1}"
|
||||
,"8HO5:"+ //args: {1}
|
||||
"Override import {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=recorder-core.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-amr.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="AMR-NB(NarrowBand),采样率设置无效(只提供8000hz),比特率范围:{1}(默认12.2kbps),一帧20ms、{2}字节;浏览器一般不支持播放amr格式,可用Recorder.amr2wav()转码成wav播放"
|
||||
//@@Put0
|
||||
"b2mN:"+ //args: {1}-{2}
|
||||
"AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
|
||||
|
||||
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
|
||||
,"tQBv:"+ //args: {1}-{2}
|
||||
"AMR Info: does not match the set {1}, has been updated to {2}"
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
,"q12D:"+ //args: {1}
|
||||
"Data sampleRate lower than {1}"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
,"TxjV:"+ //no args
|
||||
"The current browser version is too low to process in real time"
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据,只有首次回调数据(首帧)包含AMR头;在合并成AMR文件时,如果没有把首帧数据包含进去,则必须在文件开头添加上AMR头:Recorder.AMR.AMR_HEADER(转成二进制),否则无法播放"
|
||||
,"Q7p7:"+ //no args
|
||||
"takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,amr实时编码器运行在主线程中"
|
||||
,"6o9Z:"+ //no args
|
||||
"The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
|
||||
|
||||
//@@zh="amr worker剩{1}个未stop"
|
||||
,"yYWs:"+ //args: {1}
|
||||
"amr worker left {1} unstopped"
|
||||
|
||||
//@@zh="amr编码器未start"
|
||||
,"jOi8:"+ //no args
|
||||
"amr encoder not started"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-amr.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-ogg.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="Ogg Vorbis,比特率取值16-100kbps,采样率取值无限制"
|
||||
//@@Put0
|
||||
"O8Gn:"+ //no args
|
||||
"Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
,"5si6:"+ //no args
|
||||
"The current browser version is too low to process in real time"
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据,Ogg由数据页组成,一页包含多帧音频数据(含几秒的音频,一页数据无法单独解码和播放),此编码器每次输出都是完整的一页数据,因此实时性会比较低;在合并成完整ogg文件时,必须将输出的所有数据合并到一起,否则可能无法播放,不支持截取中间一部分单独解码和播放"
|
||||
,"R8yz:"+ //no args
|
||||
"takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,OggVorbis实时编码器运行在主线程中"
|
||||
,"hB9D:"+ //no args
|
||||
"The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
|
||||
|
||||
//@@zh="ogg worker剩{1}个未stop"
|
||||
,"oTiy:"+ //args: {1}
|
||||
"There are {1} unstopped ogg workers"
|
||||
|
||||
//@@zh="ogg编码器未start"
|
||||
,"dIpw:"+ //no args
|
||||
"ogg encoder not started"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-ogg.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-webm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="此浏览器不支持进行webm编码,未实现MediaRecorder"
|
||||
//@@Put0
|
||||
"L49q:"+ //no args
|
||||
"This browser does not support webm encoding, MediaRecorder is not implemented"
|
||||
|
||||
//@@zh="只有比较新的浏览器支持,压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法,只能按照类似边播放边收听形式把数据导入到MediaRecorder,有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小,但音频文件中的比特率并非设定比特率,采样率由于是我们自己采样的,到这个编码器随他怎么搞"
|
||||
,"tsTW:"+ //no args
|
||||
"Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
|
||||
|
||||
//@@zh="此浏览器不支持把录音转成webm格式"
|
||||
,"aG4z:"+ //no args
|
||||
"This browser does not support converting recordings to webm format"
|
||||
|
||||
//@@zh="转码webm出错:{1}"
|
||||
,"PIX0:"+ //args: {1}
|
||||
"Error encoding webm: {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-webm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/g711x.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1};{2}音频文件无法直接播放,可用Recorder.{2}2wav()转码成wav播放;采样率比特率设置无效,固定为8000hz采样率、16位,每个采样压缩成8位存储,音频文件大小为8000字节/秒;如需任意采样率支持,请使用Recorder.{2}_encode()方法"
|
||||
//@@Put0
|
||||
"d8YX:"+ //args: {1}-{2}
|
||||
"{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
,"29UK:"+ //args: {1}
|
||||
"Data sampleRate lower than {1}"
|
||||
|
||||
//@@zh="{1}编码器未start"
|
||||
,"quVJ:"+ //args: {1}
|
||||
"{1} encoder not started"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/g711x.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/mp3.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="采样率范围:{1};比特率范围:{2}(不同比特率支持的采样率范围不同,小于32kbps时采样率需小于32000)"
|
||||
//@@Put0
|
||||
"Zm7L:"+ //args: {1}-{2}
|
||||
"sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
|
||||
|
||||
//@@zh="{1}不在mp3支持的取值范围:{2}"
|
||||
,"eGB9:"+ //args: {1}-{2}
|
||||
"{1} is not in the value range supported by mp3: {2}"
|
||||
|
||||
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围:{3}"
|
||||
,"zLTa:"+ //args: {1}-{3}
|
||||
"sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
,"yhUs:"+ //no args
|
||||
"The current browser version is too low to process in real time"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,mp3实时编码器运行在主线程中"
|
||||
,"k9PT:"+ //no args
|
||||
"The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
|
||||
|
||||
//@@zh="mp3 worker剩{1}个未stop"
|
||||
,"fT6M:"+ //args: {1}
|
||||
"There are {1} unstopped mp3 workers left"
|
||||
|
||||
//@@zh="mp3编码器未start"
|
||||
,"mPxH:"+ //no args
|
||||
"mp3 encoder not started"
|
||||
|
||||
//@@zh="和设置的不匹配{1},已更新成{2}"
|
||||
,"uY9i:"+ //args: {1}-{2}
|
||||
"Does not match the set {1}, has been updated to {2}"
|
||||
|
||||
//@@zh="Fix移除{1}帧"
|
||||
,"iMSm:"+ //args: {1}
|
||||
"Fix remove {1} frame"
|
||||
|
||||
//@@zh="移除帧数过多"
|
||||
,"b9zm:"+ //no args
|
||||
"Remove too many frames"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/mp3.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="pcm为未封装的原始音频数据,pcm音频文件无法直接播放,可用Recorder.pcm2wav()转码成wav播放;支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
//@@Put0
|
||||
"fWsN:"+ //no args
|
||||
"pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
|
||||
|
||||
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
|
||||
,"uMUJ:"+ //args: {1}-{2}
|
||||
"PCM Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
|
||||
//@@zh="pcm2wav必须提供sampleRate和bitRate"
|
||||
,"KmRz:"+ //no args
|
||||
"pcm2wav must provide sampleRate and bitRate"
|
||||
|
||||
//@@zh="pcm编码器未start"
|
||||
,"sDkA:"+ //no args
|
||||
"pcm encoder not started"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/wav.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="支持位数8位、16位(填在比特率里面),采样率取值无限制;此编码器仅在pcm数据前加了一个44字节的wav头,编码出来的16位wav文件去掉开头的44字节即可得到pcm(注:其他wav编码器可能不是44字节)"
|
||||
//@@Put0
|
||||
"gPSE:"+ //no args
|
||||
"Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
|
||||
|
||||
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
|
||||
,"wyw9:"+ //args: {1}-{2}
|
||||
"WAV Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/wav.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="getAudioSrc方法已过时:请直接使用getMediaStream然后赋值给audio.srcObject,仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
|
||||
//@@Put0
|
||||
"0XYC:"+ //no args
|
||||
"The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
|
||||
|
||||
//@@zh="start被stop终止"
|
||||
,"6DDt:"+ //no args
|
||||
"start is terminated by stop"
|
||||
|
||||
//@@zh="{1}多次start"
|
||||
,"I4h4:"+ //args: {1}
|
||||
"{1} repeat start"
|
||||
|
||||
//@@zh="浏览器不支持打开{1}"
|
||||
,"P6Gs:"+ //args: {1}
|
||||
"The browser does not support opening {1}"
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,start需要在用户操作(触摸、点击等)时进行调用,否则会尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
,"JwDm:"+ //no args
|
||||
" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
|
||||
|
||||
//@@zh="此浏览器的AudioBuffer实现不支持动态特性,采用兼容模式"
|
||||
,"qx6X:"+ //no args
|
||||
"The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
|
||||
|
||||
//@@zh="环境检测超时"
|
||||
,"cdOx:"+ //no args
|
||||
"Environment detection timeout"
|
||||
|
||||
//@@zh="可能无法播放:{1}"
|
||||
,"S2Bu:"+ //args: {1}
|
||||
"Could not play: {1}"
|
||||
|
||||
//@@zh="input调用失败:非pcm[Int16,...]输入时,必须解码或者使用transform转换"
|
||||
,"ZfGG:"+ //no args
|
||||
"input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
|
||||
|
||||
//@@zh="input调用失败:未提供sampleRate"
|
||||
,"N4ke:"+ //no args
|
||||
"input call failed: sampleRate not provided"
|
||||
|
||||
//@@zh="input调用失败:data的sampleRate={1}和之前的={2}不同"
|
||||
,"IHZd:"+ //args: {1}-{2}
|
||||
"input call failed: sampleRate={1} of data is different from previous={2}"
|
||||
|
||||
//@@zh="延迟过大,已丢弃{1}ms {2}"
|
||||
,"L8sC:"+ //args: {1}-{2}
|
||||
"The delay is too large, {1}ms has been discarded, {2}"
|
||||
|
||||
//@@zh="{1}未调用start方法"
|
||||
,"TZPq:"+ //args: {1}
|
||||
"{1} did not call the start method"
|
||||
|
||||
//@@zh="浏览器不支持音频解码"
|
||||
,"iCFC:"+ //no args
|
||||
"Browser does not support audio decoding"
|
||||
|
||||
//@@zh="音频解码数据必须是ArrayBuffer"
|
||||
,"wE2k:"+ //no args
|
||||
"Audio decoding data must be ArrayBuffer"
|
||||
|
||||
//@@zh="音频解码失败:{1}"
|
||||
,"mOaT:"+ //args: {1}
|
||||
"Audio decoding failed: {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/buffer_stream.player.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="符号[{1}]无效:{2}"
|
||||
//@@Put0
|
||||
"3RBa:"+ //args: {1}-{2}
|
||||
"Invalid symbol [{1}]: {2}"
|
||||
|
||||
//@@zh="音符[{1}]无效:{2}"
|
||||
,"U212:"+ //args: {1}-{2}
|
||||
"Invalid note [{1}]: {2}"
|
||||
|
||||
//@@zh="多个音时必须对齐,相差{1}ms"
|
||||
,"7qAD:"+ //args: {1}
|
||||
"Multiple tones must be aligned, with a difference of {1}ms"
|
||||
|
||||
//@@zh="祝你生日快乐"
|
||||
,"QGsW:"+ //no args
|
||||
"Happy Birthday to You"
|
||||
|
||||
//@@zh="致爱丽丝"
|
||||
,"emJR:"+ //no args
|
||||
"For Elise"
|
||||
|
||||
//@@zh="卡农-右手简谱"
|
||||
,"GsYy:"+ //no args
|
||||
"Canon - Right Hand Notation"
|
||||
|
||||
//@@zh="卡农"
|
||||
,"bSFZ:"+ //no args
|
||||
"Canon"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/sonic.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,不支持调用Sonic.Async"
|
||||
//@@Put0
|
||||
"Ikdz:"+ //no args
|
||||
"The current environment does not support Web Worker and does not support calling Sonic.Async"
|
||||
|
||||
//@@zh="sonic worker剩{1}个未flush"
|
||||
,"IC5Y:"+ //args: {1}
|
||||
"There are {1} unflushed sonic workers left"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/sonic.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app-native-support.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
|
||||
//@@Put0
|
||||
"WWoj:"+ //args: {1}-{3}
|
||||
"The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
|
||||
|
||||
//@@zh="未开始录音,但收到Native PCM数据"
|
||||
,"rCAM:"+ //no args
|
||||
"Recording does not start, but Native PCM data is received"
|
||||
|
||||
//@@zh="检测到跨域iframe,NativeRecordReceivePCM无法注入到顶层,已监听postMessage转发兼容传输数据,请自行实现将top层接收到数据转发到本iframe(不限层),不然无法接收到录音数据"
|
||||
,"t2OF:"+ //no args
|
||||
"A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
|
||||
|
||||
//@@zh="未开始录音"
|
||||
,"Z2y2:"+ //no args
|
||||
"Recording not started"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app-native-support.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@Put0
|
||||
"uXtA:"+ //args: {1}
|
||||
"Duplicate import {1}"
|
||||
|
||||
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
|
||||
,"kIBu:"+ //args: {1}
|
||||
"Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
|
||||
|
||||
//@@zh="重复注册{1}"
|
||||
,"ha2K:"+ //args: {1}
|
||||
"Duplicate registration {1}"
|
||||
|
||||
//@@zh="仅清理资源"
|
||||
,"wpTL:"+ //no args
|
||||
"Clean resources only"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
,"bpvP:"+ //no args
|
||||
"Recording not started"
|
||||
|
||||
//@@zh="当前环境不支持实时回调,无法进行{1}"
|
||||
,"fLJD:"+ //args: {1}
|
||||
"The current environment does not support real-time callback and cannot be performed {1}"
|
||||
|
||||
//@@zh="录音权限请求失败:"
|
||||
,"YnzX:"+ //no args
|
||||
"Recording permission request failed: "
|
||||
|
||||
//@@zh="需先调用{1}"
|
||||
,"nwKR:"+ //args: {1}
|
||||
"Need to call {1} first"
|
||||
|
||||
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
|
||||
,"citA:"+ //args: {1}-{2}
|
||||
"This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
|
||||
|
||||
//@@zh="开始录音失败:"
|
||||
,"ecp9:"+ //no args
|
||||
"Failed to start recording: "
|
||||
|
||||
//@@zh="不能录音:"
|
||||
,"EKmS:"+ //no args
|
||||
"Cannot record: "
|
||||
|
||||
//@@zh="已开始录音"
|
||||
,"k7Qo:"+ //no args
|
||||
"Recording started"
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
,"Douz:"+ //no args
|
||||
"Failed to stop recording: "
|
||||
|
||||
//@@zh="和Start时差:{1}ms"
|
||||
,"wqSH:"+ //args: {1}
|
||||
"Time difference from Start: {1}ms"
|
||||
|
||||
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
|
||||
,"g3VX:"+ //args: {1}-{4}
|
||||
"Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app.js ***************
|
||||
|
||||
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-2 End @@
|
||||
|
||||
}));
|
935
public/i18n/es.js
Normal file
935
public/i18n/es.js
Normal file
@ -0,0 +1,935 @@
|
||||
/*
|
||||
Recorder i18n/es.js
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
Usage: Recorder.i18n.lang="es"
|
||||
|
||||
Desc: Spanish, Español, 西班牙语。Esta traducción proviene principalmente de: traducción de google + traducción de Baidu, traducida del chino al español. 此翻译主要来自:google翻译+百度翻译,由中文翻译成西班牙语。
|
||||
|
||||
注意:请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成,只允许在字符串中填写翻译后的文本,请勿改变代码结构;翻译的文本如果需要明确的空值,请填写"=Empty";文本中的变量用{n}表示(n代表第几个变量),所有变量必须都出现至少一次,如果不要某变量用{n!}表示
|
||||
|
||||
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
factory(win.Recorder,browser);
|
||||
}(function(Recorder,isBrowser){
|
||||
"use strict";
|
||||
var i18n=Recorder.i18n;
|
||||
|
||||
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-1 End @@
|
||||
|
||||
//@@Exec i18n.lang="es";
|
||||
Recorder.CLog('Import Recorder i18n lang="es"');
|
||||
|
||||
//i18n.alias["other-lang-key"]="es";
|
||||
|
||||
var putSet={lang:"es"};
|
||||
|
||||
i18n.data["rtl$es"]=false;
|
||||
i18n.data["desc$es"]="Spanish, Español, 西班牙语。Esta traducción proviene principalmente de: traducción de google + traducción de Baidu, traducida del chino al español. 此翻译主要来自:google翻译+百度翻译,由中文翻译成西班牙语。";
|
||||
//@@Exec i18n.GenerateDisplayEnglish=true;
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=recorder-core.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@en="Duplicate import {1}"
|
||||
//@@Put0
|
||||
"K8zP:"+ //args: {1}
|
||||
"Importación duplicada {1}"
|
||||
|
||||
//@@zh="剩{1}个GetContext未close"
|
||||
//@@en="There are {1} GetContext unclosed"
|
||||
,"mSxV:"+ //args: {1}
|
||||
"Los {1} GetContext restantes no han sido close"
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用,否则将在rec.start时尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
//@@en=" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
|
||||
,"nMIy:"+ //no args
|
||||
" (Nota: ctx no está en estado running. Se debe llamar al menos a uno de rec.open y start durante la operación del usuario (tocar, hacer clic, etc.); de lo contrario, se intentará ctx.resume durante rec.start, lo que puede causar compatibilidad problemas (solo iOS), consulte la configuración de runningContext en la documentación) "
|
||||
|
||||
//@@zh="Stream的采样率{1}不等于{2},将进行采样率转换(注意:音质不会变好甚至可能变差),主要在移动端未禁用回声消除时会产生此现象,浏览器有回声消除时可能只会返回16k采样率的音频数据,"
|
||||
//@@en="The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
|
||||
,"eS8i:"+ //args: {1}-{2}
|
||||
"El sampleRate {1} de la transmisión no es igual a {2} y se realizará la conversión de sampleRate (nota: la calidad del sonido no mejorará o incluso puede empeorar). Este fenómeno ocurre principalmente cuando echoCancellation no está desactivado en el terminal móvil. Cuando el navegador tiene echoCancellation, es posible que solo se devuelvan datos de audio con una frecuencia de muestreo de 16k. "
|
||||
|
||||
//@@zh="。由于{1}内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启{1}。"
|
||||
//@@en=". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
|
||||
,"ZGlf:"+ //args: {1}
|
||||
". Debido a las 375 devoluciones de llamadas por segundo dentro de {1}, puede haber problemas de rendimiento en el lado móvil que pueden provocar que se pierdan las devoluciones de llamadas y que la grabación sea más corta. No hay ningún impacto en el lado de la PC. No se recomienda habilitar {1} por el momento."
|
||||
|
||||
//@@zh="Connect采用老的{1},"
|
||||
//@@en="Connect uses the old {1}, "
|
||||
,"7TU0:"+ //args: {1}
|
||||
"Connect utiliza el antiguo {1}, "
|
||||
|
||||
//@@zh="但已设置{1}尝试启用{2}"
|
||||
//@@en="But {1} is set trying to enable {2}"
|
||||
,"JwCL:"+ //args: {1}-{2}
|
||||
"Pero {1} está configurado para intentar habilitar {2}"
|
||||
|
||||
//@@zh="可设置{1}尝试启用{2}"
|
||||
//@@en="Can set {1} try to enable {2}"
|
||||
,"VGjB:"+ //args: {1}-{2}
|
||||
"Puedes configurar {1} para intentar habilitar {2}"
|
||||
|
||||
//@@zh="{1}未返回任何音频,恢复使用{2}"
|
||||
//@@en="{1} did not return any audio, reverting to {2}"
|
||||
,"MxX1:"+ //args: {1}-{2}
|
||||
"{1} no devolvió ningún audio, continúe usando {2}"
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
//@@en="{1} redundant callback"
|
||||
,"XUap:"+ //args: {1}
|
||||
"{1} devolución de llamada redundante"
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
|
||||
//@@en="Connect uses {1}, set {2} to restore old-fashioned {3}"
|
||||
,"yOta:"+ //args: {1}-{3}
|
||||
"Connect usa {1}, configurar {2} puede restaurar el antiguo {3}"
|
||||
|
||||
//@@zh="(此浏览器不支持{1})"
|
||||
//@@en=" (This browser does not support {1}) "
|
||||
,"VwPd:"+ //args: {1}
|
||||
" (Este navegador no soporta {1}) "
|
||||
|
||||
//@@zh="{1}未返回任何音频,降级使用{2}"
|
||||
//@@en="{1} did not return any audio, downgrade to {2}"
|
||||
,"vHnb:"+ //args: {1}-{2}
|
||||
"{1} no devuelve audio, se ha degradado para usar {2}"
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
//@@en="{1} redundant callback"
|
||||
,"O9P7:"+ //args: {1}
|
||||
"{1} devolución de llamada redundante"
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
|
||||
//@@en="Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
|
||||
,"LMEm:"+ //args: {1}-{4}
|
||||
"Connect usa {1}, configure {2} para volver a {3} o al antiguo {4}"
|
||||
|
||||
//@@zh="{1}的filter采样率变了,重设滤波"
|
||||
//@@en="The filter sampleRate of {1} has changed, reset the filter"
|
||||
,"d48C:"+ //args: {1}
|
||||
"La frecuencia de muestreo del filtro de {1} ha cambiado y el filtro se ha restablecido"
|
||||
|
||||
//@@zh="{1}似乎传入了未重置chunk {2}"
|
||||
//@@en="{1} seems to have passed in an unreset chunk {2}"
|
||||
,"tlbC:"+ //args: {1}-{2}
|
||||
"{1} parece haber introducido chunk {2} sin restablecer"
|
||||
|
||||
//@@zh="{1}和{2}必须是数值"
|
||||
//@@en="{1} and {2} must be number"
|
||||
,"VtS4:"+ //args: {1}-{2}
|
||||
"{1} y {2} deben ser valores numéricos"
|
||||
|
||||
//@@zh="录音open失败:"
|
||||
//@@en="Recording open failed: "
|
||||
,"5tWi:"+ //no args
|
||||
"Error al grabar open: "
|
||||
|
||||
//@@zh="open被取消"
|
||||
//@@en="open cancelled"
|
||||
,"dFm8:"+ //no args
|
||||
"open fue cancelado"
|
||||
|
||||
//@@zh="open被中断"
|
||||
//@@en="open interrupted"
|
||||
,"VtJO:"+ //no args
|
||||
"open fue interrumpido"
|
||||
|
||||
//@@zh=",可尝试使用RecordApp解决方案"
|
||||
//@@en=", you can try to use the RecordApp solution "
|
||||
,"EMJq:"+ //no args
|
||||
", puedes probar la solución RecordApp"
|
||||
|
||||
//@@zh="不能录音:"
|
||||
//@@en="Cannot record: "
|
||||
,"A5bm:"+ //no args
|
||||
"No se puede grabar: "
|
||||
|
||||
//@@zh="不支持此浏览器从流中获取录音"
|
||||
//@@en="This browser does not support obtaining recordings from stream"
|
||||
,"1iU7:"+ //no args
|
||||
"Este navegador no admite la recuperación de grabaciones de transmisiones"
|
||||
|
||||
//@@zh="从流中打开录音失败:"
|
||||
//@@en="Failed to open recording from stream: "
|
||||
,"BTW2:"+ //no args
|
||||
"No se pudo abrir la grabación desde la transmisión: "
|
||||
|
||||
//@@zh="无权录音(跨域,请尝试给iframe添加麦克风访问策略,如{1})"
|
||||
//@@en="No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
|
||||
,"Nclz:"+ //args: {1}
|
||||
"Sin permiso para grabar (entre dominios, intente agregar una política de acceso al micrófono al iframe, como {1})"
|
||||
|
||||
//@@zh=",无可用麦克风"
|
||||
//@@en=", no microphone available"
|
||||
,"jBa9:"+ //no args
|
||||
", no hay micrófono disponible"
|
||||
|
||||
//@@zh="用户拒绝了录音权限"
|
||||
//@@en="User denied recording permission"
|
||||
,"gyO5:"+ //no args
|
||||
"Usuario denegado permiso de grabación"
|
||||
|
||||
//@@zh="浏览器禁止不安全页面录音,可开启https解决"
|
||||
//@@en="Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
|
||||
,"oWNo:"+ //no args
|
||||
"El navegador prohíbe el registro de páginas no seguras, lo que se puede solucionar activando https"
|
||||
|
||||
//@@zh="此浏览器不支持录音"
|
||||
//@@en="This browser does not support recording"
|
||||
,"COxc:"+ //no args
|
||||
"Este navegador no admite la grabación"
|
||||
|
||||
//@@zh="发现同时多次调用open"
|
||||
//@@en="It was found that open was called multiple times at the same time"
|
||||
,"upb8:"+ //no args
|
||||
"Descubrí que se llamó a open varias veces al mismo tiempo"
|
||||
|
||||
//@@zh="录音功能无效:无音频流"
|
||||
//@@en="Invalid recording: no audio stream"
|
||||
,"Q1GA:"+ //no args
|
||||
"La función de grabación no funciona: no hay transmisión de audio"
|
||||
|
||||
//@@zh=",将尝试禁用回声消除后重试"
|
||||
//@@en=", will try to disable echoCancellation and try again"
|
||||
,"KxE2:"+ //no args
|
||||
", intentaré deshabilitar echoCancellation y volveré a intentarlo"
|
||||
|
||||
//@@zh="请求录音权限错误"
|
||||
//@@en="Error requesting recording permission"
|
||||
,"xEQR:"+ //no args
|
||||
"Error al solicitar permiso de grabación"
|
||||
|
||||
//@@zh="无法录音:"
|
||||
//@@en="Unable to record: "
|
||||
,"bDOG:"+ //no args
|
||||
"No se puede grabar: "
|
||||
|
||||
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
|
||||
//@@en="Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
|
||||
,"IjL3:"+ //args: {1}
|
||||
"Nota: Se ha configurado el parámetro {1}, lo que puede provocar que el navegador no seleccione correctamente el micrófono, o que el terminal móvil no habilite echoCancellation, etc. "
|
||||
|
||||
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除,移动端未禁用回声消除时可能会降低系统播放音量(关闭录音后可恢复)和仅提供16k采样率的音频流(不需要回声消除时可明确配置成禁用来获得48k高音质的流),请参阅文档中{2}配置"
|
||||
//@@en=", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
|
||||
,"RiWe:"+ //args: {1}-{2}
|
||||
", cuando no se configura {1}, el navegador puede habilitar automáticamente la cancelación de eco. Cuando la cancelación de eco no está deshabilitada en el terminal móvil, el volumen de reproducción del sistema puede reducirse (se puede restaurar después de cerrar la grabación) y solo se proporciona una transmisión de audio con una frecuencia de muestreo de 16 k (cuando no se requiere la cancelación de eco, se puede configurar explícitamente para que se deshabilite y se obtenga una transmisión de alta calidad de 48 k). Consulte la configuración {2} en el documento"
|
||||
|
||||
//@@zh="close被忽略(因为同时open了多个rec,只有最后一个会真正close)"
|
||||
//@@en="close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
|
||||
,"hWVz:"+ //no args
|
||||
"close se ignora (debido a que se abren varios recs al mismo tiempo, solo el último será realmente close)"
|
||||
|
||||
//@@zh="忽略"
|
||||
//@@en="ignore"
|
||||
,"UHvm:"+ //no args
|
||||
"descuido"
|
||||
|
||||
//@@zh="不支持{1}架构"
|
||||
//@@en="{1} architecture not supported"
|
||||
,"Essp:"+ //args: {1}
|
||||
"No es compatible con la arquitectura {1}"
|
||||
|
||||
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
|
||||
//@@en="{1} type does not support setting takeoffEncodeChunk"
|
||||
,"2XBl:"+ //args: {1}
|
||||
"El tipo {1} no admite la configuración de takeoffEncodeChunk"
|
||||
|
||||
//@@zh="(未加载编码器)"
|
||||
//@@en="(Encoder not loaded)"
|
||||
,"LG7e:"+ //no args
|
||||
"(sin codificador cargado)"
|
||||
|
||||
//@@zh="{1}环境不支持实时处理"
|
||||
//@@en="{1} environment does not support real-time processing"
|
||||
,"7uMV:"+ //args: {1}
|
||||
"El entorno {1} no admite el procesamiento en tiempo real"
|
||||
|
||||
//@@zh="补偿{1}ms"
|
||||
//@@en="Compensation {1}ms"
|
||||
,"4Kfd:"+ //args: {1}
|
||||
"Compensación {1}ms"
|
||||
|
||||
//@@zh="未补偿{1}ms"
|
||||
//@@en="Uncompensated {1}ms"
|
||||
,"bM5i:"+ //args: {1}
|
||||
"{1} ms sin compensar"
|
||||
|
||||
//@@zh="回调出错是不允许的,需保证不会抛异常"
|
||||
//@@en="Callback error is not allowed, you need to ensure that no exception will be thrown"
|
||||
,"gFUF:"+ //no args
|
||||
"No se permiten errores en las devoluciones de llamada y se debe garantizar que no se produzcan excepciones"
|
||||
|
||||
//@@zh="低性能,耗时{1}ms"
|
||||
//@@en="Low performance, took {1}ms"
|
||||
,"2ghS:"+ //args: {1}
|
||||
"Bajo rendimiento, tarda {1} ms"
|
||||
|
||||
//@@zh="未进入异步前不能清除buffers"
|
||||
//@@en="Buffers cannot be cleared before entering async"
|
||||
,"ufqH:"+ //no args
|
||||
"Los buffers no se pueden borrar antes de entrar asíncrono"
|
||||
|
||||
//@@zh="start失败:未open"
|
||||
//@@en="start failed: not open"
|
||||
,"6WmN:"+ //no args
|
||||
"start falló: no open"
|
||||
|
||||
//@@zh="start 开始录音,"
|
||||
//@@en="start recording, "
|
||||
,"kLDN:"+ //no args
|
||||
"start, comenzar a grabar, "
|
||||
|
||||
//@@zh="start被中断"
|
||||
//@@en="start was interrupted"
|
||||
,"Bp2y:"+ //no args
|
||||
"start fue interrumpido"
|
||||
|
||||
//@@zh=",可能无法录音:"
|
||||
//@@en=", may fail to record: "
|
||||
,"upkE:"+ //no args
|
||||
", es posible que no sea posible grabar: "
|
||||
|
||||
//@@zh="stop 和start时差:"
|
||||
//@@en="Stop and start time difference: "
|
||||
,"Xq4s:"+ //no args
|
||||
"stop, diferencia horaria con start: "
|
||||
|
||||
//@@zh="补偿:"
|
||||
//@@en="compensate: "
|
||||
,"3CQP:"+ //no args
|
||||
"compensar: "
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
//@@en="Failed to stop recording: "
|
||||
,"u8JG:"+ //no args
|
||||
"No se pudo finalizar la grabación: "
|
||||
|
||||
//@@zh=",请设置{1}"
|
||||
//@@en=", please set {1}"
|
||||
,"1skY:"+ //args: {1}
|
||||
", por favor establece {1}"
|
||||
|
||||
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
|
||||
//@@en="Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
|
||||
,"Wv7l:"+ //args: {1}-{3}
|
||||
"Finalizar la grabación. La codificación tarda {1} ms. La duración del audio es de {2} ms. El tamaño del archivo es {3}b"
|
||||
|
||||
//@@zh="{1}编码器返回的不是{2}"
|
||||
//@@en="{1} encoder returned not {2}"
|
||||
,"Vkbd:"+ //args: {1}-{2}
|
||||
"El codificador {1} no devuelve {2}"
|
||||
|
||||
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
|
||||
//@@en="After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
|
||||
,"QWnr:"+ //no args
|
||||
"Después de habilitar takeoffEncodeChunk, la longitud del blob devuelta por stop es 0 y no se proporcionan datos de audio"
|
||||
|
||||
//@@zh="生成的{1}无效"
|
||||
//@@en="Invalid generated {1}"
|
||||
,"Sz2H:"+ //args: {1}
|
||||
"El {1} generado no es válido"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"wf9t:"+ //no args
|
||||
"Grabación no iniciada"
|
||||
|
||||
//@@zh=",开始录音前无用户交互导致AudioContext未运行"
|
||||
//@@en=", No user interaction before starting recording, resulting in AudioContext not running"
|
||||
,"Dl2c:"+ //no args
|
||||
", no hay interacción del usuario antes de comenzar a grabar, lo que hace que AudioContext no se ejecute"
|
||||
|
||||
//@@zh="未采集到录音"
|
||||
//@@en="Recording not captured"
|
||||
,"Ltz3:"+ //no args
|
||||
"No se recopiló ninguna grabación"
|
||||
|
||||
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
|
||||
//@@en="The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
|
||||
,"xGuI:"+ //args: {1}-{2}
|
||||
"El codificador de {1} no está cargado. Intente encontrar el codificador de {1} en src/engine de {2} y cárguelo"
|
||||
|
||||
//@@zh="录音错误:"
|
||||
//@@en="Recording error: "
|
||||
,"AxOH:"+ //no args
|
||||
"Error de grabación: "
|
||||
|
||||
//@@zh="音频buffers被释放"
|
||||
//@@en="Audio buffers are released"
|
||||
,"xkKd:"+ //no args
|
||||
"Se liberan los buffers de audio"
|
||||
|
||||
//@@zh="采样:{1} 花:{2}ms"
|
||||
//@@en="Sampled: {1}, took: {2}ms"
|
||||
,"CxeT:"+ //args: {1}-{2}
|
||||
"Muestra: {1} Flor: {2}ms"
|
||||
|
||||
//@@zh="非浏览器环境,不支持{1}"
|
||||
//@@en="Non-browser environment, does not support {1}"
|
||||
,"NonBrowser-1:"+ //args: {1}
|
||||
"Entorno sin navegador, no es compatible con {1}"
|
||||
|
||||
//@@zh="参数错误:{1}"
|
||||
//@@en="Illegal argument: {1}"
|
||||
,"IllegalArgs-1:"+ //args: {1}
|
||||
"Error de parámetro: {1}"
|
||||
|
||||
//@@zh="调用{1}需要先导入{2}"
|
||||
//@@en="Calling {1} needs to import {2} first"
|
||||
,"NeedImport-2:"+ //args: {1}-{2}
|
||||
"Para llamar a {1}, primero debes importar {2}"
|
||||
|
||||
//@@zh="不支持:{1}"
|
||||
//@@en="Not support: {1}"
|
||||
,"NotSupport-1:"+ //args: {1}
|
||||
"No compatible: {1}"
|
||||
|
||||
//@@zh="覆盖导入{1}"
|
||||
//@@en="Override import {1}"
|
||||
,"8HO5:"+ //args: {1}
|
||||
"Anular importación {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=recorder-core.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-amr.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="AMR-NB(NarrowBand),采样率设置无效(只提供8000hz),比特率范围:{1}(默认12.2kbps),一帧20ms、{2}字节;浏览器一般不支持播放amr格式,可用Recorder.amr2wav()转码成wav播放"
|
||||
//@@en="AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
|
||||
//@@Put0
|
||||
"b2mN:"+ //args: {1}-{2}
|
||||
"AMR-NB (NarrowBand), la configuración sampleRate no es válida (solo se proporcionan 8000 hz), rango bitRate: {1} (predeterminado 12.2 kbps), un cuadro de 20 ms, {2} bytes; los navegadores generalmente no admiten la reproducción en formato amr, disponible Recorder.amr2wav() Transcodifica a wav para reproducción"
|
||||
|
||||
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
|
||||
//@@en="AMR Info: does not match the set {1}, has been updated to {2}"
|
||||
,"tQBv:"+ //args: {1}-{2}
|
||||
"AMR Info: no coincide con el conjunto {1}, se ha actualizado a {2}"
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
//@@en="Data sampleRate lower than {1}"
|
||||
,"q12D:"+ //args: {1}
|
||||
"Los datos sampleRate están por debajo de {1}"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"TxjV:"+ //no args
|
||||
"La versión actual del navegador es demasiado baja y no se puede procesar en tiempo real"
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据,只有首次回调数据(首帧)包含AMR头;在合并成AMR文件时,如果没有把首帧数据包含进去,则必须在文件开头添加上AMR头:Recorder.AMR.AMR_HEADER(转成二进制),否则无法播放"
|
||||
//@@en="takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
|
||||
,"Q7p7:"+ //no args
|
||||
"takeoffEncodeChunk se hace cargo de la salida de datos binarios del codificador AMR. Solo los primeros datos de devolución de llamada (primer cuadro) contienen el encabezado AMR; al fusionarlos en un archivo AMR, si los datos del primer cuadro no están incluidos, el encabezado AMR debe agregarse en el comienzo del archivo: Recorder.AMR.AMR_HEADER (convertido a binario), de lo contrario no se puede reproducir"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,amr实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
|
||||
,"6o9Z:"+ //no args
|
||||
"El entorno actual no es compatible con Web Worker y el codificador en tiempo real amr se ejecuta en el hilo principal"
|
||||
|
||||
//@@zh="amr worker剩{1}个未stop"
|
||||
//@@en="amr worker left {1} unstopped"
|
||||
,"yYWs:"+ //args: {1}
|
||||
"a amr worker le quedan {1} no stop"
|
||||
|
||||
//@@zh="amr编码器未start"
|
||||
//@@en="amr encoder not started"
|
||||
,"jOi8:"+ //no args
|
||||
"codificador amr no start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-amr.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-ogg.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="Ogg Vorbis,比特率取值16-100kbps,采样率取值无限制"
|
||||
//@@en="Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
|
||||
//@@Put0
|
||||
"O8Gn:"+ //no args
|
||||
"Ogg Vorbis, el valor de bitRate es de 16-100 kbps, el valor de sampleRate es ilimitado"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"5si6:"+ //no args
|
||||
"La versión actual del navegador es demasiado baja y no se puede procesar en tiempo real"
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据,Ogg由数据页组成,一页包含多帧音频数据(含几秒的音频,一页数据无法单独解码和播放),此编码器每次输出都是完整的一页数据,因此实时性会比较低;在合并成完整ogg文件时,必须将输出的所有数据合并到一起,否则可能无法播放,不支持截取中间一部分单独解码和播放"
|
||||
//@@en="takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
|
||||
,"R8yz:"+ //no args
|
||||
"takeoffEncodeChunk se hace cargo de la salida de datos binarios del codificador OggVorbis. Ogg se compone de páginas de datos. Una página contiene múltiples fotogramas de datos de audio (incluidos varios segundos de audio. Una página de datos no se puede decodificar ni reproducir por separado). Cada salida de este codificador está completo. Una página de datos, por lo que el rendimiento en tiempo real será relativamente bajo; al fusionar en un archivo ogg completo, todos los datos de salida deben fusionarse; de lo contrario, es posible que no se reproduzca y no se admita intercepte la parte media y decodifíquela y reprodúzcala por separado"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,OggVorbis实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
|
||||
,"hB9D:"+ //no args
|
||||
"El entorno actual no admite Web Workers y el codificador en tiempo real OggVorbis se ejecuta en el hilo principal"
|
||||
|
||||
//@@zh="ogg worker剩{1}个未stop"
|
||||
//@@en="There are {1} unstopped ogg workers"
|
||||
,"oTiy:"+ //args: {1}
|
||||
"a ogg worker le quedan {1} no stop"
|
||||
|
||||
//@@zh="ogg编码器未start"
|
||||
//@@en="ogg encoder not started"
|
||||
,"dIpw:"+ //no args
|
||||
"codificador ogg no start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-ogg.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-webm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="此浏览器不支持进行webm编码,未实现MediaRecorder"
|
||||
//@@en="This browser does not support webm encoding, MediaRecorder is not implemented"
|
||||
//@@Put0
|
||||
"L49q:"+ //no args
|
||||
"Este navegador no admite la codificación webm y MediaRecorder no está implementado"
|
||||
|
||||
//@@zh="只有比较新的浏览器支持,压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法,只能按照类似边播放边收听形式把数据导入到MediaRecorder,有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小,但音频文件中的比特率并非设定比特率,采样率由于是我们自己采样的,到这个编码器随他怎么搞"
|
||||
//@@en="Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
|
||||
,"tsTW:"+ //no args
|
||||
"Sólo los navegadores más nuevos lo admiten y la tasa de compresión es similar a la de mp3. Dado que no hay forma de codificar rápidamente los datos pcm existentes, los datos sólo se pueden importar a MediaRecorder de forma similar a la reproducción y escucha, y hay que esperar unos segundos. Aunque el tamaño del archivo de audio de salida se puede controlar mediante la velocidad de bits, la velocidad de bits en el archivo de audio no es la velocidad de bits establecida. Dado que la frecuencia de muestreo la probamos nosotros mismos, podemos hacer lo que queramos con este codificador"
|
||||
|
||||
//@@zh="此浏览器不支持把录音转成webm格式"
|
||||
//@@en="This browser does not support converting recordings to webm format"
|
||||
,"aG4z:"+ //no args
|
||||
"Este navegador no admite la conversión de grabaciones al formato webm"
|
||||
|
||||
//@@zh="转码webm出错:{1}"
|
||||
//@@en="Error encoding webm: {1}"
|
||||
,"PIX0:"+ //args: {1}
|
||||
"Error al transcodificar webm: {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-webm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/g711x.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1};{2}音频文件无法直接播放,可用Recorder.{2}2wav()转码成wav播放;采样率比特率设置无效,固定为8000hz采样率、16位,每个采样压缩成8位存储,音频文件大小为8000字节/秒;如需任意采样率支持,请使用Recorder.{2}_encode()方法"
|
||||
//@@en="{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
|
||||
//@@Put0
|
||||
"d8YX:"+ //args: {1}-{2}
|
||||
"{1}; {2} El archivo de audio no se puede reproducir directamente. Puede utilizar Recorder.{2}2wav() para transcodificarlo a wav para su reproducción; la configuración de velocidad de bits de frecuencia de muestreo no es válida y está fijada en 8000 hz de muestreo velocidad, 16 bits, y cada muestra está comprimida. en un almacenamiento de 8 bits, el tamaño del archivo de audio es 8000 bytes/segundo; si necesita compatibilidad con cualquier frecuencia de muestreo, utilice el método Recorder.{2}_encode()"
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
//@@en="Data sampleRate lower than {1}"
|
||||
,"29UK:"+ //args: {1}
|
||||
"Los datos sampleRate están por debajo de {1}"
|
||||
|
||||
//@@zh="{1}编码器未start"
|
||||
//@@en="{1} encoder not started"
|
||||
,"quVJ:"+ //args: {1}
|
||||
"codificador {1} no start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/g711x.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/mp3.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="采样率范围:{1};比特率范围:{2}(不同比特率支持的采样率范围不同,小于32kbps时采样率需小于32000)"
|
||||
//@@en="sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
|
||||
//@@Put0
|
||||
"Zm7L:"+ //args: {1}-{2}
|
||||
"rango sampleRate: {1}; rango bitRate: {2} (diferentes bitRate admiten diferentes rangos sampleRate. Cuando es inferior a 32 kbps, sampleRate debe ser inferior a 32000)"
|
||||
|
||||
//@@zh="{1}不在mp3支持的取值范围:{2}"
|
||||
//@@en="{1} is not in the value range supported by mp3: {2}"
|
||||
,"eGB9:"+ //args: {1}-{2}
|
||||
"{1} no está en el rango de valores soportado por mp3: {2}"
|
||||
|
||||
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围:{3}"
|
||||
//@@en="sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
|
||||
,"zLTa:"+ //args: {1}-{3}
|
||||
"sampleRate se ha actualizado a {1} porque {2} no está en el rango de valores admitido por mp3: {3}"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"yhUs:"+ //no args
|
||||
"La versión actual del navegador es demasiado baja y no se puede procesar en tiempo real"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,mp3实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
|
||||
,"k9PT:"+ //no args
|
||||
"El entorno actual no es compatible con Web Worker y el codificador en tiempo real mp3 se ejecuta en el hilo principal"
|
||||
|
||||
//@@zh="mp3 worker剩{1}个未stop"
|
||||
//@@en="There are {1} unstopped mp3 workers left"
|
||||
,"fT6M:"+ //args: {1}
|
||||
"a mp3 worker le quedan {1} no stop"
|
||||
|
||||
//@@zh="mp3编码器未start"
|
||||
//@@en="mp3 encoder not started"
|
||||
,"mPxH:"+ //no args
|
||||
"codificador mp3 no start"
|
||||
|
||||
//@@zh="和设置的不匹配{1},已更新成{2}"
|
||||
//@@en="Does not match the set {1}, has been updated to {2}"
|
||||
,"uY9i:"+ //args: {1}-{2}
|
||||
"No coincide con la configuración {1}, se ha actualizado a {2}"
|
||||
|
||||
//@@zh="Fix移除{1}帧"
|
||||
//@@en="Fix remove {1} frame"
|
||||
,"iMSm:"+ //args: {1}
|
||||
"Fix elimina {1} fotogramas"
|
||||
|
||||
//@@zh="移除帧数过多"
|
||||
//@@en="Remove too many frames"
|
||||
,"b9zm:"+ //no args
|
||||
"Eliminar demasiados fotogramas"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/mp3.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="pcm为未封装的原始音频数据,pcm音频文件无法直接播放,可用Recorder.pcm2wav()转码成wav播放;支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
//@@en="pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
|
||||
//@@Put0
|
||||
"fWsN:"+ //no args
|
||||
"pcm son datos de audio originales no encapsulados. Los archivos de audio Pcm no se pueden reproducir directamente. Recorder.pcm2wav() se puede utilizar para transcodificar a wav para su reproducción. Admite dígitos de 8 y 16 bits (rellene bitRate) y el valor de sampleRate es ilimitado"
|
||||
|
||||
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
|
||||
//@@en="PCM Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
,"uMUJ:"+ //args: {1}-{2}
|
||||
"PCM Info: El bit {1} no es compatible y se ha actualizado al bit {2}"
|
||||
|
||||
//@@zh="pcm2wav必须提供sampleRate和bitRate"
|
||||
//@@en="pcm2wav must provide sampleRate and bitRate"
|
||||
,"KmRz:"+ //no args
|
||||
"pcm2wav debe proporcionar sampleRate y bitRate"
|
||||
|
||||
//@@zh="pcm编码器未start"
|
||||
//@@en="pcm encoder not started"
|
||||
,"sDkA:"+ //no args
|
||||
"codificador pcm no start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/wav.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="支持位数8位、16位(填在比特率里面),采样率取值无限制;此编码器仅在pcm数据前加了一个44字节的wav头,编码出来的16位wav文件去掉开头的44字节即可得到pcm(注:其他wav编码器可能不是44字节)"
|
||||
//@@en="Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
|
||||
//@@Put0
|
||||
"gPSE:"+ //no args
|
||||
"Admite dígitos de 8 y 16 bits (completados en bitRate) y el valor de sampleRate es ilimitado; este codificador solo agrega un encabezado wav de 44 bytes antes de los datos pcm, y el archivo wav codificado de 16 bits elimina los 44 bits iniciales. Bytes para obtener pcm (nota: es posible que otros codificadores WAV no tengan 44 bytes)"
|
||||
|
||||
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
|
||||
//@@en="WAV Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
,"wyw9:"+ //args: {1}-{2}
|
||||
"WAV Info: El bit {1} no es compatible y se ha actualizado al bit {2}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/wav.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="getAudioSrc方法已过时:请直接使用getMediaStream然后赋值给audio.srcObject,仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
|
||||
//@@en="The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
|
||||
//@@Put0
|
||||
"0XYC:"+ //no args
|
||||
"El método getAudioSrc está obsoleto: utilice getMediaStream directamente y asígnelo a audio.srcObject. Solo se permite llamar a este método en navegadores que no admiten srcObject y asignarlo a audio.src por compatibilidad"
|
||||
|
||||
//@@zh="start被stop终止"
|
||||
//@@en="start is terminated by stop"
|
||||
,"6DDt:"+ //no args
|
||||
"start es cancelado por stop"
|
||||
|
||||
//@@zh="{1}多次start"
|
||||
//@@en="{1} repeat start"
|
||||
,"I4h4:"+ //args: {1}
|
||||
"{1} se repite start"
|
||||
|
||||
//@@zh="浏览器不支持打开{1}"
|
||||
//@@en="The browser does not support opening {1}"
|
||||
,"P6Gs:"+ //args: {1}
|
||||
"El navegador no admite la apertura de {1}"
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,start需要在用户操作(触摸、点击等)时进行调用,否则会尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
//@@en=" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
|
||||
,"JwDm:"+ //no args
|
||||
" (Nota: ctx no está en estado running. Es necesario llamar a start cuando el usuario opera (tocar, hacer clic, etc.); de lo contrario, se intentará ctx.resume, lo que puede causar problemas de compatibilidad (solo iOS). Consulte la configuración runningContext en el documento) "
|
||||
|
||||
//@@zh="此浏览器的AudioBuffer实现不支持动态特性,采用兼容模式"
|
||||
//@@en="The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
|
||||
,"qx6X:"+ //no args
|
||||
"La implementación AudioBuffer de este navegador no admite funciones dinámicas y utiliza el modo de compatibilidad"
|
||||
|
||||
//@@zh="环境检测超时"
|
||||
//@@en="Environment detection timeout"
|
||||
,"cdOx:"+ //no args
|
||||
"Tiempo de espera de detección del entorno"
|
||||
|
||||
//@@zh="可能无法播放:{1}"
|
||||
//@@en="Could not play: {1}"
|
||||
,"S2Bu:"+ //args: {1}
|
||||
"No puede jugar: {1}"
|
||||
|
||||
//@@zh="input调用失败:非pcm[Int16,...]输入时,必须解码或者使用transform转换"
|
||||
//@@en="input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
|
||||
,"ZfGG:"+ //no args
|
||||
"Falló la llamada input: no PCM [int16,...] al ingresar, se debe decodificar o usar la conversión transform"
|
||||
|
||||
//@@zh="input调用失败:未提供sampleRate"
|
||||
//@@en="input call failed: sampleRate not provided"
|
||||
,"N4ke:"+ //no args
|
||||
"Falló la llamada input: no se proporcionó sampleRate"
|
||||
|
||||
//@@zh="input调用失败:data的sampleRate={1}和之前的={2}不同"
|
||||
//@@en="input call failed: sampleRate={1} of data is different from previous={2}"
|
||||
,"IHZd:"+ //args: {1}-{2}
|
||||
"Falló la llamada a input: sampleRate={1} de los datos es diferente de la anterior ={2}"
|
||||
|
||||
//@@zh="延迟过大,已丢弃{1}ms {2}"
|
||||
//@@en="The delay is too large, {1}ms has been discarded, {2}"
|
||||
,"L8sC:"+ //args: {1}-{2}
|
||||
"El retraso es demasiado grande, se han descartado {1}ms, {2}"
|
||||
|
||||
//@@zh="{1}未调用start方法"
|
||||
//@@en="{1} did not call the start method"
|
||||
,"TZPq:"+ //args: {1}
|
||||
"{1} no se llama al método start"
|
||||
|
||||
//@@zh="浏览器不支持音频解码"
|
||||
//@@en="Browser does not support audio decoding"
|
||||
,"iCFC:"+ //no args
|
||||
"El navegador no admite decodificación de audio"
|
||||
|
||||
//@@zh="音频解码数据必须是ArrayBuffer"
|
||||
//@@en="Audio decoding data must be ArrayBuffer"
|
||||
,"wE2k:"+ //no args
|
||||
"Los datos de decodificación de audio deben ser ArrayBuffer"
|
||||
|
||||
//@@zh="音频解码失败:{1}"
|
||||
//@@en="Audio decoding failed: {1}"
|
||||
,"mOaT:"+ //args: {1}
|
||||
"Falló la decodificación de audio: {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/buffer_stream.player.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="符号[{1}]无效:{2}"
|
||||
//@@en="Invalid symbol [{1}]: {2}"
|
||||
//@@Put0
|
||||
"3RBa:"+ //args: {1}-{2}
|
||||
"Símbolo [{1}] no válido: {2}"
|
||||
|
||||
//@@zh="音符[{1}]无效:{2}"
|
||||
//@@en="Invalid note [{1}]: {2}"
|
||||
,"U212:"+ //args: {1}-{2}
|
||||
"Nota [{1}] no válido: {2}"
|
||||
|
||||
//@@zh="多个音时必须对齐,相差{1}ms"
|
||||
//@@en="Multiple tones must be aligned, with a difference of {1}ms"
|
||||
,"7qAD:"+ //args: {1}
|
||||
"Hay que alinearse a la hora de múltiples sonidos, con diferencias {1}ms"
|
||||
|
||||
//@@zh="祝你生日快乐"
|
||||
//@@en="Happy Birthday to You"
|
||||
,"QGsW:"+ //no args
|
||||
"Happy Birthday to You"
|
||||
|
||||
//@@zh="致爱丽丝"
|
||||
//@@en="For Elise"
|
||||
,"emJR:"+ //no args
|
||||
"For Elise"
|
||||
|
||||
//@@zh="卡农-右手简谱"
|
||||
//@@en="Canon - Right Hand Notation"
|
||||
,"GsYy:"+ //no args
|
||||
"Canon - símbolo de la mano derecha"
|
||||
|
||||
//@@zh="卡农"
|
||||
//@@en="Canon"
|
||||
,"bSFZ:"+ //no args
|
||||
"Canon"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/sonic.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,不支持调用Sonic.Async"
|
||||
//@@en="The current environment does not support Web Worker and does not support calling Sonic.Async"
|
||||
//@@Put0
|
||||
"Ikdz:"+ //no args
|
||||
"El entorno actual no admite Web Worker, no admite llamadas a Sonic.Async"
|
||||
|
||||
//@@zh="sonic worker剩{1}个未flush"
|
||||
//@@en="There are {1} unflushed sonic workers left"
|
||||
,"IC5Y:"+ //args: {1}
|
||||
"sonic worker deja {1} sin flush"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/sonic.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app-native-support.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
|
||||
//@@en="The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
|
||||
//@@Put0
|
||||
"WWoj:"+ //args: {1}-{3}
|
||||
"El método {2} en {1} no se implementa, por favor implemente este método en el archivo {3} o en el archivo de configuración"
|
||||
|
||||
//@@zh="未开始录音,但收到Native PCM数据"
|
||||
//@@en="Recording does not start, but Native PCM data is received"
|
||||
,"rCAM:"+ //no args
|
||||
"No se inició la grabación, pero se recibieron los datos de Native PCM"
|
||||
|
||||
//@@zh="检测到跨域iframe,NativeRecordReceivePCM无法注入到顶层,已监听postMessage转发兼容传输数据,请自行实现将top层接收到数据转发到本iframe(不限层),不然无法接收到录音数据"
|
||||
//@@en="A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
|
||||
,"t2OF:"+ //no args
|
||||
"Detectado iframe entre dominios, NativeRecordReceivePCM no se puede inyectar en el nivel superior, se han monitoreado los datos de transmisión compatibles con el reenvío de postMessage, por favor, realice por sí mismo el reenvío de los datos recibidos en la capa superior a este iframe (capa ilimitada), de lo contrario no se pueden recibir los datos de grabación"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"Z2y2:"+ //no args
|
||||
"Grabación no iniciada"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app-native-support.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@en="Duplicate import {1}"
|
||||
//@@Put0
|
||||
"uXtA:"+ //args: {1}
|
||||
"Importación duplicada {1}"
|
||||
|
||||
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
|
||||
//@@en="Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
|
||||
,"kIBu:"+ //args: {1}
|
||||
"Nota: Debido a que se llaman simultáneamente otros métodos relacionados con la grabación, el resultado de la llamada actual de {1} se ha descartado y no habrá devolución de llamada"
|
||||
|
||||
//@@zh="重复注册{1}"
|
||||
//@@en="Duplicate registration {1}"
|
||||
,"ha2K:"+ //args: {1}
|
||||
"Doble registro {1}"
|
||||
|
||||
//@@zh="仅清理资源"
|
||||
//@@en="Clean resources only"
|
||||
,"wpTL:"+ //no args
|
||||
"Solo limpiar recursos"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"bpvP:"+ //no args
|
||||
"Grabación no iniciada"
|
||||
|
||||
//@@zh="当前环境不支持实时回调,无法进行{1}"
|
||||
//@@en="The current environment does not support real-time callback and cannot be performed {1}"
|
||||
,"fLJD:"+ //args: {1}
|
||||
"El entorno actual no admite devoluciones de llamada en tiempo real y no se puede realizar {1}"
|
||||
|
||||
//@@zh="录音权限请求失败:"
|
||||
//@@en="Recording permission request failed: "
|
||||
,"YnzX:"+ //no args
|
||||
"La solicitud de permiso de grabación falló: "
|
||||
|
||||
//@@zh="需先调用{1}"
|
||||
//@@en="Need to call {1} first"
|
||||
,"nwKR:"+ //args: {1}
|
||||
"Primero hay que llamar a {1}"
|
||||
|
||||
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
|
||||
//@@en="This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
|
||||
,"citA:"+ //args: {1}-{2}
|
||||
"Actualmente no es un entorno de navegador, es necesario introducir un archivo de soporte para esta plataforma ({1}), o llamar al {2} para lograr su propio acceso."
|
||||
|
||||
//@@zh="开始录音失败:"
|
||||
//@@en="Failed to start recording: "
|
||||
,"ecp9:"+ //no args
|
||||
"Falló al iniciar la grabación: "
|
||||
|
||||
//@@zh="不能录音:"
|
||||
//@@en="Cannot record: "
|
||||
,"EKmS:"+ //no args
|
||||
"No se puede grabar: "
|
||||
|
||||
//@@zh="已开始录音"
|
||||
//@@en="Recording started"
|
||||
,"k7Qo:"+ //no args
|
||||
"Se ha iniciado la grabación"
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
//@@en="Failed to stop recording: "
|
||||
,"Douz:"+ //no args
|
||||
"Falló al finalizar la grabación: "
|
||||
|
||||
//@@zh="和Start时差:{1}ms"
|
||||
//@@en="Time difference from Start: {1}ms"
|
||||
,"wqSH:"+ //args: {1}
|
||||
"Diferencia horaria con start: {1}ms"
|
||||
|
||||
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
|
||||
//@@en="Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
|
||||
,"g3VX:"+ //args: {1}-{4}
|
||||
"Terminar la grabación lleva tiempo {1}ms , duración del audio {2}ms , tamaño del archivo {3}b , {4}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app.js ***************
|
||||
|
||||
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-2 End @@
|
||||
|
||||
}));
|
935
public/i18n/fr.js
Normal file
935
public/i18n/fr.js
Normal file
@ -0,0 +1,935 @@
|
||||
/*
|
||||
Recorder i18n/fr.js
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
Usage: Recorder.i18n.lang="fr"
|
||||
|
||||
Desc: French, Français, 法语。Cette traduction provient principalement de : traduction google + traduction Baidu, traduit du chinois vers le français. 此翻译主要来自:google翻译+百度翻译,由中文翻译成法语。
|
||||
|
||||
注意:请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成,只允许在字符串中填写翻译后的文本,请勿改变代码结构;翻译的文本如果需要明确的空值,请填写"=Empty";文本中的变量用{n}表示(n代表第几个变量),所有变量必须都出现至少一次,如果不要某变量用{n!}表示
|
||||
|
||||
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
factory(win.Recorder,browser);
|
||||
}(function(Recorder,isBrowser){
|
||||
"use strict";
|
||||
var i18n=Recorder.i18n;
|
||||
|
||||
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-1 End @@
|
||||
|
||||
//@@Exec i18n.lang="fr";
|
||||
Recorder.CLog('Import Recorder i18n lang="fr"');
|
||||
|
||||
//i18n.alias["other-lang-key"]="fr";
|
||||
|
||||
var putSet={lang:"fr"};
|
||||
|
||||
i18n.data["rtl$fr"]=false;
|
||||
i18n.data["desc$fr"]="French, Français, 法语。Cette traduction provient principalement de : traduction google + traduction Baidu, traduit du chinois vers le français. 此翻译主要来自:google翻译+百度翻译,由中文翻译成法语。";
|
||||
//@@Exec i18n.GenerateDisplayEnglish=true;
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=recorder-core.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@en="Duplicate import {1}"
|
||||
//@@Put0
|
||||
"K8zP:"+ //args: {1}
|
||||
"Répéter l'importation {1}"
|
||||
|
||||
//@@zh="剩{1}个GetContext未close"
|
||||
//@@en="There are {1} GetContext unclosed"
|
||||
,"mSxV:"+ //args: {1}
|
||||
"Les {1} GetContext restants n'ont pas été close"
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,rec.open和start至少要有一个在用户操作(触摸、点击等)时进行调用,否则将在rec.start时尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
//@@en=" (Note: ctx is not in the running state. At least one of rec.open and start must be called during user operations (touch, click, etc.), otherwise ctx.resume will be attempted during rec.start, which may cause compatibility issues (iOS only), please refer to the runningContext configuration in the documentation) "
|
||||
,"nMIy:"+ //no args
|
||||
" (Remarque : ctx n'est pas dans l'état running. Au moins l'un des rec.open et start doit être appelé pendant l'opération de l'utilisateur (toucher, cliquer, etc.), sinon ctx.resume sera tenté pendant rec.start, ce qui peut entraîner une compatibilité. problèmes (iOS uniquement), voir la configuration de runningContext dans la documentation) "
|
||||
|
||||
//@@zh="Stream的采样率{1}不等于{2},将进行采样率转换(注意:音质不会变好甚至可能变差),主要在移动端未禁用回声消除时会产生此现象,浏览器有回声消除时可能只会返回16k采样率的音频数据,"
|
||||
//@@en="The sampleRate of the Stream {1} is not equal to {2}, so the sampleRate conversion will be performed (note: the sound quality will not improve and may even deteriorate). This phenomenon mainly occurs when echoCancellation is not disabled on the mobile terminal. When the browser has echoCancellation, it may only return audio data with a sampleRate of 16k. "
|
||||
,"eS8i:"+ //args: {1}-{2}
|
||||
"Si le sampleRate {1} du Stream n'est pas égal à {2}, la conversion sampleRate sera effectuée (attention : la qualité du son ne s'améliorera pas ou pourra même se dégrader. Ce phénomène se produit principalement lorsque echoCancellation n'est pas désactivé sur le mobile). terminal Lorsque le navigateur a echoCancellation, seules les données audio avec un taux d'échantillonnage de 16k seront renvoyées. "
|
||||
|
||||
//@@zh="。由于{1}内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启{1}。"
|
||||
//@@en=". Due to 375 callbacks in 1 second in {1}, there may be performance problems on the mobile side, which may cause the callback to be lost and the recording to be shortened, but it will not affect the PC side. It is not recommended to enable {1} for now."
|
||||
,"ZGlf:"+ //args: {1}
|
||||
". En raison des 375 rappels par seconde dans un délai de {1}, il peut y avoir des problèmes de performances du côté mobile qui peuvent entraîner la perte des rappels et un raccourcissement de l'enregistrement. Il n'y a aucun impact du côté PC. Il n'est pas recommandé d'activer {1} pour le moment."
|
||||
|
||||
//@@zh="Connect采用老的{1},"
|
||||
//@@en="Connect uses the old {1}, "
|
||||
,"7TU0:"+ //args: {1}
|
||||
"Connect utilise l'ancien {1}, "
|
||||
|
||||
//@@zh="但已设置{1}尝试启用{2}"
|
||||
//@@en="But {1} is set trying to enable {2}"
|
||||
,"JwCL:"+ //args: {1}-{2}
|
||||
"Mais {1} est configuré pour essayer d'activer {2}"
|
||||
|
||||
//@@zh="可设置{1}尝试启用{2}"
|
||||
//@@en="Can set {1} try to enable {2}"
|
||||
,"VGjB:"+ //args: {1}-{2}
|
||||
"Vous pouvez configurer {1} pour essayer d'activer {2}"
|
||||
|
||||
//@@zh="{1}未返回任何音频,恢复使用{2}"
|
||||
//@@en="{1} did not return any audio, reverting to {2}"
|
||||
,"MxX1:"+ //args: {1}-{2}
|
||||
"{1} n'a renvoyé aucun son et a repris l'utilisation de {2}"
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
//@@en="{1} redundant callback"
|
||||
,"XUap:"+ //args: {1}
|
||||
"{1} rappel redondant"
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复老式{3}"
|
||||
//@@en="Connect uses {1}, set {2} to restore old-fashioned {3}"
|
||||
,"yOta:"+ //args: {1}-{3}
|
||||
"Connect utilise {1} et la configuration de {2} peut restaurer l'ancien {3}"
|
||||
|
||||
//@@zh="(此浏览器不支持{1})"
|
||||
//@@en=" (This browser does not support {1}) "
|
||||
,"VwPd:"+ //args: {1}
|
||||
" (Ce navigateur ne prend pas en charge {1}) "
|
||||
|
||||
//@@zh="{1}未返回任何音频,降级使用{2}"
|
||||
//@@en="{1} did not return any audio, downgrade to {2}"
|
||||
,"vHnb:"+ //args: {1}-{2}
|
||||
"{1} ne renvoie aucun audio, rétrogradez pour utiliser {2}"
|
||||
|
||||
//@@zh="{1}多余回调"
|
||||
//@@en="{1} redundant callback"
|
||||
,"O9P7:"+ //args: {1}
|
||||
"{1} rappel redondant"
|
||||
|
||||
//@@zh="Connect采用{1},设置{2}可恢复使用{3}或老式{4}"
|
||||
//@@en="Connect uses {1}, set {2} to restore to using {3} or old-fashioned {4}"
|
||||
,"LMEm:"+ //args: {1}-{4}
|
||||
"Connect utilise {1}, la configuration de {2} peut restaurer l'utilisation de {3} ou de l'ancien {4}"
|
||||
|
||||
//@@zh="{1}的filter采样率变了,重设滤波"
|
||||
//@@en="The filter sampleRate of {1} has changed, reset the filter"
|
||||
,"d48C:"+ //args: {1}
|
||||
"Le taux d'échantillonnage du filtre de {1} a changé, réinitialisez le filtre"
|
||||
|
||||
//@@zh="{1}似乎传入了未重置chunk {2}"
|
||||
//@@en="{1} seems to have passed in an unreset chunk {2}"
|
||||
,"tlbC:"+ //args: {1}-{2}
|
||||
"{1} semble passer dans chunk {2} qui n'est pas réinitialisé"
|
||||
|
||||
//@@zh="{1}和{2}必须是数值"
|
||||
//@@en="{1} and {2} must be number"
|
||||
,"VtS4:"+ //args: {1}-{2}
|
||||
"{1} et {2} doivent être des valeurs numériques"
|
||||
|
||||
//@@zh="录音open失败:"
|
||||
//@@en="Recording open failed: "
|
||||
,"5tWi:"+ //no args
|
||||
"L'enregistrement de open a échoué : "
|
||||
|
||||
//@@zh="open被取消"
|
||||
//@@en="open cancelled"
|
||||
,"dFm8:"+ //no args
|
||||
"open a été annulé"
|
||||
|
||||
//@@zh="open被中断"
|
||||
//@@en="open interrupted"
|
||||
,"VtJO:"+ //no args
|
||||
"open a été interrompu"
|
||||
|
||||
//@@zh=",可尝试使用RecordApp解决方案"
|
||||
//@@en=", you can try to use the RecordApp solution "
|
||||
,"EMJq:"+ //no args
|
||||
", vous pouvez essayer la solution RecordApp"
|
||||
|
||||
//@@zh="不能录音:"
|
||||
//@@en="Cannot record: "
|
||||
,"A5bm:"+ //no args
|
||||
"Impossible d'enregistrer : "
|
||||
|
||||
//@@zh="不支持此浏览器从流中获取录音"
|
||||
//@@en="This browser does not support obtaining recordings from stream"
|
||||
,"1iU7:"+ //no args
|
||||
"Ce navigateur ne prend pas en charge la récupération d'enregistrements à partir de flux"
|
||||
|
||||
//@@zh="从流中打开录音失败:"
|
||||
//@@en="Failed to open recording from stream: "
|
||||
,"BTW2:"+ //no args
|
||||
"Échec de l'ouverture de l'enregistrement à partir du flux : "
|
||||
|
||||
//@@zh="无权录音(跨域,请尝试给iframe添加麦克风访问策略,如{1})"
|
||||
//@@en="No permission to record (cross domain, please try adding microphone access policy to iframe, such as: {1})"
|
||||
,"Nclz:"+ //args: {1}
|
||||
"Aucune autorisation d'enregistrement (inter-domaine, veuillez essayer d'ajouter une stratégie d'accès au microphone à l'iframe, telle que {1})"
|
||||
|
||||
//@@zh=",无可用麦克风"
|
||||
//@@en=", no microphone available"
|
||||
,"jBa9:"+ //no args
|
||||
", pas de micro disponible"
|
||||
|
||||
//@@zh="用户拒绝了录音权限"
|
||||
//@@en="User denied recording permission"
|
||||
,"gyO5:"+ //no args
|
||||
"L'utilisateur a refusé l'autorisation d'enregistrement"
|
||||
|
||||
//@@zh="浏览器禁止不安全页面录音,可开启https解决"
|
||||
//@@en="Browser prohibits recording of unsafe pages, which can be resolved by enabling HTTPS"
|
||||
,"oWNo:"+ //no args
|
||||
"Le navigateur interdit l'enregistrement des pages dangereuses, ce qui peut être résolu en activant https"
|
||||
|
||||
//@@zh="此浏览器不支持录音"
|
||||
//@@en="This browser does not support recording"
|
||||
,"COxc:"+ //no args
|
||||
"Ce navigateur ne prend pas en charge l'enregistrement"
|
||||
|
||||
//@@zh="发现同时多次调用open"
|
||||
//@@en="It was found that open was called multiple times at the same time"
|
||||
,"upb8:"+ //no args
|
||||
"J'ai constaté que open était appelé plusieurs fois en même temps"
|
||||
|
||||
//@@zh="录音功能无效:无音频流"
|
||||
//@@en="Invalid recording: no audio stream"
|
||||
,"Q1GA:"+ //no args
|
||||
"La fonction d'enregistrement ne fonctionne pas : pas de flux audio"
|
||||
|
||||
//@@zh=",将尝试禁用回声消除后重试"
|
||||
//@@en=", will try to disable echoCancellation and try again"
|
||||
,"KxE2:"+ //no args
|
||||
", je vais essayer de désactiver echoCancellation et réessayer"
|
||||
|
||||
//@@zh="请求录音权限错误"
|
||||
//@@en="Error requesting recording permission"
|
||||
,"xEQR:"+ //no args
|
||||
"Erreur lors de la demande d'autorisation d'enregistrement"
|
||||
|
||||
//@@zh="无法录音:"
|
||||
//@@en="Unable to record: "
|
||||
,"bDOG:"+ //no args
|
||||
"Impossible d'enregistrer : "
|
||||
|
||||
//@@zh="注意:已配置{1}参数,可能会出现浏览器不能正确选用麦克风、移动端无法启用回声消除等现象"
|
||||
//@@en="Note: The {1} parameter has been configured, which may cause the browser to not correctly select the microphone, or the mobile terminal to not enable echoCancellation, etc. "
|
||||
,"IjL3:"+ //args: {1}
|
||||
"Remarque : Le paramètre {1} a été configuré, ce qui peut empêcher le navigateur de sélectionner correctement le microphone ou le terminal mobile de ne pas activer echoCancellation, etc. "
|
||||
|
||||
//@@zh=",未配置 {1} 时浏览器可能会自动启用回声消除,移动端未禁用回声消除时可能会降低系统播放音量(关闭录音后可恢复)和仅提供16k采样率的音频流(不需要回声消除时可明确配置成禁用来获得48k高音质的流),请参阅文档中{2}配置"
|
||||
//@@en=", when {1} is not configured, the browser may automatically enable echoCancellation. When echoCancellation is not disabled on the mobile terminal, the system playback volume may be reduced (can be restored after closing the recording) and only 16k sampleRate audio stream is provided (when echoCancellation is not required, it can be explicitly configured to disable to obtain 48k high-quality stream). Please refer to the {2} configuration in the document"
|
||||
,"RiWe:"+ //args: {1}-{2}
|
||||
", lorsque {1} n'est pas configuré, le navigateur peut activer automatiquement echoCancellation. Lorsque echoCancellation n'est pas désactivé sur le terminal mobile, le volume de lecture du système peut être réduit (peut être restauré après la fermeture de l'enregistrement) et seul le flux audio à 16 k de fréquence d'échantillonnage est fourni (lorsque echoCancellation n'est pas requis, il peut être explicitement configuré pour être désactivé afin d'obtenir un flux de haute qualité à 48 k). Veuillez vous référer à la configuration {2} dans le document"
|
||||
|
||||
//@@zh="close被忽略(因为同时open了多个rec,只有最后一个会真正close)"
|
||||
//@@en="close is ignored (because multiple recs are open at the same time, only the last one will actually close)"
|
||||
,"hWVz:"+ //no args
|
||||
"close est ignoré (car plusieurs recs sont ouverts en même temps, seul le dernier sera en fait close)"
|
||||
|
||||
//@@zh="忽略"
|
||||
//@@en="ignore"
|
||||
,"UHvm:"+ //no args
|
||||
"négligence"
|
||||
|
||||
//@@zh="不支持{1}架构"
|
||||
//@@en="{1} architecture not supported"
|
||||
,"Essp:"+ //args: {1}
|
||||
"Ne prend pas en charge l'architecture {1}"
|
||||
|
||||
//@@zh="{1}类型不支持设置takeoffEncodeChunk"
|
||||
//@@en="{1} type does not support setting takeoffEncodeChunk"
|
||||
,"2XBl:"+ //args: {1}
|
||||
"Le type {1} ne prend pas en charge le paramètre takeoffEncodeChunk"
|
||||
|
||||
//@@zh="(未加载编码器)"
|
||||
//@@en="(Encoder not loaded)"
|
||||
,"LG7e:"+ //no args
|
||||
"(aucun encodeur chargé)"
|
||||
|
||||
//@@zh="{1}环境不支持实时处理"
|
||||
//@@en="{1} environment does not support real-time processing"
|
||||
,"7uMV:"+ //args: {1}
|
||||
"L'environnement {1} ne prend pas en charge le traitement en temps réel"
|
||||
|
||||
//@@zh="补偿{1}ms"
|
||||
//@@en="Compensation {1}ms"
|
||||
,"4Kfd:"+ //args: {1}
|
||||
"Compensation {1} ms"
|
||||
|
||||
//@@zh="未补偿{1}ms"
|
||||
//@@en="Uncompensated {1}ms"
|
||||
,"bM5i:"+ //args: {1}
|
||||
"Non compensé {1} ms"
|
||||
|
||||
//@@zh="回调出错是不允许的,需保证不会抛异常"
|
||||
//@@en="Callback error is not allowed, you need to ensure that no exception will be thrown"
|
||||
,"gFUF:"+ //no args
|
||||
"Les erreurs dans les rappels ne sont pas autorisées et doivent être assurées qu'aucune exception n'est levée"
|
||||
|
||||
//@@zh="低性能,耗时{1}ms"
|
||||
//@@en="Low performance, took {1}ms"
|
||||
,"2ghS:"+ //args: {1}
|
||||
"Faible performance, prend {1} ms"
|
||||
|
||||
//@@zh="未进入异步前不能清除buffers"
|
||||
//@@en="Buffers cannot be cleared before entering async"
|
||||
,"ufqH:"+ //no args
|
||||
"Les buffers ne peuvent pas être effacés avant d'entrer en mode asynchrone"
|
||||
|
||||
//@@zh="start失败:未open"
|
||||
//@@en="start failed: not open"
|
||||
,"6WmN:"+ //no args
|
||||
"Échec de start: pas open"
|
||||
|
||||
//@@zh="start 开始录音,"
|
||||
//@@en="start recording, "
|
||||
,"kLDN:"+ //no args
|
||||
"start, démarre l'enregistrement, "
|
||||
|
||||
//@@zh="start被中断"
|
||||
//@@en="start was interrupted"
|
||||
,"Bp2y:"+ //no args
|
||||
"start a été interrompu"
|
||||
|
||||
//@@zh=",可能无法录音:"
|
||||
//@@en=", may fail to record: "
|
||||
,"upkE:"+ //no args
|
||||
", l'enregistrement peut ne pas être possible : "
|
||||
|
||||
//@@zh="stop 和start时差:"
|
||||
//@@en="Stop and start time difference: "
|
||||
,"Xq4s:"+ //no args
|
||||
"stop, décalage horaire avec start : "
|
||||
|
||||
//@@zh="补偿:"
|
||||
//@@en="compensate: "
|
||||
,"3CQP:"+ //no args
|
||||
"compenser: "
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
//@@en="Failed to stop recording: "
|
||||
,"u8JG:"+ //no args
|
||||
"Échec de la fin de l'enregistrement : "
|
||||
|
||||
//@@zh=",请设置{1}"
|
||||
//@@en=", please set {1}"
|
||||
,"1skY:"+ //args: {1}
|
||||
", veuillez définir {1}"
|
||||
|
||||
//@@zh="结束录音 编码花{1}ms 音频时长{2}ms 文件大小{3}b"
|
||||
//@@en="Stop recording, encoding takes {1}ms, audio duration {2}ms, file size {3}b"
|
||||
,"Wv7l:"+ //args: {1}-{3}
|
||||
"Terminer l'enregistrement. L'encodage prend {1} ms. La durée audio est de {2} ms. La taille du fichier est de {3}b"
|
||||
|
||||
//@@zh="{1}编码器返回的不是{2}"
|
||||
//@@en="{1} encoder returned not {2}"
|
||||
,"Vkbd:"+ //args: {1}-{2}
|
||||
"L'encodeur {1} ne renvoie pas {2}"
|
||||
|
||||
//@@zh="启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据"
|
||||
//@@en="After enabling takeoffEncodeChunk, the length of the blob returned by stop is 0 and no audio data is provided"
|
||||
,"QWnr:"+ //no args
|
||||
"Après avoir activé takeoffEncodeChunk, la longueur du blob renvoyée par stop est 0 et aucune donnée audio n'est fournie"
|
||||
|
||||
//@@zh="生成的{1}无效"
|
||||
//@@en="Invalid generated {1}"
|
||||
,"Sz2H:"+ //args: {1}
|
||||
"Le {1} généré n'est pas valide"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"wf9t:"+ //no args
|
||||
"L'enregistrement n'a pas démarré"
|
||||
|
||||
//@@zh=",开始录音前无用户交互导致AudioContext未运行"
|
||||
//@@en=", No user interaction before starting recording, resulting in AudioContext not running"
|
||||
,"Dl2c:"+ //no args
|
||||
", il n'y a aucune interaction de l'utilisateur avant de démarrer l'enregistrement, ce qui empêche AudioContext de s'exécuter"
|
||||
|
||||
//@@zh="未采集到录音"
|
||||
//@@en="Recording not captured"
|
||||
,"Ltz3:"+ //no args
|
||||
"Aucun enregistrement n'a été collecté"
|
||||
|
||||
//@@zh="未加载{1}编码器,请尝试到{2}的src/engine内找到{1}的编码器并加载"
|
||||
//@@en="The {1} encoder is not loaded. Please try to find the {1} encoder in the src/engine directory of the {2} and load it"
|
||||
,"xGuI:"+ //args: {1}-{2}
|
||||
"L'encodeur {1} n'est pas chargé. Veuillez essayer de trouver l'encodeur {1} dans src/engine de {2} et de le charger"
|
||||
|
||||
//@@zh="录音错误:"
|
||||
//@@en="Recording error: "
|
||||
,"AxOH:"+ //no args
|
||||
"Erreur d'enregistrement : "
|
||||
|
||||
//@@zh="音频buffers被释放"
|
||||
//@@en="Audio buffers are released"
|
||||
,"xkKd:"+ //no args
|
||||
"Les tampons audio sont libérés"
|
||||
|
||||
//@@zh="采样:{1} 花:{2}ms"
|
||||
//@@en="Sampled: {1}, took: {2}ms"
|
||||
,"CxeT:"+ //args: {1}-{2}
|
||||
"Échantillon : {1} Fleur : {2}ms"
|
||||
|
||||
//@@zh="非浏览器环境,不支持{1}"
|
||||
//@@en="Non-browser environment, does not support {1}"
|
||||
,"NonBrowser-1:"+ //args: {1}
|
||||
"Environnement sans navigateur, ne prend pas en charge {1}"
|
||||
|
||||
//@@zh="参数错误:{1}"
|
||||
//@@en="Illegal argument: {1}"
|
||||
,"IllegalArgs-1:"+ //args: {1}
|
||||
"Erreur de paramètre : {1}"
|
||||
|
||||
//@@zh="调用{1}需要先导入{2}"
|
||||
//@@en="Calling {1} needs to import {2} first"
|
||||
,"NeedImport-2:"+ //args: {1}-{2}
|
||||
"Pour appeler {1}, vous devez d'abord importer {2}"
|
||||
|
||||
//@@zh="不支持:{1}"
|
||||
//@@en="Not support: {1}"
|
||||
,"NotSupport-1:"+ //args: {1}
|
||||
"Non pris en charge : {1}"
|
||||
|
||||
//@@zh="覆盖导入{1}"
|
||||
//@@en="Override import {1}"
|
||||
,"8HO5:"+ //args: {1}
|
||||
"Remplacer l'importation {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=recorder-core.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-amr.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="AMR-NB(NarrowBand),采样率设置无效(只提供8000hz),比特率范围:{1}(默认12.2kbps),一帧20ms、{2}字节;浏览器一般不支持播放amr格式,可用Recorder.amr2wav()转码成wav播放"
|
||||
//@@en="AMR-NB (NarrowBand), sampleRate setting is invalid (only 8000hz is provided), bitRate range: {1} (default 12.2kbps), one frame 20ms, {2} bytes; browsers generally do not support playing amr format, available Recorder.amr2wav() transcoding into wav playback"
|
||||
//@@Put0
|
||||
"b2mN:"+ //args: {1}-{2}
|
||||
"AMR-NB (NarrowBand), le paramètre sampleRate n'est pas valide (seul 8000 Hz est fourni), plage bitRate : {1} (par défaut 12.2 kbps), une image 20 ms, {2} octets ; les navigateurs ne prennent généralement pas en charge la lecture du format amr, disponible Recorder.amr2wav() Transcoder en wav pour la lecture"
|
||||
|
||||
//@@zh="AMR Info: 和设置的不匹配{1},已更新成{2}"
|
||||
//@@en="AMR Info: does not match the set {1}, has been updated to {2}"
|
||||
,"tQBv:"+ //args: {1}-{2}
|
||||
"AMR Info : ne correspond pas à l'ensemble {1}, a été mis à jour vers {2}"
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
//@@en="Data sampleRate lower than {1}"
|
||||
,"q12D:"+ //args: {1}
|
||||
"Les données sampleRate sont inférieures à {1}"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"TxjV:"+ //no args
|
||||
"La version actuelle du navigateur est trop basse et ne peut pas être traitée en temps réel"
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管AMR编码器输出的二进制数据,只有首次回调数据(首帧)包含AMR头;在合并成AMR文件时,如果没有把首帧数据包含进去,则必须在文件开头添加上AMR头:Recorder.AMR.AMR_HEADER(转成二进制),否则无法播放"
|
||||
//@@en="takeoffEncodeChunk takes over the binary data output by the AMR encoder, and only the first callback data (the first frame) contains the AMR header; when merging into an AMR file, if the first frame data is not included, the AMR header must be added at the beginning of the file: Recorder.AMR.AMR_HEADER (converted to binary), otherwise it cannot be played"
|
||||
,"Q7p7:"+ //no args
|
||||
"takeoffEncodeChunk prend en charge les données binaires sorties par l'encodeur AMR. Seules les premières données de rappel (première image) contiennent l'en-tête AMR ; lors de la fusion dans un fichier AMR, si les données de la première image ne sont pas incluses, l'en-tête AMR doit être ajouté à la début du fichier : Recorder.AMR.AMR_HEADER (converti en binaire), sinon il ne peut pas être lu"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,amr实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the amr real-time encoder runs in the main thread"
|
||||
,"6o9Z:"+ //no args
|
||||
"L'environnement actuel ne prend pas en charge Web Worker et l'encodeur en temps réel amr s'exécute dans le thread principal"
|
||||
|
||||
//@@zh="amr worker剩{1}个未stop"
|
||||
//@@en="amr worker left {1} unstopped"
|
||||
,"yYWs:"+ //args: {1}
|
||||
"amr worker reste {1} non stop"
|
||||
|
||||
//@@zh="amr编码器未start"
|
||||
//@@en="amr encoder not started"
|
||||
,"jOi8:"+ //no args
|
||||
"encodeur amr pas start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-amr.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-ogg.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="Ogg Vorbis,比特率取值16-100kbps,采样率取值无限制"
|
||||
//@@en="Ogg Vorbis, bitRate 16-100kbps, sampleRate unlimited"
|
||||
//@@Put0
|
||||
"O8Gn:"+ //no args
|
||||
"Ogg Vorbis, la valeur bitRate est de 16 à 100 kbps, la valeur sampleRate est illimitée"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"5si6:"+ //no args
|
||||
"La version actuelle du navigateur est trop basse et ne peut pas être traitée en temps réel"
|
||||
|
||||
//@@zh="takeoffEncodeChunk接管OggVorbis编码器输出的二进制数据,Ogg由数据页组成,一页包含多帧音频数据(含几秒的音频,一页数据无法单独解码和播放),此编码器每次输出都是完整的一页数据,因此实时性会比较低;在合并成完整ogg文件时,必须将输出的所有数据合并到一起,否则可能无法播放,不支持截取中间一部分单独解码和播放"
|
||||
//@@en="takeoffEncodeChunk takes over the binary data output by the OggVorbis encoder. Ogg is composed of data pages. One page contains multiple frames of audio data (including a few seconds of audio, and one page of data cannot be decoded and played alone). This encoder outputs a complete page of data each time, so the real-time performance will be relatively low; when merging into a complete ogg file, all the output data must be merged together, otherwise it may not be able to play, and it does not support intercepting the middle part to decode and play separately"
|
||||
,"R8yz:"+ //no args
|
||||
"takeoffEncodeChunk prend en charge les données binaires sorties par l'encodeur OggVorbis. Ogg est composé de pages de données. Une page contient plusieurs images de données audio (y compris plusieurs secondes d'audio. Une page de données ne peut pas être décodée et lue séparément). Chaque sortie de ce L'encodeur est complet. Une page de données, donc les performances en temps réel seront relativement faibles ; lors de la fusion dans un fichier ogg complet, toutes les données de sortie doivent être fusionnées ensemble, sinon elles risquent de ne pas être lues et ne sont pas prises en charge. interceptez la partie médiane, décodez-la et jouez-la séparément"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,OggVorbis实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the OggVorbis real-time encoder runs in the main thread"
|
||||
,"hB9D:"+ //no args
|
||||
"L'environnement actuel ne prend pas en charge les Web Workers et l'encodeur en temps réel OggVorbis s'exécute dans le thread principal."
|
||||
|
||||
//@@zh="ogg worker剩{1}个未stop"
|
||||
//@@en="There are {1} unstopped ogg workers"
|
||||
,"oTiy:"+ //args: {1}
|
||||
"ogg worker reste {1} non stop"
|
||||
|
||||
//@@zh="ogg编码器未start"
|
||||
//@@en="ogg encoder not started"
|
||||
,"dIpw:"+ //no args
|
||||
"encodeur ogg pas start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-ogg.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/beta-webm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="此浏览器不支持进行webm编码,未实现MediaRecorder"
|
||||
//@@en="This browser does not support webm encoding, MediaRecorder is not implemented"
|
||||
//@@Put0
|
||||
"L49q:"+ //no args
|
||||
"Ce navigateur ne prend pas en charge l'encodage Webm et MediaRecorder n'est pas implémenté"
|
||||
|
||||
//@@zh="只有比较新的浏览器支持,压缩率和mp3差不多。由于未找到对已有pcm数据进行快速编码的方法,只能按照类似边播放边收听形式把数据导入到MediaRecorder,有几秒就要等几秒。输出音频虽然可以通过比特率来控制文件大小,但音频文件中的比特率并非设定比特率,采样率由于是我们自己采样的,到这个编码器随他怎么搞"
|
||||
//@@en="Only newer browsers support it, and the compression rate is similar to mp3. Since there is no way to quickly encode the existing pcm data, the data can only be imported into MediaRecorder in a similar manner while playing and listening, and it takes a few seconds to wait for a few seconds. Although the output audio can control the file size through the bitRate, the bitRate in the audio file is not the set bitRate. Since the sampleRate is sampled by ourselves, we can do whatever we want with this encoder."
|
||||
,"tsTW:"+ //no args
|
||||
"Seuls les navigateurs les plus récents le prennent en charge et le taux de compression est similaire à celui du mp3. Puisqu'il n'existe aucun moyen d'encoder rapidement les données pcm existantes, les données ne peuvent être importées dans MediaRecorder que d'une manière similaire de lecture et d'écoute, et vous devez attendre quelques secondes. Bien que la taille du fichier audio de sortie puisse être contrôlée par le débit binaire, le débit binaire du fichier audio n'est pas le débit binaire défini. Puisque le taux d'échantillonnage est échantillonné par nous-mêmes, nous pouvons faire ce que nous voulons avec cet encodeur"
|
||||
|
||||
//@@zh="此浏览器不支持把录音转成webm格式"
|
||||
//@@en="This browser does not support converting recordings to webm format"
|
||||
,"aG4z:"+ //no args
|
||||
"Ce navigateur ne prend pas en charge la conversion des enregistrements au format Webm"
|
||||
|
||||
//@@zh="转码webm出错:{1}"
|
||||
//@@en="Error encoding webm: {1}"
|
||||
,"PIX0:"+ //args: {1}
|
||||
"Erreur de transcodage Webm : {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/beta-webm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/g711x.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1};{2}音频文件无法直接播放,可用Recorder.{2}2wav()转码成wav播放;采样率比特率设置无效,固定为8000hz采样率、16位,每个采样压缩成8位存储,音频文件大小为8000字节/秒;如需任意采样率支持,请使用Recorder.{2}_encode()方法"
|
||||
//@@en="{1}; {2} audio files cannot be played directly, and can be transcoded into wav by Recorder.{2}2wav(); the sampleRate bitRate setting is invalid, fixed at 8000hz sampleRate, 16 bits, each sample is compressed into 8 bits for storage, and the audio file size is 8000 bytes/second; if you need any sampleRate support, please use Recorder.{2}_encode() Method"
|
||||
//@@Put0
|
||||
"d8YX:"+ //args: {1}-{2}
|
||||
"{1} ; {2} Le fichier audio ne peut pas être lu directement. Vous pouvez utiliser Recorder.{2}2wav() pour le transcoder en wav pour la lecture ; le paramètre de débit binaire du taux d'échantillonnage n'est pas valide et est fixé à 8 000 hz. taux, 16 bits, et chaque échantillon est compressé. dans un stockage de 8 bits, la taille du fichier audio est de 8 000 octets/seconde ; si vous avez besoin de prise en charge pour un taux d'échantillonnage, veuillez utiliser la méthode Recorder.{2}_encode()"
|
||||
|
||||
//@@zh="数据采样率低于{1}"
|
||||
//@@en="Data sampleRate lower than {1}"
|
||||
,"29UK:"+ //args: {1}
|
||||
"Les données sampleRate sont inférieures à {1}"
|
||||
|
||||
//@@zh="{1}编码器未start"
|
||||
//@@en="{1} encoder not started"
|
||||
,"quVJ:"+ //args: {1}
|
||||
"encodeur {1} pas start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/g711x.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/mp3.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="采样率范围:{1};比特率范围:{2}(不同比特率支持的采样率范围不同,小于32kbps时采样率需小于32000)"
|
||||
//@@en="sampleRate range: {1}; bitRate range: {2} (the sampleRate range supported by different bitRate is different, when the bitRate is less than 32kbps, the sampleRate must be less than 32000)"
|
||||
//@@Put0
|
||||
"Zm7L:"+ //args: {1}-{2}
|
||||
"Plage sampleRate : {1} ; plage bitRate : {2} (différents bitRate prennent en charge différentes plages sampleRate. Lorsqu'il est inférieur à 32 kbit/s, le sampleRate doit être inférieur à 32000)"
|
||||
|
||||
//@@zh="{1}不在mp3支持的取值范围:{2}"
|
||||
//@@en="{1} is not in the value range supported by mp3: {2}"
|
||||
,"eGB9:"+ //args: {1}-{2}
|
||||
"{1} n'est pas dans la plage de valeurs prise en charge par mp3 : {2}"
|
||||
|
||||
//@@zh="sampleRate已更新为{1},因为{2}不在mp3支持的取值范围:{3}"
|
||||
//@@en="sampleRate has been updated to {1}, because {2} is not in the value range supported by mp3: {3}"
|
||||
,"zLTa:"+ //args: {1}-{3}
|
||||
"sampleRate a été mis à jour à {1} car {2} n'est pas dans la plage de valeurs prise en charge par mp3 : {3}"
|
||||
|
||||
//@@zh="当前浏览器版本太低,无法实时处理"
|
||||
//@@en="The current browser version is too low to process in real time"
|
||||
,"yhUs:"+ //no args
|
||||
"La version actuelle du navigateur est trop basse et ne peut pas être traitée en temps réel"
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,mp3实时编码器运行在主线程中"
|
||||
//@@en="The current environment does not support Web Worker, and the mp3 real-time encoder runs in the main thread"
|
||||
,"k9PT:"+ //no args
|
||||
"L'environnement actuel ne prend pas en charge Web Worker et l'encodeur en temps réel mp3 s'exécute dans le thread principal"
|
||||
|
||||
//@@zh="mp3 worker剩{1}个未stop"
|
||||
//@@en="There are {1} unstopped mp3 workers left"
|
||||
,"fT6M:"+ //args: {1}
|
||||
"mp3 worker left {1} unstopped"
|
||||
|
||||
//@@zh="mp3编码器未start"
|
||||
//@@en="mp3 encoder not started"
|
||||
,"mPxH:"+ //no args
|
||||
"encodeur mp3 pas start"
|
||||
|
||||
//@@zh="和设置的不匹配{1},已更新成{2}"
|
||||
//@@en="Does not match the set {1}, has been updated to {2}"
|
||||
,"uY9i:"+ //args: {1}-{2}
|
||||
"Ne correspond pas au paramètre {1}, a été mis à jour vers {2}"
|
||||
|
||||
//@@zh="Fix移除{1}帧"
|
||||
//@@en="Fix remove {1} frame"
|
||||
,"iMSm:"+ //args: {1}
|
||||
"Fix supprime {1} images"
|
||||
|
||||
//@@zh="移除帧数过多"
|
||||
//@@en="Remove too many frames"
|
||||
,"b9zm:"+ //no args
|
||||
"Supprimer trop de cadres"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/mp3.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="pcm为未封装的原始音频数据,pcm音频文件无法直接播放,可用Recorder.pcm2wav()转码成wav播放;支持位数8位、16位(填在比特率里面),采样率取值无限制"
|
||||
//@@en="pcm is unencapsulated original audio data, pcm audio files cannot be played directly, and can be transcoded into wav by Recorder.pcm2wav(); it supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited"
|
||||
//@@Put0
|
||||
"fWsN:"+ //no args
|
||||
"pcm est une donnée audio originale non encapsulée. Les fichiers audio Pcm ne peuvent pas être lus directement. Recorder.pcm2wav() peut être utilisé pour transcoder en wav pour la lecture. Il prend en charge 8 et 16 chiffres (remplis dans bitRate) et la valeur de sampleRate est illimitée."
|
||||
|
||||
//@@zh="PCM Info: 不支持{1}位,已更新成{2}位"
|
||||
//@@en="PCM Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
,"uMUJ:"+ //args: {1}-{2}
|
||||
"PCM Info: Le bit {1} n'est pas pris en charge et a été mis à jour vers le bit {2}"
|
||||
|
||||
//@@zh="pcm2wav必须提供sampleRate和bitRate"
|
||||
//@@en="pcm2wav must provide sampleRate and bitRate"
|
||||
,"KmRz:"+ //no args
|
||||
"pcm2wav doit fournir sampleRate et bitRate"
|
||||
|
||||
//@@zh="pcm编码器未start"
|
||||
//@@en="pcm encoder not started"
|
||||
,"sDkA:"+ //no args
|
||||
"encodeur pcm pas start"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=engine/wav.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="支持位数8位、16位(填在比特率里面),采样率取值无限制;此编码器仅在pcm数据前加了一个44字节的wav头,编码出来的16位wav文件去掉开头的44字节即可得到pcm(注:其他wav编码器可能不是44字节)"
|
||||
//@@en="Supports 8-bit and 16-bit bits (fill in the bitRate), and the sampleRate is unlimited; this encoder only adds a 44-byte wav header before the pcm data, and the encoded 16-bit wav file removes the beginning 44 bytes to get pcm (note: other wav encoders may not be 44 bytes)"
|
||||
//@@Put0
|
||||
"gPSE:"+ //no args
|
||||
"Prend en charge les chiffres de 8 bits et 16 bits (remplis dans bitRate) et la valeur de sampleRate est illimitée ; cet encodeur ajoute uniquement un en-tête wav de 44 octets avant les données pcm, et le fichier wav codé de 16 bits supprime les 44 premiers bits. Octets pour obtenir pcm (remarque : les autres encodeurs wav peuvent ne pas faire 44 octets)"
|
||||
|
||||
//@@zh="WAV Info: 不支持{1}位,已更新成{2}位"
|
||||
//@@en="WAV Info: {1} bit is not supported, has been updated to {2} bit"
|
||||
,"wyw9:"+ //args: {1}-{2}
|
||||
"WAV Info: Le bit {1} n'est pas pris en charge et a été mis à jour vers le bit {2}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=engine/wav.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/buffer_stream.player.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="getAudioSrc方法已过时:请直接使用getMediaStream然后赋值给audio.srcObject,仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"
|
||||
//@@en="The getAudioSrc method is obsolete: please use getMediaStream directly and then assign it to audio.srcObject, it is only allowed to call this method in browsers that do not support srcObject and assign it to audio.src for compatibility"
|
||||
//@@Put0
|
||||
"0XYC:"+ //no args
|
||||
"La méthode getAudioSrc est obsolète : veuillez utiliser getMediaStream directement et l'attribuer à audio.srcObject. Cette méthode ne peut être appelée que dans les navigateurs qui ne prennent pas en charge srcObject et attribuée à audio.src pour des raisons de compatibilité."
|
||||
|
||||
//@@zh="start被stop终止"
|
||||
//@@en="start is terminated by stop"
|
||||
,"6DDt:"+ //no args
|
||||
"start est terminé par stop"
|
||||
|
||||
//@@zh="{1}多次start"
|
||||
//@@en="{1} repeat start"
|
||||
,"I4h4:"+ //args: {1}
|
||||
"Répétition {1} start"
|
||||
|
||||
//@@zh="浏览器不支持打开{1}"
|
||||
//@@en="The browser does not support opening {1}"
|
||||
,"P6Gs:"+ //args: {1}
|
||||
"Le navigateur ne prend pas en charge l'ouverture de {1}"
|
||||
|
||||
//@@zh="(注意:ctx不是running状态,start需要在用户操作(触摸、点击等)时进行调用,否则会尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"
|
||||
//@@en=" (Note: ctx is not in the running state, start needs to be called when the user operates (touch, click, etc.), otherwise it will try to perform ctx.resume, which may cause compatibility issues (only iOS), please refer to the runningContext configuration in the document) "
|
||||
,"JwDm:"+ //no args
|
||||
"(Remarque : ctx n'est pas dans l'état running. start doit être appelé lorsque l'utilisateur opère (toucher, cliquer, etc.), sinon ctx.resume sera tenté, ce qui peut entraîner des problèmes de compatibilité (iOS uniquement). Veuillez vous référer au configuration runningContext dans le document)"
|
||||
|
||||
//@@zh="此浏览器的AudioBuffer实现不支持动态特性,采用兼容模式"
|
||||
//@@en="The AudioBuffer implementation of this browser does not support dynamic features, use compatibility mode"
|
||||
,"qx6X:"+ //no args
|
||||
"L'implémentation AudioBuffer de ce navigateur ne prend pas en charge les fonctionnalités dynamiques et utilise le mode de compatibilité"
|
||||
|
||||
//@@zh="环境检测超时"
|
||||
//@@en="Environment detection timeout"
|
||||
,"cdOx:"+ //no args
|
||||
"Expiration du délai de détection de l'environnement"
|
||||
|
||||
//@@zh="可能无法播放:{1}"
|
||||
//@@en="Could not play: {1}"
|
||||
,"S2Bu:"+ //args: {1}
|
||||
"Peut ne pas jouer: {1}"
|
||||
|
||||
//@@zh="input调用失败:非pcm[Int16,...]输入时,必须解码或者使用transform转换"
|
||||
//@@en="input call failed: non-pcm[Int16,...] input must be decoded or converted using transform"
|
||||
,"ZfGG:"+ //no args
|
||||
"L'appel input a échoué: non - PCM [int16,...] en entrée, il doit être décodé ou converti avec transform"
|
||||
|
||||
//@@zh="input调用失败:未提供sampleRate"
|
||||
//@@en="input call failed: sampleRate not provided"
|
||||
,"N4ke:"+ //no args
|
||||
"L'appel input a échoué: sampleRate n'a pas été fourni"
|
||||
|
||||
//@@zh="input调用失败:data的sampleRate={1}和之前的={2}不同"
|
||||
//@@en="input call failed: sampleRate={1} of data is different from previous={2}"
|
||||
,"IHZd:"+ //args: {1}-{2}
|
||||
"L'appel input a échoué: sampleRate={1} de Data est différent de ={2} précédent"
|
||||
|
||||
//@@zh="延迟过大,已丢弃{1}ms {2}"
|
||||
//@@en="The delay is too large, {1}ms has been discarded, {2}"
|
||||
,"L8sC:"+ //args: {1}-{2}
|
||||
"Le délai est trop important, {1}ms ont été ignorées, {2}"
|
||||
|
||||
//@@zh="{1}未调用start方法"
|
||||
//@@en="{1} did not call the start method"
|
||||
,"TZPq:"+ //args: {1}
|
||||
"{1} la méthode start n'est pas appelée"
|
||||
|
||||
//@@zh="浏览器不支持音频解码"
|
||||
//@@en="Browser does not support audio decoding"
|
||||
,"iCFC:"+ //no args
|
||||
"Le navigateur ne supporte pas le décodage audio"
|
||||
|
||||
//@@zh="音频解码数据必须是ArrayBuffer"
|
||||
//@@en="Audio decoding data must be ArrayBuffer"
|
||||
,"wE2k:"+ //no args
|
||||
"Les données de décodage audio doivent être ArrayBuffer"
|
||||
|
||||
//@@zh="音频解码失败:{1}"
|
||||
//@@en="Audio decoding failed: {1}"
|
||||
,"mOaT:"+ //args: {1}
|
||||
"Le décodage audio a échoué: {1}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/buffer_stream.player.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="符号[{1}]无效:{2}"
|
||||
//@@en="Invalid symbol [{1}]: {2}"
|
||||
//@@Put0
|
||||
"3RBa:"+ //args: {1}-{2}
|
||||
"Le symbole [{1}] est invalide: {2}"
|
||||
|
||||
//@@zh="音符[{1}]无效:{2}"
|
||||
//@@en="Invalid note [{1}]: {2}"
|
||||
,"U212:"+ //args: {1}-{2}
|
||||
"Note [{1}] invalide: {2}"
|
||||
|
||||
//@@zh="多个音时必须对齐,相差{1}ms"
|
||||
//@@en="Multiple tones must be aligned, with a difference of {1}ms"
|
||||
,"7qAD:"+ //args: {1}
|
||||
"Doit être aligné lorsque plusieurs tonalités, différence {1}ms"
|
||||
|
||||
//@@zh="祝你生日快乐"
|
||||
//@@en="Happy Birthday to You"
|
||||
,"QGsW:"+ //no args
|
||||
"Happy Birthday to You"
|
||||
|
||||
//@@zh="致爱丽丝"
|
||||
//@@en="For Elise"
|
||||
,"emJR:"+ //no args
|
||||
"For Elise"
|
||||
|
||||
//@@zh="卡农-右手简谱"
|
||||
//@@en="Canon - Right Hand Notation"
|
||||
,"GsYy:"+ //no args
|
||||
"Canon - symbole de la main droite"
|
||||
|
||||
//@@zh="卡农"
|
||||
//@@en="Canon"
|
||||
,"bSFZ:"+ //no args
|
||||
"Canon"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/create-audio.nmn2pcm.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=extensions/sonic.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="当前环境不支持Web Worker,不支持调用Sonic.Async"
|
||||
//@@en="The current environment does not support Web Worker and does not support calling Sonic.Async"
|
||||
//@@Put0
|
||||
"Ikdz:"+ //no args
|
||||
"Web Worker n'est pas supporté dans l'environnement actuel, appel Sonic.Async n'est pas supporté"
|
||||
|
||||
//@@zh="sonic worker剩{1}个未flush"
|
||||
//@@en="There are {1} unflushed sonic workers left"
|
||||
,"IC5Y:"+ //args: {1}
|
||||
"sonic worker reste {1} non flush"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=extensions/sonic.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app-native-support.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="{1}中的{2}方法未实现,请在{3}文件中或配置文件中实现此方法"
|
||||
//@@en="The {2} method in {1} is not implemented, please implement this method in the {3} file or configuration file"
|
||||
//@@Put0
|
||||
"WWoj:"+ //args: {1}-{3}
|
||||
"La méthode {2} dans {1} n'est pas implémentée, implémentez - la dans un fichier {3} ou dans un fichier de configuration"
|
||||
|
||||
//@@zh="未开始录音,但收到Native PCM数据"
|
||||
//@@en="Recording does not start, but Native PCM data is received"
|
||||
,"rCAM:"+ //no args
|
||||
"L'enregistrement n'a pas commencé, mais les données Native PCM ont été reçues"
|
||||
|
||||
//@@zh="检测到跨域iframe,NativeRecordReceivePCM无法注入到顶层,已监听postMessage转发兼容传输数据,请自行实现将top层接收到数据转发到本iframe(不限层),不然无法接收到录音数据"
|
||||
//@@en="A cross-domain iframe is detected. NativeRecordReceivePCM cannot be injected into the top layer. It has listened to postMessage to be compatible with data transmission. Please implement it by yourself to forward the data received by the top layer to this iframe (no limit on layer), otherwise the recording data cannot be received."
|
||||
,"t2OF:"+ //no args
|
||||
"Iframe Cross - Domain détecté, NativeRecordReceivePCM ne peut pas être injecté à la couche supérieure, a écouté postMessage pour transmettre des données de transfert compatibles, s'il vous plaît implémenter vous - même pour transmettre les données reçues à la couche supérieure à cette iframe (couche illimitée), sinon les données d'enregistrement ne peuvent pas être reçues"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"Z2y2:"+ //no args
|
||||
"L'enregistrement n'a pas commencé"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app-native-support.js ***************
|
||||
|
||||
|
||||
|
||||
//*************** Begin srcFile=app-support/app.js ***************
|
||||
i18n.put(putSet,
|
||||
[ //@@PutList
|
||||
|
||||
//@@zh="重复导入{1}"
|
||||
//@@en="Duplicate import {1}"
|
||||
//@@Put0
|
||||
"uXtA:"+ //args: {1}
|
||||
"Importation répétée {1}"
|
||||
|
||||
//@@zh="注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调"
|
||||
//@@en="Note: Because other recording-related methods are called concurrently, the current call result of {1} has been discarded and there will be no callback"
|
||||
,"kIBu:"+ //args: {1}
|
||||
"Remarque : Étant donné que d'autres méthodes liées à l'enregistrement sont appelées simultanément, le résultat de l'appel actuel de {1} a été ignoré et il n'y aura pas de rappel"
|
||||
|
||||
//@@zh="重复注册{1}"
|
||||
//@@en="Duplicate registration {1}"
|
||||
,"ha2K:"+ //args: {1}
|
||||
"Enregistrement répété {1}"
|
||||
|
||||
//@@zh="仅清理资源"
|
||||
//@@en="Clean resources only"
|
||||
,"wpTL:"+ //no args
|
||||
"Nettoyage des ressources uniquement"
|
||||
|
||||
//@@zh="未开始录音"
|
||||
//@@en="Recording not started"
|
||||
,"bpvP:"+ //no args
|
||||
"L'enregistrement n'a pas commencé"
|
||||
|
||||
//@@zh="当前环境不支持实时回调,无法进行{1}"
|
||||
//@@en="The current environment does not support real-time callback and cannot be performed {1}"
|
||||
,"fLJD:"+ //args: {1}
|
||||
"L'environnement actuel ne prend pas en charge le Callback en temps réel, impossible de faire {1}"
|
||||
|
||||
//@@zh="录音权限请求失败:"
|
||||
//@@en="Recording permission request failed: "
|
||||
,"YnzX:"+ //no args
|
||||
"La demande d'autorisation d'enregistrement a échoué: "
|
||||
|
||||
//@@zh="需先调用{1}"
|
||||
//@@en="Need to call {1} first"
|
||||
,"nwKR:"+ //args: {1}
|
||||
"Appelez d'abord {1}"
|
||||
|
||||
//@@zh="当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入"
|
||||
//@@en="This is not a browser environment. You need to import support files for this platform ({1}), or call {2} to implement the access yourself."
|
||||
,"citA:"+ //args: {1}-{2}
|
||||
"Actuellement, ce n'est pas un environnement de navigateur, il est nécessaire d'introduire un fichier de support ({1}) pour cette plate - forme ou d'appeler {2} pour implémenter l'accès par vous - même"
|
||||
|
||||
//@@zh="开始录音失败:"
|
||||
//@@en="Failed to start recording: "
|
||||
,"ecp9:"+ //no args
|
||||
"Le début de l'enregistrement échoue: "
|
||||
|
||||
//@@zh="不能录音:"
|
||||
//@@en="Cannot record: "
|
||||
,"EKmS:"+ //no args
|
||||
"Ne peut pas enregistrer: "
|
||||
|
||||
//@@zh="已开始录音"
|
||||
//@@en="Recording started"
|
||||
,"k7Qo:"+ //no args
|
||||
"Enregistrement commencé"
|
||||
|
||||
//@@zh="结束录音失败:"
|
||||
//@@en="Failed to stop recording: "
|
||||
,"Douz:"+ //no args
|
||||
"Fin de l'enregistrement échoué: "
|
||||
|
||||
//@@zh="和Start时差:{1}ms"
|
||||
//@@en="Time difference from Start: {1}ms"
|
||||
,"wqSH:"+ //args: {1}
|
||||
"Et le décalage horaire de départ: {1}ms"
|
||||
|
||||
//@@zh="结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}"
|
||||
//@@en="Stop recording, takes {1}ms, audio duration {2}ms, file size {3}b, {4}"
|
||||
,"g3VX:"+ //args: {1}-{4}
|
||||
"Fin de l'enregistrement, prend du temps {1}ms Durée audio {2}ms , taille du fichier {3}b , {4}"
|
||||
|
||||
]);
|
||||
//*************** End srcFile=app-support/app.js ***************
|
||||
|
||||
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-2 End @@
|
||||
|
||||
}));
|
41
public/i18n/zh-CN.js
Normal file
41
public/i18n/zh-CN.js
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Recorder i18n/zh-CN.js
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
Usage: Recorder.i18n.lang="zh-CN" or "zh"
|
||||
|
||||
Desc: Simplified Chinese, 简体中文。代码内置完整的中文支持,无需额外翻译,本文件存在的意义是方便查看支持的语言。
|
||||
|
||||
注意:请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成,只允许在字符串中填写翻译后的文本,请勿改变代码结构;翻译的文本如果需要明确的空值,请填写"=Empty";文本中的变量用{n}表示(n代表第几个变量),所有变量必须都出现至少一次,如果不要某变量用{n!}表示
|
||||
|
||||
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
|
||||
*/
|
||||
(function(factory){
|
||||
var browser=typeof window=="object" && !!window.document;
|
||||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||||
factory(win.Recorder,browser);
|
||||
}(function(Recorder,isBrowser){
|
||||
"use strict";
|
||||
var i18n=Recorder.i18n;
|
||||
|
||||
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-1 End @@
|
||||
|
||||
//@@Exec i18n.lang="zh-CN";
|
||||
Recorder.CLog('Import Recorder i18n lang="zh-CN"');
|
||||
|
||||
i18n.alias["zh-CN"]="zh";
|
||||
|
||||
var putSet={lang:"zh"};
|
||||
|
||||
i18n.data["rtl$zh"]=false;
|
||||
i18n.data["desc$zh"]="Simplified Chinese, 简体中文。代码内置完整的中文支持,无需额外翻译,本文件存在的意义是方便查看支持的语言。";
|
||||
//@@Exec i18n.GenerateDisplayEnglish=false;
|
||||
//@@Exec i18n.put(putSet,[]);
|
||||
|
||||
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
|
||||
|
||||
//@@User Code-2 End @@
|
||||
|
||||
}));
|
2
public/recorder.mp3.min.d.ts
vendored
Normal file
2
public/recorder.mp3.min.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare let Recorder : any;
|
||||
export default Recorder;
|
6
public/recorder.mp3.min.js
vendored
Normal file
6
public/recorder.mp3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/recorder.wav.min.d.ts
vendored
Normal file
2
public/recorder.wav.min.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare let Recorder : any;
|
||||
export default Recorder;
|
6
public/recorder.wav.min.js
vendored
Normal file
6
public/recorder.wav.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,7 @@
|
||||
// import Recorder from 'recorder-core/recorder.mp3.min'; //已包含recorder-core和mp3格式支持
|
||||
// import 'recorder-core/src/extensions/waveview'
|
||||
|
||||
// Recorder.a = 1;
|
||||
// @ts-ignore
|
||||
const Recorder = window.Recorder;
|
||||
export { Recorder };
|
10
src/main.tsx
10
src/main.tsx
@ -1,10 +1,4 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App, AppRoute } from './pages/App.tsx';
|
||||
import { CustomThemeProvider } from '@kevisual/components/theme/index.tsx';
|
||||
import { RecordInfo } from './pages/record';
|
||||
|
||||
console.log('cu',)
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<CustomThemeProvider>
|
||||
<AppRoute />
|
||||
</CustomThemeProvider>,
|
||||
);
|
||||
createRoot(document.getElementById('root')!).render(<RecordInfo />);
|
||||
|
82
src/pages/record/index.tsx
Normal file
82
src/pages/record/index.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { MyRecorder } from './module/MyRecorder';
|
||||
import { AliAsr } from './module/AliAsr';
|
||||
export const RecordInfo = () => {
|
||||
const recorderRef = useRef<MyRecorder | null>(null);
|
||||
const asrRef = useRef<AliAsr | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
const init = async () => {
|
||||
recorderRef.current = new MyRecorder();
|
||||
asrRef.current = new AliAsr();
|
||||
asrRef.current?.init();
|
||||
const open = await recorderRef.current?.init();
|
||||
if (open) {
|
||||
setMounted(true);
|
||||
}
|
||||
};
|
||||
const start = () => {
|
||||
const startRecord = () => {
|
||||
recorderRef.current?.startRecord();
|
||||
};
|
||||
// asrRef.current?.start(startRecord, (msg) => {
|
||||
// console.log('start fail', msg);
|
||||
// });
|
||||
startRecord();
|
||||
};
|
||||
const stop = async () => {
|
||||
const stopRecord = () => {
|
||||
recorderRef.current?.stopRecord();
|
||||
};
|
||||
|
||||
const blob = await recorderRef.current?.stopRecord();
|
||||
if (blob !== false) {
|
||||
asrRef.current?.asr?.audioToText(
|
||||
blob,
|
||||
(text) => {
|
||||
console.log('text', text);
|
||||
setText(text);
|
||||
},
|
||||
(msg) => {
|
||||
console.log('text fail', msg);
|
||||
},
|
||||
);
|
||||
}
|
||||
console.log('stop');
|
||||
// asrRef.current?.stop(
|
||||
// (text, abortMsg) => {
|
||||
// console.log('stop', text, abortMsg);
|
||||
// setText(text);
|
||||
// stopRecord();
|
||||
// },
|
||||
// (msg) => {
|
||||
// console.log('stop fail', msg);
|
||||
// stopRecord();
|
||||
// },
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='p-5'>
|
||||
<div className='flex py-4'>
|
||||
<button
|
||||
disabled={!mounted}
|
||||
onClick={start}
|
||||
className='btn btn-primary px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 active:bg-blue-700 transition-colors duration-200 text-white font-medium mr-4 cursor-pointer'>
|
||||
start
|
||||
</button>
|
||||
<button
|
||||
disabled={!mounted}
|
||||
onClick={stop}
|
||||
className='btn btn-danger px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600 active:bg-red-700 transition-colors duration-200 text-white font-medium cursor-pointer'>
|
||||
stop
|
||||
</button>
|
||||
</div>
|
||||
<div className='mt-4'>{text}</div>
|
||||
<div className='recwave w-[100px] h-[100px] border p-2'></div>
|
||||
</div>
|
||||
);
|
||||
};
|
95
src/pages/record/module/AliAsr.ts
Normal file
95
src/pages/record/module/AliAsr.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Recorder } from '../../../app';
|
||||
|
||||
/** Token的请求结果 */
|
||||
export type ApiShortResult = {
|
||||
c: number;
|
||||
m: string;
|
||||
v: {
|
||||
appkey: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** 阿里云一句话识别配置 */
|
||||
export class AliAsr {
|
||||
asr?: any;
|
||||
constructor() {}
|
||||
init() {
|
||||
this.asr = this.create();
|
||||
}
|
||||
create() {
|
||||
const asr = Recorder.ASR_Aliyun_Short({
|
||||
tokenApi: '/token',
|
||||
apiArgs: {
|
||||
//请求tokenApi时要传的参数
|
||||
action: 'token',
|
||||
lang: '普通话', //语言模型设置(具体取值取决于tokenApi支持了哪些语言)
|
||||
},
|
||||
compatibleWebSocket: null,
|
||||
//高级选项
|
||||
fileSpeed: 6, //单个文件识别发送速度控制,取值1-n;1:为按播放速率发送,最慢,识别精度完美;6:按六倍播放速度发送,花10秒识别60秒文件比较快,精度还行;再快测试发现似乎会缺失内容,可能是发送太快底层识别不过来导致返回的结果缺失。
|
||||
log: (msg, error) => {
|
||||
console.log('AliAsr log', msg, error);
|
||||
},
|
||||
});
|
||||
this.asr = asr;
|
||||
return asr;
|
||||
}
|
||||
/**
|
||||
* 获取输入的音频数据总时长
|
||||
* @returns
|
||||
*/
|
||||
getInputDuration() {
|
||||
return this.asr?.inputDuration();
|
||||
}
|
||||
/**
|
||||
* 获取已发送识别的音频数据总时长
|
||||
* @returns
|
||||
*/
|
||||
getSendDuration() {
|
||||
return this.asr?.sendDuration();
|
||||
}
|
||||
/**
|
||||
* 获取已识别的音频数据总时长
|
||||
* @returns
|
||||
*/
|
||||
getAsrDuration() {
|
||||
return this.asr?.asrDuration();
|
||||
}
|
||||
/**
|
||||
* 获取实时结果文本
|
||||
* @returns
|
||||
*/
|
||||
getText() {
|
||||
return this.asr?.getText();
|
||||
}
|
||||
/**
|
||||
* 一次性将单个完整音频Blob文件转成文字
|
||||
* @param audioBlob
|
||||
* @returns
|
||||
*/
|
||||
audioToText(audioBlob: Blob) {
|
||||
return this.asr?.audioToText(audioBlob);
|
||||
}
|
||||
/**
|
||||
* 一次性将单个完整PCM音频数据转成文字
|
||||
* @param buffer
|
||||
* @param sampleRate
|
||||
* @returns
|
||||
*/
|
||||
pcmToText(buffer: ArrayBuffer, sampleRate: number) {
|
||||
return this.asr?.pcmToText(buffer, sampleRate);
|
||||
}
|
||||
/**
|
||||
* 开始识别
|
||||
*/
|
||||
start(recordStartFn: () => void, fail?: (msg?: string) => void) {
|
||||
this.asr?.start(recordStartFn, fail);
|
||||
}
|
||||
/**
|
||||
* 停止识别
|
||||
*/
|
||||
stop(success?: (text: string, abortMsg?: string) => void, fail?: (msg?: string) => void) {
|
||||
this.asr?.stop(success, fail);
|
||||
}
|
||||
}
|
111
src/pages/record/module/MyRecorder.ts
Normal file
111
src/pages/record/module/MyRecorder.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { Recorder } from '../../../app';
|
||||
import { AliAsr } from './AliAsr';
|
||||
|
||||
export class MyRecorder {
|
||||
private recorder?: typeof Recorder;
|
||||
private processTime = 0;
|
||||
private wave?: any;
|
||||
asr?: AliAsr;
|
||||
blob?: Blob;
|
||||
constructor() {}
|
||||
async init(asr?: AliAsr) {
|
||||
const that = this;
|
||||
this.asr = asr;
|
||||
this.recorder = new Recorder({
|
||||
type: 'mp3',
|
||||
sampleRate: 16000,
|
||||
bitRate: 16,
|
||||
onProcess: function (buffers, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) {
|
||||
//录音实时回调,大约1秒调用12次本回调,buffers为开始到现在的所有录音pcm数据块(16位小端LE)
|
||||
//可利用extensions/sonic.js插件实时变速变调,此插件计算量巨大,onProcess需要返回true开启异步模式
|
||||
//可实时上传(发送)数据,配合Recorder.SampleData方法,将buffers中的新数据连续的转换成pcm上传,或使用mock方法将新数据连续的转码成其他格式上传,可以参考文档里面的:Demo片段列表 -> 实时转码并上传-通用版;基于本功能可以做到:实时转发数据、实时保存数据、实时语音识别(ASR)等
|
||||
that.processTime = Date.now();
|
||||
//可实时绘制波形(extensions目录内的waveview.js、wavesurfer.view.js、frequency.histogram.view.js插件功能)
|
||||
that.wave && that.wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
|
||||
if (asr) {
|
||||
asr.asr?.input(buffers, bufferSampleRate, 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
const isOpen = await this.open();
|
||||
console.log('open recorder', isOpen);
|
||||
return isOpen;
|
||||
}
|
||||
async open() {
|
||||
const that = this;
|
||||
return new Promise((resolve) => {
|
||||
that.recorder?.open(
|
||||
function () {
|
||||
if (Recorder.WaveView) that.wave = Recorder.WaveView({ elem: '.recwave' });
|
||||
console.log('open success');
|
||||
resolve(true);
|
||||
},
|
||||
function (msg, isUserNotAllow) {
|
||||
//用户拒绝未授权或不支持
|
||||
console.log('open fail', msg, isUserNotAllow);
|
||||
console.log((isUserNotAllow ? 'UserNotAllow,' : '') + '无法录音:' + msg);
|
||||
resolve(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
async startRecord() {
|
||||
let that = this;
|
||||
if (!this.recorder) {
|
||||
await this.init();
|
||||
}
|
||||
this.recorder?.start();
|
||||
this.processTime = 0;
|
||||
//【稳如老狗WDT】可选的,监控是否在正常录音有onProcess回调,如果长时间没有回调就代表录音不正常
|
||||
let rec = this.recorder;
|
||||
console.log(rec);
|
||||
rec.wdtPauseT = 0;
|
||||
const startTime = Date.now();
|
||||
const wdt = setInterval(function () {
|
||||
const processTime = that.processTime;
|
||||
console.log(rec, wdt, 'processTime', processTime);
|
||||
if (!rec || wdt != rec.watchDogTimer) {
|
||||
clearInterval(wdt);
|
||||
return;
|
||||
} //sync
|
||||
if (Date.now() < rec.wdtPauseT) return; //如果暂停录音了就不检测:puase时赋值rec.wdtPauseT=Date.now()*2(永不监控),resume时赋值rec.wdtPauseT=Date.now()+1000(1秒后再监控)
|
||||
if (Date.now() - (processTime || startTime) > 1500) {
|
||||
clearInterval(wdt);
|
||||
console.error(processTime ? '录音被中断' : '录音未能正常开始');
|
||||
// ... 错误处理,关闭录音,提醒用户
|
||||
}
|
||||
}, 1000);
|
||||
rec.watchDogTimer = wdt;
|
||||
}
|
||||
async stopRecord() {
|
||||
const that = this;
|
||||
let rec = this.recorder;
|
||||
rec.watchDogTimer = 0; //停止监控onProcess超时
|
||||
return new Promise((resolve) =>
|
||||
this.recorder?.stop(
|
||||
function (blob, duration) {
|
||||
const localUrl = (window.URL || webkitURL).createObjectURL(blob);
|
||||
console.log(blob, localUrl, '时长:' + duration + 'ms');
|
||||
rec.close(); //释放录音资源,当然可以不释放,后面可以连续调用start;但不释放时系统或浏览器会一直提示在录音,最佳操作是录完就close掉
|
||||
that.recorder = null;
|
||||
//已经拿到blob文件对象想干嘛就干嘛:立即播放、上传、下载保存
|
||||
that.blob = blob;
|
||||
/*** 【立即播放例子】 ***/
|
||||
const audio = document.createElement('audio');
|
||||
document.body.prepend(audio);
|
||||
audio.controls = true;
|
||||
audio.src = localUrl;
|
||||
audio.play();
|
||||
resolve(blob);
|
||||
},
|
||||
function (msg) {
|
||||
console.log('录音失败:' + msg);
|
||||
rec.close(); //可以通过stop方法的第3个参数来自动调用close
|
||||
rec = null;
|
||||
that.recorder = null;
|
||||
resolve(false);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -82,6 +82,13 @@ export default defineConfig({
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/token': {
|
||||
target: 'http://127.0.0.1:9527',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/token/, '/token'),
|
||||
},
|
||||
'/api/router': {
|
||||
target: 'ws://localhost:3000',
|
||||
changeOrigin: true,
|
||||
|
Reference in New Issue
Block a user