This commit is contained in:
熊潇 2025-04-20 18:46:48 +08:00
parent e66f7ce00e
commit a696bc3bbe
34 changed files with 9681 additions and 328 deletions

5
backend/demo/index.html Normal file
View 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
View 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,"&quot;")+'" 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

File diff suppressed because one or more lines are too long

14
backend/package.json Normal file
View 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"
}

View 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 TokenToken有效期内可以重复使用实际使用时应当全局共享并做好并发控制
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
View 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");
}
};

View File

@ -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,

View File

@ -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
View File

@ -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: {}

View 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. 到阿里云开通 一句话识别 服务可试用一段时间正式使用时应当开通商用版很便宜得到AccessKeySecret参考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 //code0接口调用正常其他数值接口调用出错
,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-n1为按播放速率发送最慢识别精度完美6按六倍播放速度发送花10秒识别60秒文件比较快精度还行再快测试发现似乎会缺失内容可能是发送太快底层识别不过来导致返回的结果缺失。
};
for(var k in set){
o[k]=set[k];
};
This.set=set=o;
This.state=0;//0 未start1 start2 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);
}
/**,mp3wavPCM -> 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);
}
/**inputstopstartinputstart
建议在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();
}
/**startstartstart
buffers:[[Int16...],...] pcm片段列表为二维数组第一维数组内存放1个或多个pcm数据比如可以是rec.buffersonProcess中的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;
}));

View File

@ -0,0 +1,887 @@
/*
录音 Recorder扩展实时播放录音片段文件把片段文件转换成MediaStream流
https://github.com/xiangyuecn/Recorder
BufferStreamPlayer可以通过input方法一次性输入整个音频文件或者实时输入音频片段文件然后播放出来输入支持格式pcmwavmp3等浏览器支持的音频格式非pcm格式会自动解码成pcm播放音质效果比pcmwav格式差点输入前输入后都可进行处理要播放的音频比如混音变速变调输入的音频会写入到内部的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音质会相对差一点其他浏览器测试AndroidIOSChrome无此问题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;
}
/**inputstart()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();
}
}
/**startstart
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的二进制长度
如果是提供的pcmwav格式数据数据长度对播放无太大影响很短的数据也能很好的连续播放
如果是提供的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;
}));

View 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为一拍67各占1/2相当于八分音符
- 音符后面用 "-" 表示二分音符简单计算为1+1=2拍时长几个-就加几拍
- 音符后面用 "_" 表示八分音符两两在一拍里面的音符可以免写_自动会按1/2分配一拍里面只有一个音时这拍会被简单计算为1/2=0.5其他情况计算会按权重分配这一拍的时长(复杂)6,7_为1/2+1/2/2=0.756*,7_才是(1+0.5)/2+1/2/2=1其中6权重1分配1/2=0.57权重0.5分配1/2/2=0.25多加一个"_"就多除个26_,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: Int16Arraypcm数据
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;
}));

View 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 信号检查因子取值123默认为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=16ms16ms*(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;
};
}));

View 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;
}
/** pcmpcms:[[Int16,...],...]sampleRatepcmindexpcmspcm
返回混合状态对象
注意调用本方法会修改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]
};
}));

View 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;
}));

View 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

File diff suppressed because it is too large Load Diff

View 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;
}));

View 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
View 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 Workeramr实时编码器运行在主线程中"
//@@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 WorkerOggVorbis实时编码器运行在主线程中"
//@@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 Workermp3实时编码器运行在主线程中"
//@@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="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听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
View 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 Workeramr实时编码器运行在主线程中"
,"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 WorkerOggVorbis实时编码器运行在主线程中"
,"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 Workermp3实时编码器运行在主线程中"
,"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="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听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
View 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 Workeramr实时编码器运行在主线程中"
//@@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 WorkerOggVorbis实时编码器运行在主线程中"
//@@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 Workermp3实时编码器运行在主线程中"
//@@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="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听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
View 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 Workeramr实时编码器运行在主线程中"
//@@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 WorkerOggVorbis实时编码器运行在主线程中"
//@@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 Workermp3实时编码器运行在主线程中"
//@@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="检测到跨域iframeNativeRecordReceivePCM无法注入到顶层已监听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
View 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
View File

@ -0,0 +1,2 @@
declare let Recorder : any;
export default Recorder;

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
View File

@ -0,0 +1,2 @@
declare let Recorder : any;
export default Recorder;

6
public/recorder.wav.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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 />);

View 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>
);
};

View 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-n1为按播放速率发送最慢识别精度完美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);
}
}

View 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()+10001秒后再监控
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);
},
),
);
}
}

View File

@ -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,