Tone.js は、Web Audio API を簡単に扱うためのライブラリです。
UIに関する機能は持たずに完全に音を出すことに特化しており、サンプルプレイバックや簡単なシンセサイザーも扱えるので、Webアプリに効果音をつけるだけでなく、これ1つで複数の楽器を使ったアンサンブル音源を構成することも可能です。
また、BPMでテンポ指定可能なスケジューラーも用意されているので、シーケンサー的なものも簡単に作れます。
今回は、Tone.jsのスケジューラーのちょっとした小ネタを紹介したいと思います。(ホント小ネタですいません)
リズムがバタつく!?(→正しくコードが書けてないだけ)
下記のようなコードを書いたところリズムがめちゃくちゃバタつきました。
const sampler = new Tone.Sampler({...}); // ← wavデータなどを読み込んでサンプラーを作成。詳細は割愛。
Tone.Transport.scheduleRepeat((time) => {
sampler.triggerAttackRelease("C1","1n");
},"16n");
上記コードでは、コールバックが一定のインターバルで正確に呼び出されないと、正確なリズムを刻むことはできませんが、上記コードを書いた時の私は「コールバックは正確なインターバルで呼び出されるだろう。」という勝手な幻想を抱いていたようです。
実際には、コールバックはタイマー割り込みのように一定のインターバルで正確に呼び出されるわけではない! ということのようです。
そういえば、そうでした。
どうすれば正確になる?
コールバックのタイミングは正確ではありませんので、コールバックから即時再生するような音源呼び出しをするのではなく、「音を鳴らすタイミングを予約する」 機能を使って、近い未来の時間へ再生を予約する必要があるようです。
Tone.js
では、xxx.triggerAttackRelease()
の第3パラメータに(今より未来の)時刻を設定すれば、その時に音を鳴らしてくれます。この機能を使えば解決です!
そして、実は Tone.Transport.scheduleRepeat()
に設定したコールバック関数のパラメータとして、次のTick時刻が渡されますので、この時刻をxxx.triggerAttackRelease()
の第3パラメータに設定すれば良いだけです。
というわけで、下記のように修正してみました。
const sampler = new Tone.Sampler({...}); // ← wavデータなどを読み込んでサンプラーを作成。詳細は割愛。
Tone.Transport.scheduleRepeat((time) => {
sampler.triggerAttackRelease("C1","1n",time);
},"16n");
簡単でしたね!
まとめ
Tone.js でタイトなリズムを刻むには、xxx.triggerAttackRelease()
の第3パラメータをちゃんと設定しましょう。というお話でした。
参考情報
(tone.jsの Transport
もおそらく内部で利用しているであろう)setInterval
やrequestAnimationFrame
などを使ったスケジューラが一定のインターバルで発火されない状況や、そうした状況を前提として AudioContext.currentTime
などを用いた正確なリズムを刻む方法については @toyoshim さんが 丁寧に解説してくれていますので、ぜひ参考にしてください。
例えば、Web Audio と Web MIDI を同期させたい、といったユースケースを実現しようとすると必須の内容が解説されています。