miso_soup3 Blog

主に ASP.NET 関連について書いています。

ポリリズム・メトロノームを作った話

ポリリズムを練習するためのメトロノームを、Web Audio API を使って作成しました。

🌎 Website にアクセスして使えます

Movie:

www.youtube.com

GitHub:

github.com

ポリリズムとは?

ポリリズム(英: polyrhythm)は、リズムの異なる声部が同時に奏されること。 ポリリズム - Wikipedia

ピアノでいえば、ショパンの幻想即興曲(左手が3連符、右手が4連符。3拍4連とも言います。)、ドビュッシーアラベスク第1番(左手が2連符、右が3連符)のように、異なる2つのリズムが同時に奏されることを言います。

なぜ作ったか

  • ピアノでポリリズムを練習するときに、専用のメトロノームが欲しいと思っていた
    • 視覚的にリズムを確認したい
    • ゆっくり練習したい
  • Web 上でメトロノームを実装するときはどう実装するのか気になっていた
  • 短期間で軽いものを何か作りたかった
視覚的にリズムを確認したい

アプリストアで「PolyRhythm Metronome」と検索すれば、ポリリズム用のメトロノームがたくさんでてきますが、妙に価格が高かったり変なサブスクリプション課金だったり使いにくかったりで不満がありました。一番欲しかったのは、下のような視覚的な情報です。

f:id:miso_soup3:20211214152830p:plain

3拍4連の場合、リズムは以下のように分解できます。

f:id:miso_soup3:20211214153753p:plain

自分の場合は、必ずこの分解を意識して練習していますが、毎回思い出して楽譜に書いていたので、このような視覚的な情報が常にあるメトロノームが欲しいと思っていました。ちなみに3、4連符あたりは分解できるのですが、5、7連符に関しては分解せず、だいたいこのタイミングくらいかな、と大雑把に認識しています。そのときも、だいたいどのタイミングに位置するのか、視覚的に把握するために、楽譜へ図の記入が必要でした。

ゆっくり練習したい

ゆっくり練習するときも、リズムを正確に当てたいときがあります。ゆっくりから徐々に理想の速さに移行していくのですが、その途中の段階でも、正しいリズムを把握できるメトロノームが欲しいと思っていました。

Web でのメトロノームの実装について

メトロノームは、一定の時間毎に音を鳴らせば実現できます。

Web でメトロノームを実装する方法は、だいたい3つの方法があり、理想の方法については、2013年頃からすでにロジックが固まっています。その3つの方法は以下の通りで、3番目が一番良いだろうとされている方法です。

  • JavaScriptsetInterval() を、メインスレッドで定期実行する
  • JavaScriptsetInterval() を、Worker スレッドで定期実行する
  • Web Audio API を使って、Audio events をスケジューリングする

これらについては、以下の記事にて説明されています

今回作成したメトロのノームは、3つめの方法である Web Audio API を使っています。

Web Audio API を使ってメトロノームを実装する

Web Audio API では、AudioContext のオブジェクトが currentTime というプロパティを持っていて、それが経過時間を表しています。その時間軸を基準に、いつ音を鳴らすかをスケジューリングしていきます。

例えば、1秒ごとに音を鳴らしたい場合は、

osc.start(0); // 0 秒のときに音を鳴らす
osc.start(1); // 1 秒のときに音を鳴らす
osc.start(2); // 2 秒のときに音を鳴らす
osc.start(3); // 3 秒のときに音を鳴らす
...

と、鳴らす時間を予め指定して命令します。

メトロノームの場合、これを一定の時間分スケジューリングしておけば実現できますが、ここで、じゃあいつまでの時間分をスケジューリングすればよいのか?という問題が発生します。永遠に…と思ってしまいますがそれは無限ループになります。

そこで、定期的にこの時間分だけをスケジューリングする、というのを、setInterval()を使い、X秒後を先読みしてその間にどのタイミングで音を鳴らせばいいかを計算しスケジューリングする、そしてまた setInterval() によって処理が呼び出されるときに、またX秒後を先読みしてスケジューリングする、という実装を行います。Web Audio API を使った方法においても、setInterval() も使うことになります。

この実装方法と、最適なタイミングについての説明が、先ほど紹介した3番目の記事にあります。この記事の図をざっとみたとき、”複雑そう…”と及び腰になりましたが、実際にコードを書いてみると、大したことはありません。

先読みする時間(たとえば10秒になるとき)を、 scheduleAheadTime で定義しておき、次に音を鳴らす時間がその10秒を越えるまで、スケジューリングを繰り返す、という処理を記述します。

private _scheduleTicks(): void {
    const scheduleAheadTime = this._audioContext.currentTime + this._scheduleAheadTimeSec;

    while (this._nextTickTimeToSchedule < scheduleAheadTime) {
        Metronome._scheduleTick(
            this._audioContext,
            this._nextTickTimeToSchedule,
            this._frequency);

        this.moveNextTickTimeToSchedule();
    }
}

…とこのような実装についても、すべて紹介した記事で説明されています。

テンポを変えるとき、あらかじめスケジューリングしたものはキャンセルしたいですが、良い実装方法が見つからず、手っ取り早いAudioContext.close() による破棄を行いました。これが良い実装であるかどうかは不明です。

UI への反映

音のスケジューリングの後、そのタイミングをどのように UI に反映させるか?については、AudioContext.currentTime から時間を参照し、その時間のための描画を計算すれば実現できます。今回は p5.js を使いました。

🛠 Technologies used

TypeScript で簡単に書きたかったので、no web framework / no webpack / no babel / no RxJS な構成にしました。

その他

  • 今後、練習するときに使いそう?
    • どうでしょう…少し使ってみましたが、すごくゆっくりな速度でも、ズレてないことが分かって安心して練習できました。意外と、目での情報も頼りになると思いました。
    • 早いテンポだと、合っているかどうか分からなくなるので向いてないかなと思います。
  • 感想
    • 何も考えずに信頼できる時間軸(AudioContext.currentTime)があると、助かるなと感じました。