itsuko
Published © MIT

M5Stack de MIDI Pipe Band

A one-person MIDI pipe band built with M5Stack: touchpads play bagpipe notes, and IMUs trigger snare rolls—no PC required.

BeginnerWork in progress15 hours105
M5Stack de MIDI Pipe Band

Things used in this project

Hardware components

M5Stack ATOMS3 Dev Kit
×1
M5Stack MIDI Synthesizer Unit
×1
M5Stack ATOMIC DIY Proto Kit for ATOM series
×1
Capacitive Touch Sensor Breakout - MPR121
Adafruit Capacitive Touch Sensor Breakout - MPR121
×1

Software apps and online services

PlatformIO IDE
PlatformIO IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

System overview

Code

M5Stack de MIDI Pipe Band for Fun

C/C++
This code was written with the help of ChatGPT.
It’s still a work in progress and I plan to keep improving it.
#include <M5Unified.h>
#include <Wire.h>
#include <Adafruit_MPR121.h>
#include <math.h>

#define MPR121_SDA 2
#define MPR121_SCL 1
#define SYNTH_TX   5
#define MIDI_BAUD  31250

#define PIPE_CHANNEL 1
#define DRUM_CHANNEL 10
#define PIPE_PROGRAM 110  // Bagpipe (1-based)

Adafruit_MPR121 cap = Adafruit_MPR121();
uint16_t last_touched = 0;
uint8_t pipe_notes[9] = {55, 57, 59, 60, 62, 64, 65, 67, 69}; // Low G〜High A

float ax, ay, az;

// ==== Roll state (non-blocking) ====
bool rollActive = false;
unsigned long rollEndAt = 0;
unsigned long nextTickAt = 0;
unsigned long noteOffAt  = 0;
unsigned long lastRollTrig = 0;

// ==== Tunables ====
const int   ROLL_INTERVAL_MS = 55;   // 連打間隔(細かさ)
const int   ROLL_DURATION_MS = 450;  // ロール継続時間
const int   NOTE_LEN_MS      = 18;   // 各打の長さ
int         COOLDOWN_MS      = 60;   // 連発抑制

// ==== jerk(速い変化)検出用 二重EMA(X/Y/Z) ====
float emaX_fast=0, emaX_slow=0, emaY_fast=0, emaY_slow=0, emaZ_fast=0, emaZ_slow=0;
bool  emaInited = false;
const float ALPHA_FAST = 0.30f;  // 速く追従
const float ALPHA_SLOW = 0.02f;  // ゆっくり追従
float JERK_TRIG_MAG = 0.60f;     // 合成jerkのしきい値(0.5〜0.8で調整)

// パターン管理
int patternStep = 0;                // どのステップか
unsigned long patternNextAt = 0;    // 次の動作時間

// 調整パラメータ
const int HIT_VELOCITY = 100;       // スネア強さ
const int HIT_INTERVAL = 120;       // か → か の間隔 (ms)
const int GAP_AFTER_ROLL = 200;     // 1セット終わって次に行く間隔 (ms)

// テンポ設定
int BPM = 120;
int BEAT_MS = 60000 / BPM;

// 1拍休み
const int KAKA_REST_MS = BEAT_MS;   // 4分音符休む

// ===== MIDI helpers =====
void sendNoteOn(uint8_t channel, uint8_t note, uint8_t velocity) {
  Serial1.write(0x90 | ((channel - 1) & 0x0F));
  Serial1.write(note & 0x7F);
  Serial1.write(velocity & 0x7F);
}
void sendNoteOff(uint8_t channel, uint8_t note, uint8_t velocity) {
  Serial1.write(0x80 | ((channel - 1) & 0x0F));
  Serial1.write(note & 0x7F);
  Serial1.write(velocity & 0x7F);
}
void sendProgramChange(uint8_t program, uint8_t channel) {
  Serial1.write(0xC0 | ((channel - 1) & 0x0F));
  Serial1.write(program & 0x7F);
}

void startRoll(unsigned long now) {
  rollActive = true;
  rollEndAt  = now + ROLL_DURATION_MS;
  nextTickAt = now;
  noteOffAt  = 0;
}

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  Serial.begin(115200);

  Wire.begin(MPR121_SDA, MPR121_SCL);
  if (!cap.begin(0x5A)) {
    Serial.println("MPR121 not found.");
    while (1);
  }

  Serial1.begin(MIDI_BAUD, SERIAL_8N1, -1, SYNTH_TX);
  sendProgramChange(PIPE_PROGRAM - 1, PIPE_CHANNEL);

  M5.Imu.begin();

  // EMA初期化
  if (M5.Imu.getAccel(&ax, &ay, &az)) {
    emaX_fast = emaX_slow = ax;
    emaY_fast = emaY_slow = ay;
    emaZ_fast = emaZ_slow = az;
    emaInited = true;
  }
}

