RSS Twitter Facebook

2016/05/13 (2016年05月 のアーカイブ)

Chrome:Web Audio APIのオートメーションの問題

今まで色々と Web Audio 関係のアプリを作ってきて、時々「あれ、おかしいなー」と思いつつずっと放置していた問題があったのですが一応原因らしきものにたどり着きました。症状としては Chrome で Web Audio アプリを長時間動作させるといつの間にか動作がおかしい / 止まっていたりすると言う感じになります。アプリの作りやマシンパワーで変わりますが、普通に音楽を鳴らすようなアプリだと起動してから1時間ほど経ってから症状が出たりしますのでなかなか因果関係が掴めませんでした。

これはノードの各種パラメータを設定する AudioParam を時間軸で操作する setValueAtTime() 等のオートメーションメソッドを使うと徐々に全体の動作が重くなって行くのが原因です。

このあたりのBlinkのソースを読んだ事があるという@mohayonaoさんの話によれば、使い終わったオートメーションイベントが破棄されずに毎回新規イベントの挿入場所を効率の悪い探し方をしているそうなのでそのせいでしょう。

という事でテストプログラム :
var actx=new AudioContext();
var osc=actx.createOscillator();
var gain=actx.createGain();
gain.gain.value=0;
osc.connect(gain);
gain.connect(actx.destination);
osc.start(0);

function Trigger(){
  var t=actx.currentTime;
  gain.gain.setValueAtTime(0,t+0.1);
  gain.gain.setValueAtTime(1,t+0.101);
  gain.gain.setValueAtTime(0,t+0.102);
  gain.gain.setValueAtTime(1,t+0.103);
  gain.gain.setValueAtTime(0,t+0.104);
}
setInterval(Trigger,50);
極端な例ですが毎秒100発のオートメーションを発行して Chrome DevTools でプロファイルを見てみました。 setValueAtTime()がどんどん遅くなってますね。ヒープも少しずつ増えているようなのですがオートメーションイベント自体は小さいものなので、それほど目立つヒープの肥大がないというのも発見しにくい所です。

という事でとりあえず今、これを回避するには
  1. オートメーションメソッドを使わない
  2. 使ったオートメーションを時々消してやる
  3. オートメーションを使う頻度と必要な連続稼働時間の兼ね合いで見なかった事にする
というような手法が必要になります。今までの Web Audio の実情って結構 3. だったかも知れません。1. は代わりに.valueへの直接代入で自前で制御する必要があり、せっかくの高精度時間制御が使えない事になります。2. は消すメソッドが指定時刻以降を消す cancelScheduledValues()しかないため、毎回全消しして登録済みの未実行オートメーションを再登録する必要があります。どっちも面倒ですね。

まだ不完全なものですが一応 2. の方向で多少楽になる関数を作りました。Polyfill的にするのはまた面倒なのであまり変更を必要としない簡単なHelper的なもので。まだ足りないメソッドもあります。
function Automation(ctx,param,v,t){
	this.param = param;
	this.cmdbuf = [];
	this.Cancel = function(){
		while(this.cmdbuf.length && ctx.currentTime > this.cmdbuf[0][2])
			this.cmdbuf.shift();
	};
	this.Reque = function(){
		this.param.cancelScheduledValues(0);
		for(var i = 0,l = this.cmdbuf.length; i < l; ++i){
			var buf = this.cmdbuf[i];
			switch(buf[0]){
			case 0:
				this.param.setValueAtTime(buf[1],buf[2]);
				break;
			case 1:
				this.param.linearRampToValueAtTime(buf[1],buf[2]);
				break;
			case 2:
				this.param.exponentialRampToValueAtTime(buf[1],buf[2]);
				break;
			case 3:
				this.param.setTargetAtTime(buf[1],buf[2],buf[3]);
				break;
			}
		}
	};
	this.cancelScheduledValues = function(t){
		this.param.cancelScheduledValues(t);
	};
	this.setValueAtTime = function(v,t){
		this.Cancel();
		this.cmdbuf.push([0,v,t]);
		this.Reque();
	};
	this.linearRampToValueAtTime = function(v,t){
		this.Cancel();
		this.cmdbuf.push([1,v,t]);
		this.Reque();
	};
	this.exponentialRampToValueAtTime = function(v,t){
		this.Cancel();
		this.cmdbuf.push([2,v,t]);
		this.Reque();
	};
	this.setTargetAtTime = function(v,t,c){
		this.Cancel();
		this.cmdbuf.push([3,v,t,c]);
		this.Reque();
	};
}
使い方としてはオートメーションを使いたい AudioParam に対して、
  var oscfreq = new Automation(audiocontext, osc.frequency);
のように Automation のオブジェクトを作り、そのオブジェクトに対して
oscfreq.setValueAtTime(440,t);
という風にオートメーションメソッドを発行します。さっきのテストプログラムの場合だと次のようになります。
var actx=new AudioContext();
var osc=actx.createOscillator();
var gain=actx.createGain();
var gaingain=new Automation(actx,gain.gain);
gain.gain.value=0;
osc.connect(gain);
gain.connect(actx.destination);
osc.start(0);

function Trigger(){
  var t=actx.currentTime;
  gaingain.setValueAtTime(0,t+0.1);
  gaingain.setValueAtTime(1,t+0.101);
  gaingain.setValueAtTime(0,t+0.102);
  gaingain.setValueAtTime(1,t+0.103);
  gaingain.setValueAtTime(0,t+0.104);
}
setInterval(Trigger,50);
境界条件とかちゃんと検証してませんし多少残念感が残る応急措置ですが一応これで長時間連続稼働が可能になりました。その内誰かがもっとちゃんとしたものを作ってくれるかも知れません。
また、これそのものではないですが関連するバグレポートは既にChrominumのBugTrackerに既にあがっているようで、それが直れば一緒に解消されそうです。早く対処されると良いですね。

Posted by g200kg : 2016/05/13 12:18:48