Little Strange Software

スマホアプリの開発を行う LittleStrangeSoftware のブログです。

【WebAudioAPI】MMLプレイヤー、タイミング合わせに成功しました^^

 どうも!LSSです!!

 

JavaScriptで曲を演奏できる!と知ってから取り掛かり始めた、MMLプレイヤー作成ですが、

複数のメロディを再生できるようにはなったものの、タイミングがズレてしまう!という致命的な問題にぶち当たっていました^^;

 

が、解決策を見つけ、試してみたところ、無事成功しちゃいました!!

他に実装したい機能もあるにはありますが、とりいそぎ、お披露目しちゃいます^^

 

 

f:id:little_strange:20210421232659p:plain

MMLプレイヤー

 

 

 

まずは「鳴らす」を押してみてください。 

iPhoneの方は「鳴らす(iPhone用テスト)」のほうをタップ)

 

皆さんよくご存じの曲が演奏されます♪

 

テキストエリアが3つあり、それぞれが1つのメロディをあらわすMMLです。

 

このMMLの書き方については

【WebAudioAPI】MMLプレイヤー制作中【なにかがおかしい】

に書いていますので、そちらを参考に書き換える事で、好きな曲を演奏させる事ができます!

 

 

コード 

<p><textarea id="mml0" cols="32" rows="4">曲データ</textarea></p>
<p><textarea id="mml1" cols="32" rows="4">曲データ</textarea></p>
<p><textarea id="mml2" cols="32" rows="4">曲データ</textarea></p>
<p> </p>
<p><input id="btn" type="button" value="鳴らす" /><input id="stopbtn" type="button" value="止める" /></p>
<p><input id="btnt" type="button" value="鳴らす(iPhone用テスト)" /></p>
<script>// <![CDATA[
AudioContext = window.AudioContext || window.webkitAudioContext;
ctxa=new AudioContext();
btn.addEventListener('click',mmlplay,false);
btnt.addEventListener('touchend',mmlplay,false);
stopbtn.addEventListener('click',function(){for(i=0;i<3;i++){clearTimeout(tm[i]);}},false);
oct=[];
ngs=[];
tm=[];
chw=[];
cttm=0;
onkai=[0,2,4,5,7,9,11];
pos=[];
function mmlplay(){
cttm=Date.now();
for(i=0;i<3;i++){
pos[i]=0;
oct[i]=4;
ngs[i]=0.25;
chw[i]=0;
tmr(i);
}
}
function tmr(ch){
mj=document.getElementById('mml'+ch).value;
mj0=mj.charAt(pos[ch]);
mj1=mj.charAt(pos[ch]+1);
if(mj1=='#'){sh=1;}else{sh=0;};
pos[ch]+=sh+1;
nagasa=ngs[ch];
mj2=mj.substr(pos[ch]).match(/^\d+/);
if(mj2!=null){
pos[ch]+=mj2.length;
nagasa=2/parseFloat(mj2);
if(mj.charAt(pos[ch])=='.'){
pos[ch]++;nagasa*=1.5;
}
}
if(mj0>='a' && mj0<='g'){
oc0p(oct[ch]*12+onkai['cdefgab'.indexOf(mj0)]+sh,nagasa);
chw[ch]+=nagasa*1000;
tm[ch]=setTimeout('tmr('+ch+')',cttm+chw[ch]-Date.now());}
else if(mj0=='r'){chw[ch]+=nagasa*1000;tm[ch]=setTimeout('tmr('+ch+')',cttm+chw[ch]-Date.now());}
else if(mj0=='l'){ngs[ch]=nagasa;tmr(ch);}
else if(mj0=='o'){oct[ch]=parseInt(mj2);tmr(ch);}
else if(mj0=='<'){oct[ch]++;tmr(ch);}
else if(mj0=='>'){oct[ch]--;tmr(ch);}
else{if(mj.length>=pos[ch]){tmr(ch);}
}
}
function oc0p(a,b) {
oc0=ctxa.createOscillator();
oc0g=ctxa.createGain();
oc0t=ctxa.currentTime;
oc0.type='triangle';
oc0.frequency.value=442*Math.pow(2,(a-9)/12-4);
oc0g.gain.setValueAtTime(0.01, oc0t);
oc0g.gain.linearRampToValueAtTime(0.3, oc0t+b/5 );
oc0g.gain.linearRampToValueAtTime(0.01, oc0t+b );
oc0.connect(oc0g);
oc0g.connect(ctxa.destination);
oc0.start();
oc0.stop(oc0t + b);
}
// ]]></script>

 

 

音ズレの解決策

前バージョンまでは、「音を鳴らす」「休符の分待ち時間」の際に、音の長さ・休符の長さ分、setTimeoutでMML解析を中断していました。 

 

が、どうにも処理落ちが入るらしく、3つあるメロディチャンネルごとの処理落ち時間がズレる事によって、積もり積もって聴いてて分かるほどの音ズレになっていたようです^^;

 

そこで「処理落ちはどうしても発生するもの」として、
「曲を演奏し始めた時刻」を変数に記録し(cttm=Date.now();
MML解析を中断し、次の呼び出しまでの待ち時間を決めるsetTimeout」を、

tm[ch]=setTimeout('tmr('+ch+')',cttm+chw[ch]-Date.now());

と書き換えた事によって、
「計算で算出した次のタイミングから、その時点の時刻を引いた分」
だけ待つ、という仕様に変更しました!

 

例えていえば「40分用事をして20分休憩してから、ここに集合!」と言って待ち合わせていたのを「〇時〇分になったら、ここに集合!」 という形に変えた、って感じですw

 

 

あとがき

これでやっと次に進められそうです^^ 

テンポ(演奏速度)を変えたり、音色を変えたりに挑戦したいところですが、後者は難しそうかも?w

 

ブログ的には、昨日の問題

の正解を1記事にするのが順当そうでしたが…やっぱ、
この曲は4/21に上げるべき!
ですよねw

  

421miyako様、お誕生日おめでとうございます^^

 

 

 

ってなとこで、今回はこのへんで!

次回もまた、よろしくお願いします^^