This repository has been archived on 2025-05-21. You can view files and clone it, but cannot push or open issues or pull requests.
mp3-to-text/public/extensions/create-audio.nmn2pcm.js
2025-04-20 18:46:48 +08:00

372 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/***
简单用 正弦波、方波、锯齿波、三角波 函数生成一段音乐简谱的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: 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;
}));