void loop() {
  M5.update();

  // --- タッチ(バグパイプ) ---
  uint16_t curr_touched = cap.touched();
  for (uint8_t i = 0; i < 9; i++) {
    if ((curr_touched & (1 << i)) && !(last_touched & (1 << i))) {
      sendNoteOn(PIPE_CHANNEL, pipe_notes[i], 100);
    } else if (!(curr_touched & (1 << i)) && (last_touched & (1 << i))) {
      sendNoteOff(PIPE_CHANNEL, pipe_notes[i], 100);
    }
  }
  last_touched = curr_touched;

  // --- IMU & ロール判定 ---
  if (M5.Imu.getAccel(&ax, &ay, &az)) {
    unsigned long now = millis();

    if (!emaInited) {
      emaX_fast = emaX_slow = ax;
      emaY_fast = emaY_slow = ay;
      emaZ_fast = emaZ_slow = az;
      emaInited = true;
    }

    // 二重EMAで高域強調(jerk)
    emaX_fast += ALPHA_FAST * (ax - emaX_fast);
    emaX_slow += ALPHA_SLOW * (ax - emaX_slow);
    emaY_fast += ALPHA_FAST * (ay - emaY_fast);
    emaY_slow += ALPHA_SLOW * (ay - emaY_slow);
    emaZ_fast += ALPHA_FAST * (az - emaZ_fast);
    emaZ_slow += ALPHA_SLOW * (az - emaZ_slow);

    float jerkX = emaX_fast - emaX_slow;
    float jerkY = emaY_fast - emaY_slow;
    float jerkZ = emaZ_fast - emaZ_slow;
    float jerkMag = sqrtf(jerkX*jerkX + jerkY*jerkY + jerkZ*jerkZ);

    // 発火トリガ
    if (!rollActive && patternStep == 0 &&
        (now - lastRollTrig) > COOLDOWN_MS &&
        (jerkMag > JERK_TRIG_MAG)) {

      // まず 1発目「か」
      Serial1.write(0x99); Serial1.write(38); Serial1.write(HIT_VELOCITY);
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);

      patternStep   = 1;
      patternNextAt = now + HIT_INTERVAL;  // 次の「か」の時間
      lastRollTrig  = now;
    }
  }
  
  // === パターン進行 ===
  if (patternStep > 0 && millis() >= patternNextAt) {
    unsigned long now = millis();

    if (patternStep == 1) {
      // 1回目:1発目「か」
      Serial1.write(0x99); Serial1.write(38); Serial1.write(HIT_VELOCITY);
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);

      // 次は休符
      patternStep   = 2;
      patternNextAt = now + KAKA_REST_MS;

    } else if (patternStep == 2) {
      // 休符明け → 2発目「か」
      Serial1.write(0x99); Serial1.write(38); Serial1.write(HIT_VELOCITY);
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);

      // 次は休符
      patternStep   = 3;
      patternNextAt = now + KAKA_REST_MS;

    } else if (patternStep == 3) {
      // 休符明け → ロール開始
      startRoll(now);
      patternStep = 4;

    } else if (patternStep == 4) {
      // 1回目ロール終了待ち
      if (!rollActive) {
        patternStep   = 5;
        patternNextAt = now + GAP_AFTER_ROLL; // セット間休み
      }

    } else if (patternStep == 5) {
      // 2回目:1発目「か」
      Serial1.write(0x99); Serial1.write(38); Serial1.write(HIT_VELOCITY);
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);

      patternStep   = 6;
      patternNextAt = now + KAKA_REST_MS;

    } else if (patternStep == 6) {
      // 休符明け → 2発目「か」
      Serial1.write(0x99); Serial1.write(38); Serial1.write(HIT_VELOCITY);
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);

      patternStep   = 7;
      patternNextAt = now + KAKA_REST_MS;

    } else if (patternStep == 7) {
      // 休符明け → 2回目ロール開始
      startRoll(now);
      patternStep = 8;

    } else if (patternStep == 8) {
      // 2回目ロール終了待ち
      if (!rollActive) {
        patternStep = 0; // 完了
      }
    }
  }

  // --- ロール実行(非ブロッキング) ---
  if (rollActive) {
    unsigned long now = millis();

    // 前回のNote OFF
    if (noteOffAt && now >= noteOffAt) {
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);
      noteOffAt = 0;
    }

    // 次のNote ON
    if (now >= nextTickAt) {
      Serial1.write(0x99); Serial1.write(38); Serial1.write(100);
      noteOffAt  = now + NOTE_LEN_MS;
      nextTickAt = now + ROLL_INTERVAL_MS;
    }

    // 終了判定
    if (now >= rollEndAt) {
      rollActive = false;
      if (noteOffAt) {
        Serial1.write(0x89); Serial1.write(38); Serial1.write(0);
        noteOffAt = 0;
      }
    }
  }

  // --- ウォッチドッグ(万一の詰み防止) ---
  static unsigned long rollWatch = 0;
  if (rollActive) {
    if (!rollWatch) rollWatch = millis();
    if (millis() - rollWatch > 1000) {
      rollActive = false; noteOffAt = 0;
      Serial1.write(0x89); Serial1.write(38); Serial1.write(0);
      rollWatch = 0;
    }
  } else {
    rollWatch = 0;
  }
}

Credits

itsuko
1 project • 0 followers
PR rep at an online store for electronics modules. I'm not an engineer, but working with makers made me want to try making things myself.

Comments