Arduino: MIDI shieldを使って一本のフェーダーで複数のフェーダーを操作する

ご同業の方以外には伝わりづらい内容だとは思いますが一応書き残しておきます。

# 動機
ミュージカルPAをやるのですが、俳優のかかえるワイヤレスマイクの本数が多すぎて手が追いつかないのです。
そこで、一本のフェーダーで複数のフェーダーが同時に操作できればいいのでは?と閃きました。

# こんな風にしたい
(っていうか完成映像)：

<iframe width="560" height="315" src="https://www.youtube.com/embed/NMVCQtG7JHw?si=gZ2gieI5B7Gm3DKd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

+ 特に忙しいシーンをよっつ、空きフェーダーに登録して親フェーダーとし、最大13本のチャンネルから動かしたいやつを選んで子フェーダーにする。
+ onボタンが点灯してるときだけ動作する(誤動作防止のため)。
+ onボタンを点灯したときに親フェーダーの値を一斉に子フェーダーにフラッシュする。
+ 親フェーダーを動かしたとき、子フェーダー同士の相対位置を保ったまま上下する。
+ 親フェーダーを下げきったら、子フェーダーも下げきる。相対位置は一旦リセットされる。

# 使うもの

![IMG20240423183016.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61597/7c9cb551-e0d3-6dbc-9691-85791e7962d3.jpeg)

Arduino Leonardoと Arduino MIDI Shield

Leonardoはその辺にころがってたやつ。MIDI Shieldは、AlliexpressとAmazonで購入しました。

こいつにプログラムを入れて、

+ 音響卓のMIDI OUT->ShieldのMIDI IN、 
+ ShieldのMIDI OUT->音響卓のMIDI IN
 
と結線して使います。あと、USB電源も必要。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61597/9ac96e76-f117-60f8-8c3a-a4add719ebfa.jpeg">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61597/0fda1cee-e7cd-e949-a55b-7a05a8f76fba.jpeg">


# コード

```c++:fader_to_faders_5.ino
#define countof(array) (sizeof(array) / sizeof(array[0]))
#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();
// 親フェーダー：
constexpr byte NUM_INS = 4; // 数
constexpr byte IN_FADER_CC[ NUM_INS ] = {21, 22, 23, 24}; // フェーダーのCCナンバー
constexpr byte IN_ON_CC[ NUM_INS] = {84, 85, 86, 87}; // onのCCナンバー
// 子フェーダー：
constexpr byte OUT_CCS_ALL[] = 
  {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};  // すべての子フェーダーのCCナンバー
constexpr byte OUT_CCS_0[] = {3, 7, 8, 9, 10, 11, 12, 13}; // 以下、同時に上下させたいフェーダーの組み合わせ4種
constexpr byte OUT_CCS_1[] = {3, 7, 8, 9, 10, 11};
constexpr byte OUT_CCS_2[] = {3, 6, 7, 8, 9, 10, 11};
constexpr byte OUT_CCS_3[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

constexpr size_t N_OUT_CCS_ALL = countof(OUT_CCS_ALL);  // すべての子フェーダーの個数 
constexpr size_t N_OUT_CCS_0 = countof(OUT_CCS_0);  // 以下、同時に上下させたいフェーダーの組み合わせ4種の個数
constexpr size_t N_OUT_CCS_1 = countof(OUT_CCS_1);
constexpr size_t N_OUT_CCS_2 = countof(OUT_CCS_2);
constexpr size_t N_OUT_CCS_3 = countof(OUT_CCS_3);

struct CCArray{
  const byte* CCS;
  const size_t N;
  CCArray(const byte* ccs, const size_t n): CCS(ccs), N(n){};
};

CCArray OUT_FADERS[ NUM_INS ] = {
  CCArray(OUT_CCS_0, N_OUT_CCS_0)
  , CCArray(OUT_CCS_1, N_OUT_CCS_1)
  , CCArray(OUT_CCS_2, N_OUT_CCS_2)
  , CCArray(OUT_CCS_3, N_OUT_CCS_3)
  };

class MyProcessing{
  private:
    byte in_on_recent[NUM_INS] = {};  // 0で初期化 
    byte in_fader_recent[NUM_INS] = {};
    byte out_fader_recent[N_OUT_CCS_ALL] = {};

  public:
    void operator()(byte channel, byte num, byte value){

      for(int i = 0; i < N_OUT_CCS_ALL; i++){
        if(num == OUT_CCS_ALL[i]){
          out_fader_recent[i] = value;
          return;
        }
      }
      
      for(int i = 0; i < NUM_INS; i++){
        if(num == IN_ON_CC[i]){
          if( ! in_on_recent[i] ){
            for (int j = 0; j < OUT_FADERS[i].N; ++j) {
              byte out_cc = OUT_FADERS[i].CCS[j];

              byte out_fader_index;
              for(int k = 0; k < N_OUT_CCS_ALL; k++){
                if(OUT_CCS_ALL[k] == out_cc){
                  out_fader_index = k;
                }
              }

              MIDI.send(
                midi::ControlChange
                , out_cc
                , out_fader_recent[out_fader_index] = 
                  in_fader_recent[i]
                , channel
              );
              if(in_fader_recent[i] == 0){
                MIDI.send(
                  midi::ControlChange
                  , out_cc + 32
                  , 0
                  , channel
                );
              }
            }
            in_on_recent[i] = value;
            return;
          }  
          in_on_recent[i] = value;
          return;
        }

        if(num == IN_FADER_CC[i]){ 
          if( in_on_recent[i] ){
            int diff = value - in_fader_recent[i];

            for(int j = 0; j < OUT_FADERS[i].N; ++j) {
              byte out_cc = OUT_FADERS[i].CCS[j];

              byte out_fader_index;
              for(int k = 0; k < N_OUT_CCS_ALL; k++){
                if(OUT_CCS_ALL[k] == out_cc){
                  out_fader_index = k;
                }
              }
              
              if(value == 0){
                MIDI.send(
                  midi::ControlChange
                  , out_cc
                  , out_fader_recent[out_fader_index] = 
                    0
                  , channel
                );
                MIDI.send(
                  midi::ControlChange
                  , out_cc + 32
                  , 0
                  , channel
                );
              }
              else{
                MIDI.send(
                  midi::ControlChange
                  , out_cc
                  , out_fader_recent[out_fader_index] = 
                      constrain(
                        out_fader_recent[out_fader_index] + diff
                        , 0
                        , 127
                        )
                  , channel
                );
              }
            }
            in_fader_recent[i] = value;
            return;
          }
          in_fader_recent[i] = value;
          return;
        }
      }
    }
};

MyProcessing handleControlChange;

void setup() {
  MIDI.setHandleControlChange(
    [&handleControlChange](byte channel, byte num, byte value) {
      handleControlChange(channel, num, value);
    }
  );
 
  MIDI.begin(MIDI_CHANNEL_OMNI);  
  MIDI.turnThruOff(); // デフォルトで thrughする仕様のため、明示的にthrough off する必要がある
}

void loop() {
  MIDI.read();
}
```
あとで解説書くかも。

# 音響卓の設定
扱うコントロールチェンジはコードの中に決め打ちされているので、必要なら卓のMIDI設定を変更します。

+ MIDIコントロールチェンジ送受信を許可する。
+ 親フェーダーはCC21~24、onボタンはCC84~87を使用する。
+ 子フェーダーはCC1~13を使用する。