ノブ8つのMIDIコントローラーを手軽に作る UNIT 8ANGLE

M5Stack

 「ノブだけのMIDIコントローラーが欲しい」「ノブは8つくらいあるとうれしい」「KORG nanoKONTROLはいいんだけどまだ大きい」「スマホでも使えるワイヤレスなやつがいい」。そんな要望をかなえるアイテムを「M5Stack Japan Creativity Contest 2024」に向けて作成しました。

 SNSを中心に、コンパクトなノブだけのMIDIコンを望む声はときどき聞かれます。私もそんなMIDIコンが欲しかった一人。「自分で作るしかないか」と思い、ポテンショメーターやロータリーエンコーダーを多数購入したものの、ケースを考えたり、はんだづけはめんどくさいなとか思っているうちに数年がたってしまいました。そこに登場したのが、M5Stackの「UNIT 8ANGLE」という製品。これなら工作なしですぐにノブだけのMIDIコンが作れそうです。

 というわけで作ったのが冒頭の写真。「UNIT 8ANGLE」はあくまでも入力用のセンサーユニットなので別途マイコンモジュールなどが必要。今回「UNIT 8ANGLE」と組み合わせて使用したのは、「M5StickC Plus」というバッテリ内蔵の比較的小型のモデル。別途電源を用意する必要がないのがポイントです。バッテリ搭載モデルとしては正方形シェイプのM5Stack COREシリーズもありますが、やはりできるだけ小型な方がいいということで(ちなみに掲載のプログラムはそのままM5Stack FIREで動作することを確認しています)。

 WindowsとAndroidでの接続を確認。iOSやMacでもいけると思います。以下のデモ動画は、最も有名なDAWのひとつ「Cubase」のモバイル版である「Cubasis」(Androidバージョン)での使用例。

 Cubasis単体ではBluetooth MIDIデバイスを認識してくれないので、「MIDI+BTLE」というユーティリティアプリ(Google Playで無料で入手可能)で先にペアリング操作を行います。あとは、CubasisのMIDIラーン機能を使って、UNIT 8ANGLEの操作と、画面上の操作子を紐付けしていけばOK。スライダーでもノブでも自由に割り当てられます。

 デモ動画では1-8のチャンネルフェーダーを割り当てており、UNIT 8ANGLEの右端にある小さなスイッチを切り替えることで、シンセのフィルターを操作できるようにしています。デモ動画では動作状態を確認できるようにミキサー画面とシンセ画面を切り替えていますが、別の画面を表示している時でもノブの操作は音に反映されます。

 ハードウェア的にはM5StickC PlusとUNIT 8ANGLE間はGROVEケーブルで接続、一方を動かすともう片方はぶらぶらついてくる。それがジャマ。ということで両者を固定したほうがベター。

 今回はLEGOブロックを活用。M5StackC Plus2 ウォッチアクセサリキットなどに付属のBrickというアイテムを使用しています。

プログラムの内容

UNIT 8ANGLEには、8つのノブの他に、右端に小さなスイッチ、それぞれのノブの上とスイッチの上にLEDが合計9個装備されています。これらを生かすようプログラムを作成しました。

