Web MIDI API、Androidでsendのtimestampが無視されるので無理やり対処

 「AndroidのChromeのWeb MIDI APIでsendのtimestampが無視されているっぽい」の続き。Web MIDI APIが持っている時間制御がうまくいかないなら時前でやるしかないんだなあ、という結論に。

 ということで、いろいろやったこと、調べたことをメモ。

 まずは、棒読みシーケンサーで作ったやつを利用しようと思ったのだけど、もっと汎用性があるものにしようと判断。オリジナルのsendメソッドに代わるものを作って追加しようと思った。
 調べてみると、メソッドの上書きができる模様。新しいメソッド内にオリジナルのメソッドを入れると無限ループみたいになってしまったので、オリジナルはコピーしておいて、それを新しいメソッド内で使うことで対処。無理やり。JavaScriptの、というかプログラミングの作法がまだわかってないけど、それなりに動くようになった。

 sendメソッドのtimestampは、時間を指定する。window.performance.now()で現在の時刻(ブラウザを開いた時点からカウントアップされている)を取得し、xxミリ秒後に発音するなら、「window.performance.now() + xx」みたいな感じで指定する。以下に例を挙げる。

var now=window.performance.now();
mOut.send([0x91, 0x4f, 0x7f], now);
mOut.send([0x80, 0x4f, 0x00], now+50);
mOut.send([0x91, 0x48, 0x7f], now+100);
mOut.send([0x80, 0x48, 0x7f], now+200);

 1行目で現在の時刻を取得。2行目でノートオン、3行目でノートオフ。4、5も同じ。2~5行目までは2つめのパラメータであるTimestampで、タイミングがずらされている。

 参考にしているプログラムは、ちょっと前のエントリで書いたポケミクにしゃべらせるKodamaだ。

 じゃあ、新しく作るメソッドはtimestampと現在時刻から差分を取って、その分待てばいいのだろう。とはいえ、タイマー関連はいまだにうまく扱える気がしない。そこで、こんなのを利用。

 JavaScript – JQuery.Deferredでwait風メソッド – Qiita

 jQueryでDefferredを使うやつ。タイマー関連を調べていて、Promiseについてもいまひとつ理解できなかったのだけど、これならなんとかわかった。これを使わせてもらう。

 元のプログラムにはsendを使っている部分が多いのだけど、そのへんにはまったく手を付けずに済みそう。

 元のプログラムではこうなっている。

mOut = outputs[selIdx];

 これにいろいろ追加して、以下のように。

mOut = outputs[selIdx];
mOut.sendOriginal = mOut.send;
mOut.send = function(data, timestamp){      //data:データ、timestamp:送るべき時刻
var sendData = data;
var now = (window.performance.now()); //now   今の時刻
var timing = (timestamp - now);       //timing    何ms後に送るか
$.wait(timing)
.then(function(){
mOut.sendOriginal(sendData);
});
}

 これでうまくいくかと思ったら、おかしなところも。

 時折、音が正常に鳴らない。プツってなったりする。

 console.log()してみると、前の音のノートオフと、次の音のノートオンの順番が逆になっていることがある。その差はほんの数ミリ秒(どんな場合でも10ミリ秒以下)。本来は同じタイミングのはず。ブラウザのタイマーの精度なのか、それとも適当に書いた処理からくるものか。

 発音した直後に、ノートオフが来るので、ちゃんとしゃべってくれないのだ。たぶん。

 ということで、ノートオンだけちょっと遅らせることに。

 この時点で最初の「汎用性」からはかけ離れたものに。

 元のプログラムでは、ノートオフの代わりに、ノートオンをベロシティを0でまかなっている(わりとよく見る手法)。一方、発音ではベロシティが127だ。で、チャンネルはポケミクのパートなので、1だ。(プログラムでは0になるので、0x90 + 0 = 0x90 を指定)。sendに送られてくるMIDIデータは[チャンネル, ノートNo, ベロシティ]なので、data[0]とdata[2]を見て判断することに。

 以下のようになった。

mOut = outputs[selIdx];
mOut.sendOriginal = mOut.send;
mOut.send = function(data, timestamp){      //data:データ、timestamp:送るべき時刻
var sendData = data;
var now = (window.performance.now()); //now   今の時刻
var timing = (timestamp - now);       //timing    何ms後に送るか
if( (data[0] == 0x90 ) && (data[2] == 127 )   ){  //NOTE ONだけ遅らせる(NOTE OFFと逆になるのに対処)
timing = timing + 10;
}
$.wait(timing)
.then(function(){
mOut.sendOriginal(sendData);
});
}

 とりあえず、これでうまくいっている。きっともっとスマートな方法はあるんだろうな、と思いつつ、自分のスキルではこれが限界。

 という、記録でした。

 で、Androidで動いてるやつを動画で。


Androidで音声認識してポケット・ミクにしゃべらせる / Web MIDI API, NSX-39, Speech Recognition

 でもって、Androidに無理やり対応させたデモはこれ

 元のAndroidで動かなかったやつはこれ

 (追記:
 2017年10月現在、動くのはこれ
 ここまで書いたところで、Androidを見ると、Chrome Betaの新バージョン42.0.2311.51が来ていた。しかし、これでもsendのTimestampの状況は変わらず。というメモ。

コメント

タイトルとURLをコピーしました