ストリームからBPMを計算する。RxJSを使って

ボタンを叩くタイミングからBPMを検出して設定してくれる、いわゆるタップテンポ機能。 シーケンサーサンプラーなどに搭載されていて、最近はブラウザ上で確認できるサービスもいくつかある。

Reactive Programming

そしてこの記事。

【翻訳】あなたが求めていたリアクティブプログラミング入門 - ninjinkun's diary

昔これ読んだときに適当なイベントストリームからBPM計算することもできそうと思ったので、勉強ついでに似たようなものを作ってみた。

Tap Tempo Machine - Simple BPM calculator

Gyazo

Jキーを押す*1か灰色部分をタイミングよくクリックしてるとBPMが表示される。直近4回のBPMの平均を表示する。それだけのページ。

BPM検出部分にRxJSを使っている。*2

GitHub - Reactive-Extensions/RxJS: The Reactive Extensions for JavaScript

クリックイベントからBPMを出力する

クリックイベントをobservableに変換してBPM出力まで持ってくあたりの部分を書いてみる。BPMの計算方法は一応書いておくと、

BPM = Beats Per Minute = Beats/Minute = Beats/60s

つまり60秒間に打つBeatの数がBPMということなので、60秒を各Beat間の秒間隔で割るとBPMが出てくるということになる。 実際のコードからちょっと改変してるけど雰囲気こんな感じになる。

// クリックイベントをobservableのシーケンスに変換
var source = Rx.Observable.fromEvent(window, 'click')
    
    // observable間の時間間隔の情報を持つobservableのシーケンスを返す
    .timeInterval()

    // 1つめのobservableの時間間隔は0になるので無視する。
    .skip(1)

    // timeInterval()で返ってきたそれぞれのobservableがもつintervalというプロパティだけを
    // 新しいobservableのシーケンスとして返す。
    .pluck('interval')

    // 前回のイベント発生から10秒経っていたら次のobservableは無視する。
    .where(function (interval) {
      return interval <= 10000;
    })

    // 4つのobservableをひとまとまりのobservableとして返す。
    // 1要素単位でずらして格納していく。
    .windowWithCount(4, 1)
    
    // 入れ子になったobservableのシーケンスを平準化する。
    // 平準化するときに各observableの値4つ(ここではintervalの値)の平均をとる。
    .selectMany(function (elements) {
      return elements.average();
    })

    // 平均されたintervalをBPMに変換して小数点以下を四捨五入する。
    .map(function (interval) {
      return Math.round(60000 / interval);
    });

// クリック毎に直近4回の平均BPMが出力される。
source.subscribe(function (bpm) {
    console.log(bpm);
});

多分この中でイメージしにくいのはwindowWithCountselectManyだと思う。

windowWithCount(4, 1)は、例えば

1,2,3,4,5,6,...

このようなシーケンスの場合、

[1,2,3,4],[2,3,4,5],[3,4,5,6]...

となって返ってくる。[ ]がひとつのobservable。[ ]に含まれている各要素もobservable。observableが入れ子になって返ってくる。

第二引数を変えると、例えばwindowWithCount(4, 4)の場合、

[1,2,3,4],[5,6,7,8],[9,10,11,12]...

となる。4つずつskipして格納される。ドキュメント見るといいです。

RxJS/windowwithcount.md at master · Reactive-Extensions/RxJS · GitHub

selectManywindowWithCountによって入れ子のobservableになってしまったシーケンスを平らなobservableのシーケンスに戻す。

[1,2,3,4],[2,3,4,5],[3,4,5,6]...

これが

1,2,3,4,2,3,4,5,3,4,5,6...

となって返ってくる。*3 BPMの例の場合、平らなシーケンスに戻す前にaverage()によって子observable各要素の平均をとっている。例えば

[120,122,122,120],[124,122,123,123]...

こんな入力の場合

121,123...

このように返ってくる。

RxJS/selectmany.md at master · Reactive-Extensions/RxJS · GitHub

おわり

普段C#Linq書いてるからRxのメソッドチェーン書いててとても気持ち良い。嬉しい。ありがとう、ありがとう。

GitHub - distkloc/tap-tempo-machine: Simple BPM calculator

Tap Tempo Machine - Simple BPM calculator

スマホでも一応動くけど、ngTouchのngClick使ってるにもかかわらず反応が鈍い。CSSアニメーション切っても改善しなかったのでJS見なおす必要ありそう。

*1:JキーにしたのはLDRとかTumblrとかVimキーバインドで一番良く押しそうなボタンとかいうどうでもいい理由から。

*2:その他はAngularJS

*3:実際はこの順序にならないかもしれない。javascript - RxJS の `flatMap` の挙動が直感と異なる - スタック・オーバーフロー