はじめに
基本の「き」でありながら、なかなかどうして簡単にはいかない時間管理について少し掘り下げてみようと思います。そしてカレンダーのほうに空きもあるので、欲張らずに何回かにわけて書いてみようと思います。今回は時刻について。
Webから見える時刻
Date.now()
デートなう(^o^)v
いわゆるUNIX時間、1970年からの経過時間をミリ秒単位で表現した数値です。現在時刻に紐付いているため、時計をずらすと戻ることもある(はずの)ふわふわタイムです。
performance.now()
DOMHighResTimeStampを返してくれます。これは現在のWindowで実行コンテキストを生成した瞬間からの経過時間をミリ秒単位で表現した数値です。ざっくり言えばページを開いてからの経過時間。リロードすれば0から数えなおしてくれますし、現在時刻とは独立しているため、時計をずらしても影響を受けずに淡々と時を刻み続けてくれます。詳しい人向けに言うとmonotonic clock。
Event.timeStamp
イベントリスナーが受けるとEventオブジェクトにくっついている、イベント発生時刻を表現するためのタイムスタンプです。実は最近になって大きな仕様変更がありました。今まではDate.now()と同じUNIX時間だったのですが、performance.now()と同じDOMHighResTimeStampを返すようになりつつあります。実はChromeでは49以降から変更されています。こんなメジャーなところを変えたら、さぞかしWebは壊れるだろう……と思ってたんですが、意外と大丈夫みたいで世の中わからないものです。
追記:他のブラウザも巻き込んでの移行が検討されています。詳細はwhatwg DOMのissue #23を追ってみると良いでしょう。
Web MIDIにまつわる時刻
MIDIMessageEvent.receivedTime
Event.timeStampが音楽にとって不都合だったために別途用意されたDOMHighResTimeStampの値です。
Chromeの実装では、OSがデータ受信時刻を提供していればその値を、持っていなければOSからデータを受け取った時点の時刻情報を使って生成しています。Chromeの管理プロセスがデータを受け取った後、ページごとの権限を確認して別プロセスで動いているレンダリングエンジンにデータを転送、さらにJavaScriptのランタイムへイベントとして配送する手間があり、ここで通常は1m秒ほどかかっています。ただしJavaScriptのメインスレッドが別の仕事をしていたりGCが走ったりするともっと遅延が乗ることもあります。そんな時でもreceivedTimeを使えばバッチリ本来の時刻がわかるというわけです。この受信データを使ってライブパフォーマンスをしている場合にはどうしようもありませんが、そういう時は得てして他の処理も重くなっているので諦めましょう。レコーディングなら本来のタイミングを保持できます。
と、ここまで説明しておきながら、実はChrome 56にて削除されます。と言っても、receivedTimeは必要なかった!みたいな話ではなく、先程書いたとおりEvent.timeStampがDOMHighResTimeStampを返すようになったために役目を終えて引退、という事です。MIDIMessageEventに関してはtimeStampが今まで説明したようなreceivedTimeと全く同一の値を持ちます。
追記:もちろんEvent.timeStampの話がなかった事になれば復活する可能性もあります。が、DOM仕様を議論している人たちが個別のAPI仕様を議論している場所に来て「これからはEvent.timeStampを使って」と言って回っている状況なので、まぁ大丈夫かな、と。
MIDIOutput.send(data, timestamp)
Web MIDIは送信時にもtimestampを付けることができます。これは未来に送信したいデータを予約するために使います。と言っても、楽曲データ丸々を一括で送りつけるような用途を想定しているわけではなく、メインスレッドが忙しかったりGCでタイミングが厳密に管理できないために発生するジッタを回避する目的で導入されています。これはどういう事かというと、描画で言う所のsetTimeout/setIntervalとrequestAnimationFrameの関係みたいな物だと思ってください。でも、できる事はそれよりちょっと優秀。
描画の例で考えてみます。60fpsで描画しようとsetInterval(f, 1000/60)としたいところですが、厳密には1フレーム毎の処理ができません。1000/60ミリ秒という数値と実際のオシレータによって制御されたグラフィックカードの描画タイミングの間に誤差があるのはもちろんですが、他の処理やGCによって邪魔をされる可能性があります。簡単な例ですが大量のイベント発生によって細かいタスクが山積みされている場合など、タイマーが発火したところでこれらの処理が終わって順番が回ってくる頃には予定時刻を大幅にオーバーしているかもしれません。一方でrequestAnimationFrameを使えば、グラフィックカードの描画パイプラインに同期する厳密なタイミングでタイマーを発火させる事ができます。積まれているタスクが多くても緊急度が高い事がわかっているので優先的に呼び出してもらう事もできるでしょう。GCの問題も緩和できるかもしれません。一方で実行時間の長いタスクの問題は回避できません。これは割り込み処理でない以上どうしようもありません。
一方でsendのtimestampを指定した場合にはブラウザの実装内部で専用のスレッドを立ててスケジュール管理する事ができます。これによりJavaScriptが忙しかろうがGCで停止していようが一切関係なく厳密なタイミングで処理されます。楽曲再生時にはこれをうまく使ってあげてください。つまり演奏ループ自体はsetIntervalで100ミリ秒毎にラフに回して、その中で今らから先200ミリ秒までの間に再生すべきデータを予約するのです。これで次のタイマーが100ミリ秒遅れても大丈夫。これでもタイミングが気になるようならインターバルを縮める、さらに先の未来まで予約発行する、などのチューニングが可能です。少し面倒ですが、本当の割り込み処理を正しく書くよりはずっと楽です。
ちなみにOSレベルで時刻情報を持たせてMIDIを送出できるmacOSでは、ここで指定した値が反映された形でMIDIメッセージが送出されますので、ブラウザとDAWなどの間で理想的な時刻情報のやり取りが可能となっています。
Web Audioにまつわる時刻
AudioContext.currentTime
Web Audioの基本となる時間で、今までのどの時間とも違います。これはAudioContexを生成してからの時間を秒(実数)で表現しています。ミリ秒ではなく秒なので注意。また、同じページ内で複数のAudioContextを作れば、それぞれが違う値を持ちますので、どのAudioContextで実行しているかは注意する必要があります。
AudioScheduledSourceNode.start(when)/stop(when)
MIDIOutput.send()と同様にwhenを使って未来の再生・停止を予約できます。ここで使うのはAudioContext.currentTimeを基準にした時刻です。0またはcurrentTimeより小さな値を指定した場合には即時実行、それ以外はwhen-currentTime秒後に実行となります。
AudioParam
Web Audioでは多くのパラメータがAudioParamとして実装されており、予約しておいた関数に従って値を自動的に書きかえる事が可能となっています。これはMIDIOutput.send()より遥かに強力で、ADSRエンベロープなどもAudioParamのオートメーションを予約する事で実現できます。startTime、endTime、cancelTimeなどありますが、この際に用いられる値もまたcurrentTimeを基準とした値となります。
まとめ
という事で、まとめると大まかに3種類の時刻が出てきました。
- Date.now()
- 現在時刻です
- DOMHighResTimeStamp
- ページを開いてからの時間です
- AudioContext.currentTime
- 該当AudioContextを作ってからの時間です
そして、音楽をやる俺達にデートなう(^o^)vは不要。DOMHighResTimeStampとAudioContext.currentTimeを使いこなせればバッチリOKという事になります。
次回はAudioとMIDIの間の時間管理について。西海岸のハッカソンで作ってきたPolymerモジュールを紹介しつつお話できたら、と思います。