UNPKG

recorder-core

Version:

Recorder库: html5 js 录音 mp3 wav ogg webm amr g711a g711u 格式,支持pc和Android、iOS部分浏览器、Hybrid App(提供Android iOS App源码)、微信,提供ASR语音识别转文字 H5版语音通话聊天示例 DTMF编码解码

372 lines (336 loc) 14.4 kB
/*** 简单用 正弦波、方波、锯齿波、三角波 函数生成一段音乐简谱的pcm数据,主要用于测试时提供音频数据。本可音频生成插件可以移植到其他语言环境,如需定制可联系作者 https://github.com/xiangyuecn/Recorder 此插件在线生成测试:assets/runtime-codes/test.create-audio.nmn2pcm.js var pcmData=Recorder.NMN2PCM(set); set配置:{ texts:""|["",""] 简谱格式化文本,如果格式不符合要求,将会抛异常 sampleRate: 生成pcm的采样率,默认48000;取值不能过低,否则会削除高音 timbre: 音色,默认2.0(使用音符对应频率的一个倍频),取值>=1.0 meterDuration: 一拍时长,毫秒,默认600ms muteDuration: 音符之间的静默,毫秒,0时无静默,默认meterDur/4(最大50ms) beginDuration: 开头的静默时长,毫秒,0时无静默,默认为200ms endDuration: 结尾的静默时长,毫秒,0时无静默,默认为200ms volume: 音量,默认0.3,取值范围0.0-1.0(最大值1) waveType: 波形发生器类型,默认"sine",取值:sine(正弦波)、square(方波,volume应当减半)、sawtooth(锯齿波)、triangle(三角波) } texts格式:单个文本,或文本数组 - 四分音符(一拍):低音: 1.-7. 中音: 1-7 高音: 1'-7' 休止符(静音):0 - 音符后面用 "." 表示低音(尽量改用".":".." 倍低音,"..." 超低音) - 音符后面用 "'" 表示高音(尽量改用"'":"''" 倍高音,"'''" 超高音) - 音符之间用 "|" 或 " " 分隔一拍 - 一拍里面多个音符用 "," 分隔,每个音按权重分配这一拍的时长占比,如:“6,7”为一拍,6、7各占1/2拍,相当于八分音符 - 音符后面用 "-" 表示二分音符,简单计算为1+1=2拍时长,几个-就加几拍 - 音符后面用 "_" 表示八分音符;两两在一拍里面的音符可以免写_,自动会按1/2分配;一拍里面只有一个音时这拍会被简单计算为1/2=0.5拍;其他情况计算会按权重分配这一拍的时长(复杂),如:“6,7_”为1/2+1/2/2=0.75拍(“6*,7_”才是(1+0.5)/2+1/2/2=1拍),其中6权重1分配1/2=0.5拍,7权重0.5分配1/2/2=0.25拍;多加一个"_"就多除个2:“6_,7_”是1/2+1/2=1拍(等同于“6,7”可免写_);“6__,7__”是1/2/2+1/2/2=0.5拍;只要权重加起来是整数就算作完整的1拍 - 音符后面用 "*" 表示1+0.5=1.5拍,多出来的1/2计算和_相同(复杂),"**"两个表示加0.25 - 可以使用 "S"(sine) "Q"(square) "A"(sawtooth) "T"(triangle) 来切换后续波形发生器类型(按一拍来书写,但不占用时长),类型后面可以接 "(2.0)" 来设置音色,接 "[0.5]" 来设置音量(为set.volume*0.5);特殊值 "R"(reset) 可重置类型成set配置值,如果R后面没有接音色或音量也会被重置;比如:"1 2|A(4.0)[0.6] 3 4 R|5 6",其中12 56使用set配置的类型和音色音量,34使用锯齿波、音色4.0、音量0.18=0.3*0.6 - 如果同时有多个音,必须提供数组格式,每个音单独提供一个完整简谱(必须同步对齐) 返回结果:{ pcm: Int16Array,pcm数据 duration: 123 pcm的时长,单位毫秒 set: {...} 使用的set配置 warns: [] 不适合抛异常的提示消息 } Recorder.NMN2PCM.GetExamples() 可获取内置的简谱 ***/ (function(factory){ var browser=typeof window=="object" && !!window.document; var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面 var rec=win.Recorder,ni=rec.i18n; factory(rec,ni,ni.$T,browser); }(function(Recorder,i18n,$T,isBrowser){ "use strict"; var NMN2PCM=function(set){ var texts=set.texts||[]; if(typeof(texts)=="string") texts=[texts]; var setSR=set.sampleRate, sampleRate=setSR; if(!sampleRate || sampleRate<1)sampleRate=48000; var meterDur=set.meterDuration||600; var timbre=set.timbre||2; if(timbre<1)timbre=1; var volume=set.volume; if(volume==null)volume=0.3; volume=Math.max(0,volume); volume=Math.min(1,volume); var waveType=set.waveType||""; if(",sine,square,sawtooth,triangle,".indexOf(","+waveType+",")==-1)waveType=""; waveType=waveType||"sine"; var muteDur=set.muteDuration; if(muteDur==null || muteDur<0){ muteDur=meterDur/4; if(muteDur>50)muteDur=50; } var mute0=new Int16Array(sampleRate*muteDur/1000); var beginDur=set.beginDuration; if(beginDur==null || beginDur<0) beginDur=200; var beginMute=new Int16Array(sampleRate*beginDur/1000); var endDur=set.endDuration; if(endDur==null || endDur<0) endDur=200; var endMute=new Int16Array(sampleRate*endDur/1000); //生成C调频率 A=440 国际标准音 var s=function(s){ return 440/Math.pow(2,s/12) }; var Freqs=[s(9),s(7),s(5),s(4),s(2),s(0),s(-2)]; var FreqMP={}; for(var i=1;i<=7;i++){ var v=Freqs[i-1]; FreqMP[i+"..."]=v/8; FreqMP[i+".."]=v/4; FreqMP[i+"."]=v/2; FreqMP[i]=v; FreqMP[i+"'"]=v*2; FreqMP[i+"''"]=v*4; FreqMP[i+"'''"]=v*8; } var tracks=[],freqMax=0,freqMin=90000; for(var iT=0;setSR!=-1 && iT<texts.length;iT++){ var meters=texts[iT].split(/[\s\|]+/); var buffers=[],size=0,wType=waveType,wTimbre=timbre,wVol=volume; for(var i0=0;i0<meters.length;i0++){ var txt0=meters[i0]; if(!txt0)continue; var v0=txt0.charCodeAt(0); if(v0<48 || v0>55){//不是0-7,切换波形或音色 var m=/^(\w)(?:\((.+)\)|\[(.+)\])*$/.exec(txt0)||[],mT=m[1]; var m=/\((.+)\)/.exec(txt0)||[],mTb=m[1]; var m=/\[(.+)\]/.exec(txt0)||[],mVol=m[1]; if(mT=="R"){ wType=waveType;wTimbre=timbre;wVol=volume; } else if(mT=="S") wType="sine"; else if(mT=="Q") wType="square"; else if(mT=="A") wType="sawtooth"; else if(mT=="T") wType="triangle"; else mT=""; if(!mT||mTb&&!+mTb||mVol&&!+mVol)throw new Error("Invalid: "+txt0); if(mTb)wTimbre=+mTb; if(mVol)wVol=volume*mVol; continue; } var ys=txt0.split(",");//一拍里面的音符 var durTotal=meterDur; //一拍的时长,如果里面有+,代表多拍 var bTotal=0,hasG=0,hasX=0; for(var i2=0;i2<ys.length;i2++){//先计算出每个音符的占用时长比例 var vs=ys[i2].split(""); var o={ y:vs[0],b:1,t:wType,tb:wTimbre,vol:wVol }; ys[i2]=o; for(var i3=1;i3<vs.length;i3++){ var v=vs[i3]; if(v=="'") o.y+="'"; else if(v==".") o.y+="."; else if(v=="-"){ o.b+=1; durTotal+=meterDur; } else if(v=="_"){ o.b/=2; hasG=1; } else if(v=="*" && !hasX){ o.b+=0.5; hasX=0.5; if(vs[i3+1]=="*"){ o.b-=0.25; hasX=0.25; i3++; } } else throw new Error($T("3RBa::符号[{1}]无效:{2}",0,v,txt0)); } bTotal+=o.b; } if(bTotal%1>0){ if(hasG){//"_"不够数量,减掉时间 durTotal*=bTotal/Math.ceil(bTotal); }else if(hasX){//"*"加上1/2|1/4拍的时间 durTotal+=meterDur*hasX; } } durTotal-=ys.length*muteDur;//减掉中间的静默 for(var i2=0;i2<ys.length;i2++){//生成每个音符的pcm var o=ys[i2],wType=o.t,wTimbre=o.tb,wVol=o.vol,freq=FreqMP[o.y]||0; if(!freq && o.y!="0") throw new Error($T("U212::音符[{1}]无效:{2}",0,o.y,txt0)); freq=freq*wTimbre; var dur=durTotal*o.b/bTotal; var pcm=new Int16Array(Math.round(dur/1000*sampleRate)); if(freq){ freqMax=Math.max(freqMax,freq); freqMin=Math.min(freqMin,freq); //不同波形算法取自 https://github.com/cristovao-trevisan/wave-generator/blob/master/index.js if(wType=="sine"){//正弦波 var V=(2 * Math.PI) * freq / sampleRate; for(var i=0;i<pcm.length;i++){ var v=wVol*Math.sin(V * i); pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF; } }else if(wType=="square"){//方波 var V=sampleRate / freq; for(var i=0;i<pcm.length;i++){ var v=wVol*((i % V) < (V / 2) ? 1 : -1); pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF; } }else if(wType=="sawtooth"){//锯齿波 var V=sampleRate / freq; for(var i=0;i<pcm.length;i++){ var v=wVol*(-1 + 2 * (i % V) / V); pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF; } }else if(wType=="triangle"){//三角波 var V=sampleRate / freq; for(var i=0;i<pcm.length;i++){ var Vi = (i + V / 4) % V; var v=wVol*(Vi<V/2?(-1+4*Vi/V):(3-4*Vi/V)); pcm[i]=Math.max(-1,Math.min(1,v))*0x7FFF; } } var pcmDur4=~~(pcm.length/sampleRate*1000/4)||1; FadeInOut(pcm,sampleRate,Math.min(pcmDur4, 10)); } var mute=mute0; if(!buffers.length)mute=beginMute; buffers.push(mute); size+=mute.length; buffers.push(pcm); size+=pcm.length; } } if(size>0){ buffers.push(endMute); size+=endMute.length; tracks.push({buffers:buffers,size:size}); } } tracks.sort(function(a,b){return b.size-a.size}); var pcm=new Int16Array(tracks[0]&&tracks[0].size||0); for(var iT=0;iT<tracks.length;iT++){ var o=tracks[iT],buffers=o.buffers,size=o.size; if(iT==0){ for(var i=0,offset=0;i<buffers.length;i++){ var buf=buffers[i]; pcm.set(buf,offset); offset+=buf.length; } }else{ var diffMs=(pcm.length-size)/sampleRate*1000; if(diffMs>10){//10毫秒误差 throw new Error($T("7qAD::多个音时必须对齐,相差{1}ms",0,diffMs)); }; for(var i=0,offset=0;i<buffers.length;i++){ var buf=buffers[i]; for(var j=0;j<buf.length;j++){ var data_mix,data1=pcm[offset],data2=buf[j]; //简单混音算法 https://blog.csdn.net/dancing_night/article/details/53080819 if(data1<0 && data2<0){ data_mix = data1+data2 - (data1 * data2 / -0x7FFF); }else{ data_mix = data1+data2 - (data1 * data2 / 0x7FFF); }; pcm[offset++]=data_mix; } } } } var dur=Math.round(pcm.length/sampleRate*1000); var Warns=[],minSR=~~(freqMax*2); if(freqMax && sampleRate<minSR){ var msg="sampleRate["+sampleRate+"] should be greater than "+minSR; Warns.push(msg); Recorder.CLog("NMN2PCM: "+msg,3); } return {pcm:pcm, duration:dur, warns:Warns, set:{ texts:texts, sampleRate:sampleRate, timbre:timbre, meterDuration:meterDur ,muteDuration:muteDur, beginDuration:beginDur, endDuration:endDur ,volume:volume,waveType:waveType }}; }; /**pcm数据进行首尾1ms淡入淡出处理,播放时可以大幅减弱爆音**/ var FadeInOut=NMN2PCM.FadeInOut=function(arr,sampleRate,dur){ var sd=sampleRate/1000*(dur||1);//浮点数,arr是Int16或者Float32 for(var i=0;i<sd;i++){ arr[i]*=i/sd; } for(var l=arr.length,i=~~(l-sd);i<l;i++){ arr[i]*=(l-i)/sd; } }; /***内置部分简谱*****/ NMN2PCM.GetExamples=function(){ return { DFH:{//前3句,https://www.hnchemeng.com/liux/201807/68393.html name:"东方红" ,get:function(sampleRate){ return NMN2PCM({ //https://www.bilibili.com/video/BV1VW4y1v7nY?p=2 sampleRate:sampleRate ,meterDuration:1000 ,timbre:3 ,texts:"5 5,6|2-|1 1,6.|2-|5 5|6,1' 6,5|1 1,6.|2-" }); } } ,HappyBirthday:{//4句,https://www.zaoxu.com/jjsh/bkdq/310228.html name:$T("QGsW::祝你生日快乐") ,get:function(sampleRate){ return NMN2PCM({ sampleRate:sampleRate ,meterDuration:450 ,timbre:4 ,waveType:"triangle", volume:0.15 ,texts:"5.,5. 6. 5.|1 7.-|5.,5. 6. 5.|2 1-|5.,5. 5 3|1 7. 6.|4*,4_ 3 1|2 1-" }); } } ,LHC:{//节选一段,https://www.qinyipu.com/jianpu/jianpudaquan/41703.html name:"兰花草(洒水版)" ,get:function(sampleRate){ return NMN2PCM({ sampleRate:sampleRate ,meterDuration:650 ,timbre:4 ,texts:"6.,3 3,3|3* 2_|1*,2_ 1,7.|6.-|6,6 6,6|6* 5_|3_,5_,5 5,4|3-|3,3_,6_ 6,5|3* 2_|1*,2_ 1,7.|6. 3.|3.,1 1,7.|6.* 2__,3__|2*,1_ 7._,7._,5.|6.-" }); } } ,ForElise:{//节选一段,https://www.qinyipu.com/jianpu/chunyinle/3023.html name:$T("emJR::致爱丽丝") ,get:function(sampleRate){ return NMN2PCM({ sampleRate:sampleRate ,meterDuration:550 ,muteDuration:20 ,timbre:6 ,texts:"3',2'|3',2' 3',7 2',1'|" +"6 0,1 3,6|7 0,3 5,7|1' 0 3',2'|" +"3',2' 3',7 2',1'|6 0,1 3,6|7 0,3 1',7|" +"6 0,7 1',2'|3' 0,5 4',3'|2' 0,4 3',2'|1' 0,3 2',1'|" +"7" }); } } ,Canon_Right:{//节选一段,https://www.cangqiang.com.cn/d/32153.html name:$T("GsYy::卡农-右手简谱") ,get:function(sampleRate){ return NMN2PCM({ sampleRate:sampleRate ,meterDuration:700 ,texts:"1',7 1',3 5 6,7|" +"1' 3' 5',3' 5',6'|4',3' 2',4' 3',2' 1',7| 7 1',2'|" +"5',3'_,4'_ 5',3'_,4'_ 5',5,6,7 1',2',3',4'|3',1'_,2'_ 3',3_,4_ 5,6,5,4 5,1',7,1'|" +"6,1'_,7_ 6,5_,4_ 5,4,3,4 5,6,7,1'|6,1'_,7_ 1',7_,6_ 7,6,7,1' 2'_,1'_,7|1'-" }); } } ,Canon:{//开头一段,https://www.kanpula.com/jianpu/21316.html name:$T("bSFZ::卡农") ,get:function(sampleRate){ var txt1="",txt2="",txt3="",txt4=""; //(1) txt1+="3'---|2'---|1'---|7---|"; txt2+="1'---|7---|6---|5---|"; txt3+="5---|5---|3---|3---|"; txt4+="R[0.3] 1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|"; //(5) txt1+="6---|5---|6---|7---|"; txt2+="4---|3---|4---|5---|"; txt3+="1---|1---|1---|2---|"; txt4+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5.. 2. 5. 7.|"; //(9) txt1+="3'---|2'---|1'---|7---|"; txt2+="1'---|7---|6---|5---|"; txt3+="5---|5---|3---|3-- 5'|"; txt4+="1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|"; //(13) txt1+="4' 3' 2' 4'|3' 2' 1' 5|6- 6 1'|7 1' 2'-|"; txt2+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5.. 2. 5. 7.|"; txt3+="0---|0---|0---|0---|"; txt4+="0---|0---|0---|0---|"; //(17) txt1+="3',5 1'_ 5' 5_ 3'|3' 4' 3' 2'|1',3 6_ 3' 3_ 1'|1' 2' 1' 7|"; txt2+="1. 5. 1 3|5.. 2. 5. 7.|6.. 3. 6. 1|3.. 7.. 3. 5.|"; txt3+="0---|0---|0---|0---|"; txt4+="0---|0---|0---|0---|"; //(21) txt1+="6,1 4_ 1' 1_ 6|5,1 3_ 1' 1_ 5|6,1 4_ 1' 1_ 6|7 7 1' 2'|"; txt2+="4.. 1. 4. 6.|1. 5. 1 3|4.. 1. 4. 6.|5..,5. 5..,5. 6..,6. 6..,6.|"; txt3+="0---|0---|0---|0---|"; txt4+="0---|0---|0---|0---|"; return NMN2PCM({ sampleRate:sampleRate ,meterDuration:500 ,texts:[txt1,txt2,txt3,txt4] }); } } } }; Recorder.NMN2PCM=NMN2PCM; }));