WebWorkersで別タブ選択中もきっちり動く無敵タイマーをつくる

ずっと悩んでいたのだけど、僕の書いた簡単便利プレイヤーではブラウザの別タブを選択すると音が途切れまくって格好よくなるという問題があった。原因はわかっていて setInterval でタイマー処理していると、別タブ選択時に精度が非常に悪くなる。requestAnimationFrame と同じで見ていないからサボるってことなんだけど、音を出してるときはサボられると困るわけで、もっとこう、無敵なタイマーがないものかと思っていた。

そしたら id:ultraist さんにコメントをもらって、WebWorkers を使えばどうにかなるっぽい事がわかった。

WebWorkersはバックグラウンド処理するためのAPIでメッセージのやり取りで並列処理ができる。


Web Workers
http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html
http://www.html5rocks.com/en/tutorials/workers/basics/
http://dev.w3.org/html5/workers/
http://ascii.jp/elem/000/000/560/560326/


まだ、ちゃんと読んでいないんだけど、ちょっと修正しただけで簡単に無敵タイマーが作れたので書いておく。

問題のコード

MozPlayer.prototype.play = function() {
    var self = this;
    this._timerId = setInterval(function() {
        self._audio.mozWriteAudio(self._stream);
        self._stream = self._generator.next();
    }, this.PLAY_INTERVAL);
};
MozPlayer.prototype.stop = function() {
    if (this._timerId !== 0) {
        clearInterval(this._timerId);
        this._timerId = 0;
    }
};

実際のコードとはちょっと違うけど大体こんな感じ

  1. PLAY_INTERVALの間隔(50msくらい)でタイマー処理をしている
  2. self._stream は再生する音のシグナル配列で self._audio.mozWriteAudio(self._stream) で再生
  3. mozWriteAudioは非ブロックなので次の音のシグナル配列を読み込む
  4. 2に戻る

ここで setInterval を使っているのがよくない。別タブを選択するとサボりだして、音が途切れる。

WebWorkers を使った無敵タイマーバージョン

まず別ファイルが必要

// muteki-timer.js
var timerId = 0;
onmessage = function(e) {
    if (timerId !== 0) {
        clearInterval(timerId);
        timerId = 0;
    }
    if (e.data > 0) {
        timerId = setInterval(function() {
            postMessage(null);
        }, e.data);
    }
};

メッセージに応じて空メッセージを送るだけのタイマーを起動/終了する。


プレイヤーのコード修正

MozPlayer.prototype.init = function() {
    var self = this;
    this._timer = new Worker("muteki-timer.js");
    this._timer.onmessage = function(e) {
        self._audio.mozWriteAudio(self._stream);
        self._stream = self._generator.next();
    };
};
MozPlayer.prototype.play = function() {
    this._timer.postMessage(this.PLAY_INTERVAL);
};
MozPlayer.prototype.stop = function() {
    this._timer.postMessage(0);
};

大体こんな感じ。コードの場所が変わっただけで中身はそのまま。超簡単。
メッセージのやり取りの分だけオーバーヘッドがあるんだけど、別タブ選択時に音が途切れなくなるメリットの方が大きい。オーバーヘッドって言っても些細なものみたいだし。


非同期パフォーマンス - JavaScriptで遊ぶよ - g:javascript
http://javascript.g.hatena.ne.jp/edvakf/20100227/1267246371


ただし、ひとつハマった部分があって、僕の環境(OSX Firefox 8, 127.0.0.1:3000)で new Worker() をコールすると "Could not get domain!" ってエラーが出て Worker のインスタンスが作れない。デプロイすると動く。


これっぽい
https://bugzilla.mozilla.org/show_bug.cgi?id=683280



今回はトリガーとしてしか使っていないけど、UIのスレッドとは別の場所(スレッド)で実行されるので、音楽系の処理は全部バックグランドにまわすとか、ガンガン使うとガンガン早くなりそう。

メモ

  • メッセージは 数値、文字列、リスト、オブジェクト(要はJSON)が送れる
  • new Worker() の引数がファイル名なのちょっとつらい
  • 非常に時間がかかる処理を泣く泣くsetIntervalで分割処理したりしなくてすむ(むしろこれが本来の使い方)
  • BlobBuilderを使えば別ファイルでなくてもよいみたい

ウェブ楽器

別タブを選択しても音が途切れない優れもの!無料です!!
Chrome(一番よい), Firefox(まあまあ), Opera(いまいち) に対応しています。


Endless Invention (バッハインターフェイスの無限インベンション)
http://mohayonao.herokuapp.com/invention


windmills (大量の風車インターフェイスのやつ)
http://mohayonao.herokuapp.com/windmills


KSDN-808II (関西電気保安協会リズムマシーン)
http://ksdn808.herokuapp.com/


ONE-LINER-ORCHESTRA (短いコードで音楽つくるやつ)
http://one-liner-orchestra.herokuapp.com/