Skip to content

MIDI | Arduino

ArduinoでDAWからBPMを取得する

はじめに

「ぼくがかんがえたさいきょうの MIDIコントローラー」作る過程で副産物が少しばかり生まれたので、可能な範囲で書き起こします。諸々の前提知識をすっ飛ばしますのでご了承ください。ソースコードはGitHubにも載せています。

https://github.com/aSumo-1xts/MIDI-HARD/tree/main/Arduino/Clock-and-BPM

環境

  • 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が必要です)。

とのことなので、タイムコードは諦めて

  1. Liveから同期クロックを出力
  2. Arduinoで受信してBPMに変換

の方向性で行くことにしました。

もっともPushシリーズは普通にBPMの読み書きが可能なので、タイムコードの機能自体は備わっていて我々に解放されていないだけなんじゃないかと疑っていますが…

スケッチ

実はMIDIUSBライブラリexampleディレクトリに、本記事と全く同じ目的で書かれたMIDIUSB_clock.inoなるスケッチが存在します。これを下敷きにしつつ、フォーラムなども参考にしました。

ベーシック ver

コメントアウトを出鱈目に英語で書いてしまったのでアレですが、大体の流れは以下の通りです。

  1. DAWからは 1/24拍の間隔でクロックが送られてくる
  2. これをカウントしておいて、24クロックになったら1拍として記録
  3. BPMに換算(カウントはリセット)

特にgetSerialMIDI関数の0xF8なる定数、こいつが構造体midiEventPacket_tの先頭として飛び込んできたら「クロックが来たぞ!」の合図になるようです。

ほとんど同じ仕組みで、MIDIUSBライブラリじゃなくControl Surfaceライブラリを使って書くこともできました。Control Surfaceはめちゃくちゃ便利な神ライブラリです。

cpp
#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(); }
cpp
#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

誤差低減 ver

何とかならないかと思い、四捨五入やらローパスフィルタやら色々と試してみましたが結果はあまり芳しくなかったです(上、スクショ撮り忘れ)。ちなみにLiveよりStudio Oneの方がクロック精度は高いようで(下)、なぜなんだ~と思いつつ手を引くことにしました。

誤差低減ver

誤差低減ver(Studio One)

おわりに

あわよくばArduino側で7セグLEDを使ってBPMを逐一表示~とか妄想してたんですが、こうも値がブレると演奏中には逆効果です。電源周りとかも含めてそれなりに色々試したんですが今のところ効果は無いので、何か改善案をお持ちの方がいたら是非お知らせください。今のままでも単純に LEDを点滅させれば視覚的なメトロノームぐらいにはなるかな…



Some rights reserved.