ArduinoでDAWからBPMを取得する
はじめに
「ぼくがかんがえたさいきょうのMIDIコントローラー」を作る過程で副産物が少しばかり生まれたので、可能な範囲で書き起こします。諸々の前提知識をすっ飛ばしますのでご了承ください。
ソースコードはGitHubにも載せています。
環境
- Windows 11
- Arduino IDE 2.3.3
- Ableton Live 12 Suite
背景と目的
ArduinoでAbleton LiveからBPMを取得したいです。適当に検索をかけるとArduino側をMIDIクロックジェネレータとして運用する方法が多くヒットしますが、実際の演奏場面を考えるとクロックの主導権はLive側に握らせた方が安心です。
DAWによってはMIDIタイムコードという内部データ的なものを吐き出してくれるんですが、Ableton Liveは公式のヘルプページ曰く
MIDIタイムコード(MTC)の出力: Liveは受信するMIDIタイムコードと同期することができますが、Liveだけでは、MIDIタイムコードを送信することができません。ただし、 MIDIタイムコードを出力するMaxのデバイス を利用することができます(Max Runtimeが必要です)。
とのことなので、タイムコードは諦めて
- Liveから同期クロックを出力
- Arduinoで受信してBPMに変換
の方向性で行くことにしました。
もっともPushシリーズは普通にBPMの読み書きが可能なので、タイムコードの機能自体は備わっていて我々に解放されていないだけなんじゃないかと疑っていますが…
スケッチ
実はMIDIUSBライブラリのexample
ディレクトリに、本記事と全く同じ目的で書かれたMIDIUSB_clock.ino
なるスケッチが存在します。これを下敷きにしつつ、フォーラムなども参考にしました。
ベーシックver
MIDIUSBライブラリ版
#include <MIDIUSB.h>
void getSerialMIDI(int16_t *vals) {
int16_t kago[3] = {-1, -1, -1};
midiEventPacket_t rx; // Open the mailbox. If empty, header=0.
do { // Retrieve received letters and store them in kago.
rx = MidiUSB.read();
if (rx.header != 0) {
kago[0] = rx.byte1; // read command byte
kago[1] = rx.byte2; // read next byte
kago[2] = rx.byte3; // read final byte
}
} while (rx.header != 0); // Repeat until mailbox is empty.
for (uint8_t i=0; i<3; i++) {
vals[i] = kago[i];
}
}
uint16_t BPM = 0; //!< global BPM
uint8_t ppqn = 0; //!< 24 Pulses Per Quarter Note
uint32_t startTime = 0; //!< for Timer
void clock2BPM() {
float preBPM = 0; // temporary BPM as a result of a single calculation
int16_t MIDIvals[3]; // means kago
getSerialMIDI(MIDIvals); // Bring the letters from the mailbox into the living room.
if (MIDIvals[0] == 0xF8) { // System Realtime Message about Clock.
if (ppqn == 0) { // the first Clock
startTime = micros(); // start Timer
}
ppqn++; // count up Clock
if (ppqn > 24) { // 24 Clocks = 1 bar
preBPM = 6.0e+07 / float(micros() - startTime); // stop Timer, calculate BPM
if(20 <= preBPM && preBPM <= 999) { // adopt if reasonable
BPM = round(preBPM);
}
Serial.println(BPM); // or just "Serial.println(preBPM)"
ppqn = 0; // reset Clock
}
}
}
//! @brief setup function
void setup() {
Serial.begin(115200);
}
//! @brief loop function
void loop() {
clock2BPM();
}
コメントアウトを出鱈目に英語で書いてしまったのでアレですが、大体の流れは以下の通りです。
- DAWからは1/24拍の間隔でクロックが送られてくる
- これをカウントしておいて、24クロックになったら1拍として記録
- BPMに換算(カウントはリセット)
特にgetSerialMIDI
関数の0xF8
なる定数、こいつが構造体midiEventPacket_t
の先頭として飛び込んできたら「クロックが来たぞ!」の合図になるようです。
Control Surfaceライブラリ版
ほとんど同じ仕組みで、MIDIUSBライブラリじゃなくControl Surfaceライブラリを使って書くこともできました。Control Surfaceはめちゃくちゃ便利な神ライブラリです。
#include <Control_Surface.h>
USBMIDI_Interface midi_usb;
uint16_t BPM = 0; //!< global BPM
uint8_t ppqn = 0; //!< 24 Pulses Per Quarter Note
uint32_t startTime = 0; //!< for Timer
bool realTimeMessageCallback(RealTimeMessage rt) {
float preBPM = 0; // temporary BPM as a result of a single calculation
if (ppqn == 0) { // the first Clock
startTime = micros(); // start Timer
}
ppqn++; // count up Clock
if (ppqn > 24) { // 24 Clocks = 1 bar
preBPM = 6.0e+07 / float(micros() - startTime); // stop Timer, calculate BPM
if(20 <= preBPM && preBPM <= 999) { // adopt if reasonable
BPM = round(preBPM);
}
Serial.println(BPM); // or just "Serial.println(preBPM)"
ppqn = 0; // reset Clock
}
return true;
}
//! @brief setup function
void setup() {
Control_Surface.begin();
Control_Surface.setMIDIInputCallbacks(nullptr, nullptr, nullptr, realTimeMessageCallback);
}
//! @brief loop function
void loop() {
Control_Surface.loop();
}
こちらのControl Surface版は動作の最中にArduino IDEのシリアルモニタを開閉すると何故か停止しますが、実際の使用状況ではそもそもIDEを開かないのでヨシ!としています。とは言えあまり健全ではないので、ゆるゆると原因調査中です。
結果
Liveの再生ボタンを押してBPM=120のクロックを読み込ませたところ、とりあえずBPMを教えてくれました(利用するライブラリに関わらず同様です)。しかしながら、BPM=120のはずなのに値がかなりブレブレです。このあとBPM=200とかになるともっと酷いことになりました。
誤差低減ver
何とかならないかと思い、四捨五入やらローパスフィルタやら色々と試してみましたが結果はあまり芳しくなかったです(上、スクショ撮り忘れ)。ちなみにLiveよりStudio Oneの方がクロック精度は高いようで(下)、なぜなんだ~と思いつつ手を引くことにしました。
おわりに
あわよくばArduino側で7セグLEDを使ってBPMを逐一表示~とか妄想してたんですが、こうも値がブレると演奏中には逆効果です。電源周りとかも含めてそれなりに色々試したんですが今のところ効果は無いので、何か改善案をお持ちの方がいたら是非お知らせください。今のままでも単純にLEDを点滅させれば視覚的なメトロノームぐらいにはなるかな…
Discussion