Слияние кода завершено, страница обновится автоматически
/***
简单用 正弦波、方波、锯齿波、三角波 函数生成一段音乐简谱的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;
}));
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )