West Gate Laboratory

人生を少しでも,面白く便利にするモノづくり

マイコンで電子ピアノのMIDI信号を読み取る

概要

先日Maker Faire Tokyo 2020(MFT2020)に先日記事にしたピアノ演奏可視化装置で応募したら、この度めでたく出展できることとなった。初出展なのでとても楽しみだ。新型コロナが収束していることを願うばかりである。

せっかくなので、MFT2020までこのピアノ演奏可視化装置の技術的な解説記事を細々と書いていこうと思う。 ピアノ演奏可視化装置の概要は以下の記事参照。

westgate-lab.hatenablog.com

今回は、マイコンで電子ピアノから出力されるMIDI信号を読み取る話だ。
マイコンにはESP32を使っているが、基本的にはどのようなマイコンでもやり方は同じである。

MIDIとは

MIDI(ミディ、Musical Instrument Digital Interface)は、電子楽器の演奏データを機器間で転送・共有するための共通規格である。(Wikipediaより)

古くからある規格で、現行の電子ピアノの大体はMIDI1.0に従った信号を出していると思われる。私の所有するRolandのFP-7もMIDI1.0の信号を出力する。
MIDI1.0の規格書はMIDI企画委員会のウェブサイトからダウンロード可能だ。
(余談だが、今年2月にMIDI2.0が策定された。今後電子ピアノのMIDIはMIDI2.0に準拠してくのかもしれない。なお、MIDI2.0は後方互換性があるとのこと)

MIDI自体は幅広い使い方のできるプロトコルのため、全てを解説することはできないが、電子ピアノの演奏に限って言うと、信号自体は非常に単純である。

ピアノには通常白鍵・黒鍵合わせて88鍵ある。
MIDI1.0では、それぞれに鍵盤に対し21~108までのノート番号が振られている。また、弾いたときの強さとしてベロシティというパラメータがある。

f:id:kaname_m:20200711154534p:plain
ノート番号(MIDI1.0規格書より)

f:id:kaname_m:20200711154635p:plain
ベロシティ(音の強弱、MIDI規格書1.0より)

MIDI出力では、ピアノを弾くと弾いたタイミングで都度「どの鍵盤を弾いたか(ノート番号)」「どれくらいの強さで弾いたか(ベロシティ)」の信号が出力される。 しかも、その信号はUARTで読むことができるため、マイコンで簡単に入力・処理することが可能だ。

なお、演奏情報が「都度」出力されるため、MIDIでは厳密には同時に鍵盤が押されたことを表現できない。だが、人間は厳密に和音を同時に弾くことは難しいし、MIDI信号自体は人間の知覚に対して十分高速なので、和音のズレを意識することはまずない。

MIDIを読み取るハードウェア

MIDI信号は、基本的に31.25kbpsの非同期式シリアル通信だ。マイコンのUARTで簡単に読み取ることができる。
ただし、マイコン側とピアノ側は電気的な絶縁が必要なため、オプトアイソレータ(フォトカプラ)で絶縁する。
以下が、MIDI標準ハードウェア回路図だ。MIDIにはOUT, IN, THRUがあるが、今回のケースではピアノの演奏をマイコンに入力することになるため、MIDI INの部分を見れば良い。

f:id:kaname_m:20200502143703p:plain
MIDI標準ハードウェア回路図(MIDI1.0規格書より)

図の通り、フォトカプラで電気的に絶縁されていることがわかる。回路自体は非常に単純なので、このとおり組めばMIDI信号は読めるのだが、ひとつ気をつける点はフォトカプラの周波数特性だ。MIDI信号の31.25kbpsは一般的なフォトカプラでは応答が追いつかず、正しく信号が読み取れない場合がある。
そのため、高速フォトカプラを使う必要がある。ピアノ演奏可視化装置では、MIDI規格書中に記載のあったTLP513を使った。(規格書が古いため、記載されているフォトカプラはほとんど廃盤になっている。TLP513も在庫限りということだ)

www.sengoku.co.jp

これをESP32のUART2に接続する(RXD2はIO16)。

f:id:kaname_m:20200711150651p:plain
MIDI入力回路(ESP32)

ちなみに、ESP32につながず、直接Teraterm等で信号を読み取ると、例えば以下のようなデータが入力される。これは、ピアノのドミソを順番に押して、全部押したらぱっと離したときのデータだ。

[2020-02-01 12:09:01.899] F8
[2020-02-01 12:09:01.921] F8
[2020-02-01 12:09:01.944] F8
[2020-02-01 12:09:01.964] F8
[2020-02-01 12:09:01.992] F8
[2020-02-01 12:09:01.992] 90   // ノートオン、Ch1
[2020-02-01 12:09:01.992] 3C   // ド(0x3C)を押した
[2020-02-01 12:09:01.992] 32   // 強さ0x32
[2020-02-01 12:09:02.012] F8
[2020-02-01 12:09:02.037] F8
[2020-02-01 12:09:02.060] F8
[2020-02-01 12:09:02.085] F8
[2020-02-01 12:09:02.105] F8
[2020-02-01 12:09:02.129] F8
[2020-02-01 12:09:02.139] FE
[2020-02-01 12:09:02.151] F8
[2020-02-01 12:09:02.173] F8
[2020-02-01 12:09:02.196] F8
[2020-02-01 12:09:02.211] 90    // ノートオン、Ch1
[2020-02-01 12:09:02.211] 40    // ミを押した
[2020-02-01 12:09:02.211] 3B    // 強さ0x3B
[2020-02-01 12:09:02.219] F8
[2020-02-01 12:09:02.242] F8
[2020-02-01 12:09:02.265] F8
[2020-02-01 12:09:02.290] F8
[2020-02-01 12:09:02.314] F8
[2020-02-01 12:09:02.338] F8
[2020-02-01 12:09:02.361] F8
[2020-02-01 12:09:02.381] F8
[2020-02-01 12:09:02.387] FE
[2020-02-01 12:09:02.403] F8
[2020-02-01 12:09:02.426] F8
[2020-02-01 12:09:02.431] 90   // ノートオン、Ch1
[2020-02-01 12:09:02.431] 43   // ソを押した
[2020-02-01 12:09:02.431] 4B // 強さ0x4B
[2020-02-01 12:09:02.449] F8
[2020-02-01 12:09:02.473] F8
[2020-02-01 12:09:02.499] F8
[2020-02-01 12:09:02.526] F8
[2020-02-01 12:09:02.546] F8
[2020-02-01 12:09:02.569] F8
[2020-02-01 12:09:02.591] F8
[2020-02-01 12:09:02.615] F8
[2020-02-01 12:09:02.641] F8
[2020-02-01 12:09:02.641] FE
[2020-02-01 12:09:02.662] F8
[2020-02-01 12:09:02.684] F8
[2020-02-01 12:09:02.702] 80   // ノートオフ、Ch1
[2020-02-01 12:09:02.702] 43 // ソを離した
[2020-02-01 12:09:02.702] 7C   // 強さ(無効値) 
[2020-02-01 12:09:02.709] F8
[2020-02-01 12:09:02.716] 80   // ノートオフ、Ch1
[2020-02-01 12:09:02.716] 3C // ドを離した
[2020-02-01 12:09:02.716] 6D // 強さ(無効値)
[2020-02-01 12:09:02.721] 80   // ノートオフ、Ch1
[2020-02-01 12:09:02.721] 40 // ミを離した
[2020-02-01 12:09:02.721] 6B   // 強さ(無効値)
[2020-02-01 12:09:02.728] F8
[2020-02-01 12:09:02.751] F8
[2020-02-01 12:09:02.774] F8
[2020-02-01 12:09:02.801] F8

このように、演奏時にはノートオンイベント(押した)ノートオフイベント(離した)が発生する。
また、それらのイベントには2つのパラメータ(ノート番号、ベロシティ)が付随することがわかる。 なお、ノートオフ時のベロシティは無効値だ。ノートオフはベロシティ0のノートオンで出力される場合がある。
0xF8は同期用の信号なので、今回は無視する。4分音符あたり24の割当で出力される。
0xFEはハートビート的な役割。定期的に出力され、これが来ないと断線したなどと判断するのに使う。今回は無視。

MIDIを読み取るソフトウェア

上述の通り、MIDI信号は31.25kbpsのUARTとして読み取れる。
今回ESP32を用いているが、UART0は基本的にデバッグポートとして使っているので、MIDIにはUART2を使う。ArduinoのExampleに”MultiSerial”なるものがあるので、それを参考に使う。以下はMultiSerialの簡単な例。これは9600bpsだが、31250bpsに設定すればMIDI信号を入出力できる。

void setup() {
  // initialize both serial ports:
  Serial.begin(9600);
  Serial2.begin(9600);
}

void loop() {
  
  // read from port 0, send to port 0:
  if (Serial.available()) {
    int inByte = Serial.read();
    Serial.write(inByte);
  }
    
  // read from port 2, send to port 0:
  if (Serial2.available()) {
    int inByte = Serial2.read();
    Serial.write(inByte);
  }
}

UART2はESP32ではIO16(RXD)、IO17(TXD)を使う。MIDI OUTはしないので、RXD(IO16)のみ使う。

また、Arduino環境が使えるマイコンであれば、MIDI信号処理にはArduino MIDI Libraryが便利だ。

github.com

今見ると、結構頻繁にアップデートされているようである。私はv4.3.1を使った。インストールする際はzipでダウンロードし、Arduino IDEからライブラリをzip形式でインストールする。
Arduino MIDI Libraryの使い方については以下の記事が非常に詳しい。

qiita.com

ベーシックな使い方としては、MIDI.read()でイベントを検出、ノートオン・ノートオフに対応する処理を行う、といった使い方だ。以下の例ではマルチタスクを使ってMIDI処理タスクを生成している。

MIDIイベントを受信した場合は、MIDI.getData1()でノート番号、MIDI.getData2()でベロシティを取得できる。また、MIDI ChはMIDI.getChannel()だ。Roland FP-7の場合は自分で弾くとCh.1で、プリセット曲をMIDI OUTに出力するとCh.4に割り当てられていたりする。

ピアノ演奏可視化装置では、特にベロシティは使っておらず、MIDI.getData1()で得られるノート番号のみを可視化に用いている。

#include <MIDI.h>

#define TASKPRI_MIDI 0
#define TASKCORE_MIDI 0

MIDI_CREATE_INSTANCE(HardwareSerial, Serial2, MIDI);
TaskHandle_t h_MidiTask;

void prvMidiTask(void *pvParameters) {
    while (1) {
        if (MIDI.read()) {
            switch (MIDI.getType()) {
                case midi::NoteOn:    // 鍵盤を押した
                    disp.noteOnScore(MIDI.getData1(), MIDI.getData2(),
                                     MIDI.getChannel());     // 鍵盤を押した場合の処理
                    break;

                case midi::NoteOff:    // 鍵盤を離した
                    disp.noteOff(MIDI.getData1(), MIDI.getData2(),
                                 MIDI.getChannel());     // 鍵盤を離した場合の処理
                    break;

                case midi::ControlChange:  // ペダルを踏むとControlChangeイベントが発生するが、今回は使わない
                    break;

                default:
                    break;
            }
        } else {
            delay(1);
        }
    }
}

void setup() {
    Serial.begin(115200);
    MIDI.begin(MIDI_CHANNEL_OMNI);
    xTaskCreatePinnedToCore(prvMidiTask, "MidiTask", 4096, NULL, TASKPRI_MIDI,
                            &h_MidiTask, TASKCORE_MIDI);
}

void loop() {
    delay(1);
}

まとめ

マイコンMIDI信号を読み取る方法について述べた。
高速フォトカプラを入手することろだけがやや特殊だが、それ以外は通常のUART通信である。MIDIを読み取りさえしてしまえば、あとはソフトウェアでどうとでも料理できてしまう。

次はピアノ演奏可視化装置+NeoPixelあたりの話でも。