スイッチでモード変更。スイッチがONでノブはCh1-8にボリューム(CC# 7)を送信(ミキサーのフェーダー操作を装丁)、スイッチOFFでCh1にそれぞれCC# 16、17、18、19、80、81、82、83を送信(シンセのパラメーター変更を装丁)します。

DAW側でパラメーターとMIDIコンの信号の紐付けをすることになるので、特にMIDIコン側にCC#変更の機能は不要と判断しました。

8つのLEDはノブの位置で明るさを変更。モード変更で色が変わります。

一番右のLEDは、Bluetooth接続したら青、未接続時は赤で点灯します。

 プログラムはGitHubにあったArduino用のサンプルをもとに作成しました。

/*
8angle MIDI
*/

// #include "SD.h"  // for SD-Updater ■
#include <Arduino.h>
#include <M5Unified.h>
// #include <M5StackUpdater.h> // https://github.com/tobozo/M5Stack-SD-Updater  ■
#include "M5_ANGLE8.h"   // https://github.com/m5stack/M5Unit-8Angle/tree/main
M5Canvas canvas(&M5.Display);

#include <BLEMIDI_Transport.h>
#include <hardware/BLEMIDI_ESP32_NimBLE.h>
#define DEVICE_NAME "M5angle MIDI"
BLEMIDI_CREATE_INSTANCE(DEVICE_NAME, MIDI);

bool isConnected = false;
bool isChordMode = false;

M5_ANGLE8 angle8;

byte mode = 0;
byte valPo[8]={255,255,255,255,255,255,255,255};
byte valPoPrev[8]={255,255,255,255,255,255,255,255};
byte cc[8][2] = {{1,16},{1,17},{1,18},{1,19},{1,80},{1,81},{1,82},{1,83}};

uint32_t rgb_c = 0;

// バッテリー https://programresource.net/2020/02/24/2960.html
unsigned long currentMillis;
unsigned long lastupdateMillis = -100000;
int lastbattery = -1;
void printBatteryLevel()
{
  if (currentMillis - lastupdateMillis > 10000) 
  {
    uint8_t battlevel = M5.Power.getBatteryLevel();
    if (lastbattery != battlevel)
    {
      lastbattery = battlevel;
      //M5.Display.setTextSize(2);
      M5.Display.setCursor(M5.Display.width() - M5.Display.textWidth("100% "), 8);
      M5.Display.setTextColor(TFT_BLACK, TFT_GREEN);
      if (battlevel < 30)
      {
        M5.Display.setTextColor(TFT_BLACK, TFT_RED);
      }
      M5.Display.printf("%3d%%", battlevel);
    } 
    lastupdateMillis = currentMillis;
  } 
}

void print_adc_val(uint8_t i, byte adc_v) {
    int x = 0; 
    if (i > 3) {x = 80;}
    int y;
    y = i * (M5.Display.fontHeight() * 2.1);// 18
    if (i > 3){
      y = (i - 4) * (M5.Display.fontHeight() * 2.1) ;
    }
    canvas.setCursor(x, y);
    canvas.setTextSize(1);
    canvas.printf("%d: %3d", i + 1, adc_v);
    uint8_t brightness = adc_v * .4;
    uint32_t color = 0x00ffff;  // bluegreen
    if(mode){color = 0xff33cc;} // pink
    RGBLED(i, color, brightness);
}

void SendMIDI(uint8_t ch, byte adc_v) {
  valPo[ch] = adc_v;
  if( (valPo[ch] != valPoPrev[ch]) ) {
    if (valPoPrev[7] != 255) { 
      if(mode) {  //mode 1: cc
        sendCC(cc[ch][0], cc[ch][1], adc_v);
        M5_LOGI("ch %2d: cc# %3d : %3d (%3d) \n",ch + 1,cc[ch][1],  valPo[ch], valPoPrev[ch]); 
      } else {   // mode 0: vol
        sendCC(ch + 1, 7, adc_v);
        M5_LOGI("ch %2d: %3d (%3d) \n",ch + 1, valPo[ch], valPoPrev[ch]); 
      }
    }
  }
  valPoPrev[ch] = valPo[ch];
}

// ADC 12 Bit
void TaskADC12(uint16_t delay_t) {
    canvas.createSprite(M5.Display.width(), 160);
    canvas.fillSprite(0);
    byte adc_v = 0; 
    for (uint8_t i = 0; i < ANGLE8_TOTAL_ADC; i++) {
        adc_v = 127 - (angle8.getAnalogInput(i, _12bit) >> 5);
        print_adc_val(i, adc_v);
        SendMIDI(i, adc_v);
    }
    canvas.pushSprite(0, 20);
    vTaskDelay(delay_t);
}

// Breathing RGBLED


void TaskRGBLED_3(uint32_t color, uint8_t br, uint16_t delay_t) {
    //canvas.createSprite(display.width(), 35);
    canvas.createSprite(M5.Display.width(), 35);
    rgb_c = 0;
    canvas.fillSprite(0);
    for (uint8_t i = 0; i < ANGLE8_TOTAL_LED -1; i++) {
        angle8.setLEDColor(i, color, br);
        vTaskDelay(delay_t);
    }
    canvas.setCursor(0, 0);
    canvas.printf("COLOR: 0x%X", rgb_c);
    //canvas.pushSprite(0, 205);
    canvas.pushSprite(0, M5.Display.height() - 35);
}

void RGBLED(uint8_t num, uint32_t color, uint8_t br) {
    angle8.setLEDColor(num, color, br);
}

// BLE-MIDI
void OnConnected() {
  isConnected = true;
  M5_LOGI("   Connected.");
  RGBLED(8, 0x0000ff, 30); // ●一番右 青
}
void OnDisconnected() {
  isConnected = false;
  M5_LOGI("   Disconnected.");
  RGBLED(8, 0xff0000, 20); // ●一番右 赤
}
void noteOn(uint8_t channel, uint8_t pitch, uint8_t velocity) {
  MIDI.sendNoteOn(pitch, velocity, channel);
}
void noteOff(uint8_t channel, uint8_t pitch, uint8_t velocity) {
  MIDI.sendNoteOff(pitch, velocity, channel);
}
void sendCC(uint8_t channel, uint8_t number, uint8_t value) {
  MIDI.sendControlChange(number, value, channel);
}


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

    // checkSDUpdater( SD, MENU_BIN, 2000  );  // SD-Updater  ■

    if( (M5.getBoard() == m5::board_t::board_M5StickC) || (M5.getBoard() == m5::board_t::board_M5StickCPlus) ){
      M5.Display.setRotation(3);
    }
    M5.Display.clear(TFT_BLACK);
      
    canvas.setColorDepth(1);  // mono color
    canvas.setFont(&fonts::efontCN_14);
    while (!angle8.begin(ANGLE8_I2C_ADDR)) {
        M5_LOGI("angle8 Connect Error\n");
        M5.Display.print("angle8 Connect Error");
        delay(100);
    }
    canvas.createSprite(M5.Display.width(), 20);
    canvas.setPaletteColor(1, YELLOW);
    canvas.fillSprite(0);
    canvas.setTextSize(1);
    canvas.drawString("Angle8 MIDI", 0, 0);
    canvas.pushSprite(0, 0);

    RGBLED(8, 0xff0000, 20); // ●一番右のRGBLEDを光らせる BLE接続の状態表示

    // BLE-MIDI
    MIDI.begin();
    BLEMIDI.setHandleConnected(OnConnected);
    BLEMIDI.setHandleDisconnected(OnDisconnected);
}

void loop() {
    M5.update();
    
    if (angle8.getDigitalInput())     // スイッチ 1(↑)
    {
      if(mode == 0){
        M5_LOGI("change mode 1\n");
      }
      mode = 1;
    }
    else  // スイッチ 0(↓)
    {
      if(mode == 1){
        M5_LOGI("change mode 0\n");
      }
      mode = 0;
    }

    TaskADC12(100);
    currentMillis = millis();
    printBatteryLevel();
}

 使用するライブラリや参考にしたサイトなどをプログラム内にメモしました。

 「■」はSD-Updater用。Lovyan Launcherなどを使用してバイナリを切り替えたい場合は有効にします。

コメント

タイトルとURLをコピーしました