ムライボックス (12) --- ソフトウェア (4)
ソフトウェアの話の続きです。
- MIDI IN から入力される MIDI ストリームを UART RX から 1 バイト読む
- MIDI チャネル番号書き換え、ビットマスク・パターン読み出しなどの処理
- マスク・パターンの出力、および MIDI バイトを UART TX に出力
という処理の流れでは、UART RX から 1 バイト読み、UART TX に 1 バイト出力という繰り返しなので、単位時間当たりの出力量は入力量と同じで、増えも減りもしません。
ただし、UART の非同期式 (asynchronous) あるいは調歩同期式 (start-stop synchronous) と呼ばれる通信方式では、送信側と受信側で独立なタイミングの基準を使用しているので、それらの間の相対的な周波数誤差により問題が発生する可能性があります。
たとえば、受信データのレートより遅いレートで連続的な送信を続けると、長い期間では、送信が終了するまえに次の受信データが到着し、送信が間に合わなくてデータが欠けてしまう可能性もあります。
非同期通信で誤りなく 1 キャラクタのデータを受け渡しできるクロックの相対誤差の許容範囲は、理想的な場合で ±5 %、16 倍ボーレート・クロックを使用する場合で ±4.375 % となります。
仮に、送信側のレートが相対的に 4 % 遅いとすると、26 キャラクタを連続受信する間に 25 キャラクタ分の連続送信しかできず、バッファを設けないとキャラクタの欠損が生じることになります。
ムライボックスの応用では、UART TX の送信完了をモニタする必要があり、そこから送信キャラクタを書き込んだ場合に送信開始が 1 ビット・タイム遅れるタイプの UART では、元のクロックに相対誤差がないとしても相対レートが -10 % ということになり、11 キャラクタを連続受信する間に 10 キャラクタしか送信できないことになります。
MIDI ストリームとしては、一般の演奏データでは連続的に多量のデータが流れてくることは少なく、「音符」のタイミングごとに間けつ的にデータが流れてくることが普通です。
データ間に「アイドル」状態が挟まれば、レートの相対誤差がリセットされ、後に響かない形となります。
一方、「システム・エクスクルーシブ・メッセージ」では比較的多量のデータが連続して出力される可能性があります。
SMF (Standard MIDI File) をシーケンサで再生する場合を想定すると、システム・エクスクルーシブ・イベントでは、なるべくそのティック内で送信を終えるためにデータは連続して送信されるものと考えられます。 データ間にアイドル状態を挿入して「バラけ」させて送信するとは考えられません。
システム・エクスクルーシブの長さに規格上の制限はありませんが、演奏データの中で良く使われるなかで長いものといったら、
- XG 「ディスプレイ・ビットマップ・データ」の 56 バイト
- GS 「Displayed Dot Data」の 74 バイト
があります。
したがって、この程度の長さの連続メッセージで破綻しない程度のバッファを設ける必要があります。
プログラムの説明は後に回して、Arduino でムライボックスを実現するスケッチを下に示します。
2017 年 12 月 8 日付け記事 (→こちら) の「ムライシールド」を併用します。
(2017 年 12 月 28 日追記: システム・エクスクルーシブによるテーブル・ローディング機能をサポートしました)
(2017 年 12 月 21 日追記: ディジタル 13 番ピンの LED を MIDI アクティビティ・インジケータとして使うように変更を加えました)
// // MuraiBOX_ardu.ino : Murai-BOX with 7 output port // using Arduino (ATmega core) // external OR/NOR gate needed // // 2017/12/23 added table loading by System Exclusive // 2017/12/21 using D13 LED as MIDI activity indicator // 2017/11/10 created by pcm1723 // #include <avr/sleep.h> // uncomment following line to inhibit ch. remapping //#define REMAP_INH // shift right macro (negative shift count means left shift) #define SHIFT_R(x,n) ((0 <= (n)) ? (x >> (n)) : (x << -(n))) // macro for "OR" mask output port polarity #define MASK_POL(x) (~(x)) // for '1' output = mask, '0' output = enable //#define MASK_POL(x) (x) // for '0' output = mask, '1' output = enable #define N_PORTS (7) // Number of MIDI-OUT ports #define LED_pin (13) // LED // "OR" mask bit output port part-1 (4 bit) #define MASK1_PORT PORTB #define MASK1_DDR DDRB #define MASK1_SHIFT (-1) // left shift #define MASK1_MASK (0x1e) // PORTB[4:1] // "OR" mask bit output port part-2 (3 bit) #define MASK2_PORT PORTD #define MASK2_DDR DDRD #define MASK2_SHIFT (-1) // left shift #define MASK2_MASK (0xe0) // PORTD[7:5] // "running" port mask pattern volatile uint16_t r_mask = 0x0000; // // MIDI channel remapping table for channel message // uint16_t remap_tab[16] = { 0, // ch 1 (1 origin) 0, // ch 2 0, // ch 3 0, // ch 4 0, // ch 5 0, // ch 6 0, // ch 7 7, // ch 8, no remap 8, // ch 9, no remap 9, // ch 10 (drum channel), no remap 10, // ch 11, no remap 11, // ch 12, no remap 12, // ch 13, no remap 13, // ch 14, no remap 14, // ch 15, no remap 15 // ch 16, no remap }; // uint16_t remap_tab[] // // ch. no. to port mask pattern table // port bitmap assignment: // b15 = port16, ... , b1 = port2, b0 = port1 // uint16_t mask_tab[16] = { 0x0001, // ch 1 --> port 1 0x0002, // ch 2 --> port 2 0x0004, // ch 3 --> port 3 0x0008, // ch 4 --> port 4 0x0010, // ch 5 --> port 5 0x0020, // ch 6 --> port 6 0x0040, // ch 7 --> port 7 0x0040, // ch 8 --> port 7 0x0040, // ch 9 --> port 7 0xffff, // ch 10 --> all port (broadcast drum ch) 0x0040, // ch 11 --> port 7 0x0040, // ch 12 --> port 7 0x0040, // ch 13 --> port 7 0x0040, // ch 14 --> port 7 0x0040, // ch 15 --> port 7 0x0040 // ch 16 --> port 7 }; // uint16_t mask_tab[] // // system common/realtime message to port mask pattern table // port bitmap assignment: // b15 = port16, ... , b1 = port2, b0 = port1 // uint16_t sysmsg_tab[16] = { 0xffff, // 0xF0, Start of Exclusive (arbitrary) 0xffff, // 0xF1, MTC Quarter frame (2 byte) 0xffff, // 0xF2, Song Position Pointer (3 byte) 0xffff, // 0xF3, Song Select (2 byte) 0x0000, // 0xF4, undefined 0x0000, // 0xF5, undefined 0xffff, // 0xF6, Tune Request (obsolete) 0xffff, // 0xF7, End of Exclusive 0xffff, // 0xF8, MIDI Clock 0x0000, // 0xF9, undefined 0xffff, // 0xFA, Start 0xffff, // 0xFB, Continue 0xffff, // 0xFC, Stop 0x0000, // 0xFD, undefined 0xffff, // 0xFE, Active sensing 0x0000 // 0xFF, System Reset (obsolete) }; // uint16_t sysmsg_tab[] // not System Exclusive mode flag value #define NO_SYSX (-1) // System Exclusive flag and buffer index int8_t sysx_ind = NO_SYSX; // initially no sysx mode // header length of // XG Display Bitmap Data System Exclusive // (0xf0 and address Mid/Low not included) #define SYSX_HDR_LEN (4) // header of XG Display Bitmap Data message uint8_t sysx_hdr[SYSX_HDR_LEN] = { // 0xf0 : Start Of Exclusive (not icluded) 0x43, // YAMAHA ID 0x1f, // device number = 16 0x4c, // XG model ID 0x07 // address High (XG display bitmap data) // vh : address Mid (vert/horiz) // 00 : address Low }; // uint8_t sysx_hdr[] // buffer size for XG display bitmap system exclusive // (0xf0 (SOX), 0xf7 (EOX) ommitted) // SYSX message len = 56, buffer len = 54 #define SYSX_BUF_LEN (SYSX_HDR_LEN + 2 + (3 * 16)) uint8_t sysx_buf[SYSX_BUF_LEN]; // table of pointer to remap/mask table uint16_t *(tab_list[3]) = { remap_tab, mask_tab, sysmsg_tab }; // uint16_t tab_list[][] // load remap/mask table // by System Exclusive message void load_mask(uint8_t n) { uint16_t *p; // remap/mask table pointer uint16_t bm; // assembled bitmap // extract "vertical" part (b5..b4) n = (0x03 & (n >> 4)); if (2 < n) { n = 0; } // validate n = 0..2 p = tab_list[n]; // get remap/mask table pointer for (uint8_t i = 0; i < 16; i++) { // assemble bitmap data bm = (0xfe00 & (sysx_buf[i+SYSX_HDR_LEN+2 ] << 9)); // b15..b9 bm |= (0x01fc & (sysx_buf[i+SYSX_HDR_LEN+2+16] << 2)); // b9..b2 bm |= (0x0003 & (sysx_buf[i+SYSX_HDR_LEN+2+32] >> 5)); // b1..b0 p[i] = bm; // assembled bitmap } // for (int i = 0; ...) } // void load_mask() // // System Exclusive message processing // void sysx_proc(uint8_t c) { // ignore System Realtime message if (0xf8 <= c) { return; } // channel message or system common/exclusive message if (0x80 <= c) { // status byte if (0xf7 == c) { // EOX if ((((int8_t)SYSX_BUF_LEN) <= sysx_ind) && // sufficient data received (0 == memcmp(sysx_hdr, sysx_buf, sizeof(sysx_hdr)))) { // header match load_mask(sysx_buf[SYSX_HDR_LEN]); } // if ((SYSX_BUF_LEN <= ...) { ... sysx_ind = NO_SYSX; // leave sysx mode } else if (0xf0 == c) { // SOX sysx_ind = 0; // enter sysx mode } else { // other status byte sysx_ind = NO_SYSX; // leave sysx mode } // if (0xf7 == c) { ... } else { // data byte if (0 <= sysx_ind) { // copy MIDI byte to sysx buffer if (SYSX_BUF_LEN <= sysx_ind) { // sysx buffer full sysx_ind = NO_SYSX; // leave sysx mode } else { // buffer has room sysx_buf[sysx_ind++] = c; } // if (SYSX_BUF_LEN <= sysx_ind) { ... } // if (0 <= sysx_ind) { ... } // if (0x80 <= c) { ... } // void sysx_proc() void send_midibyte( void ) { uint8_t c; // received MIDI byte uint8_t midi_ch; // MIDI ch / system message type uint16_t bm; // bitmask pattern if (0 == Serial.available()) { return; } c = Serial.read(); // get byte from MIDI-IN midi_ch = (0x0f & c); // extract MIDI ch / system msg type bm = r_mask; // use "running" port mask if (0x80 & c) { // status byte if (0xf0 > c) { // channel mode message [0x80..0xef] bm = mask_tab[midi_ch]; // get port mask pattern r_mask = bm; // set "running" port mask pattern #if !defined(REMAP_INH) c = ((c & 0xf0) | remap_tab[midi_ch]); // remap MIDI channel #endif } else { // system common/realtime message [0xf0..0xff] bm = sysmsg_tab[midi_ch]; // escaping for system realtime if (0xf8 > c) { // system common msg [0xf0..0xf7] r_mask = bm; // set "running" port mask pattern } // if (0xf8 > c) { ... } // if (0xf0 > c) {} else { ... } else { // data byte // no processing required for data byte } // if (0x80 & c) { ... bm = MASK_POL(bm); // effective port mask pattern // setup "OR" mask MASK1_PORT = ((~MASK1_MASK & MASK1_PORT) | (MASK1_MASK & (uint8_t)SHIFT_R(bm, MASK1_SHIFT))); MASK2_PORT = ((~MASK2_MASK & MASK2_PORT) | (MASK2_MASK & (uint8_t)SHIFT_R(bm, MASK2_SHIFT))); // send MIDI byte UDR0 = c; sysx_proc(c); } // void send_midibyte() // UART TX COMPLETE interrupt handler ISR(USART_TX_vect) { send_midibyte(); if (0 == Serial.available()) { // clear TXCIE0 (TX Complete Interrupt Enable 0) bitClear(UCSR0B, TXCIE0); // disable TXC int. } // if (0 == ...) } // ISR(USART_TX_vect) void setup() { // put your setup code here, to run once: Serial.begin(31250); // (legacy) MIDI // clear UDRIE0 (UART Data Register empty Interrupt Enable 0) bitClear(UCSR0B, UDRIE0); // setup initial "OR" mask (all masked) MASK1_PORT = ((~MASK1_MASK & MASK1_PORT) | (MASK1_MASK & (uint8_t)MASK_POL(0))); MASK2_PORT = ((~MASK2_MASK & MASK2_PORT) | (MASK2_MASK & (uint8_t)MASK_POL(0))); // set data direction of output port MASK1_DDR |= MASK1_MASK; MASK2_DDR |= MASK2_MASK; set_sleep_mode(SLEEP_MODE_IDLE); UDR0 = 0xfe; // kick TX by dummy Active Sensing pinMode(LED_pin, OUTPUT); } // void setup() void loop() { // put your main code here, to run repeatedly: noInterrupts(); // enter critical section if (Serial.available()) { // MIDI byte received digitalWrite(LED_pin, HIGH); // set TXCIE0 (TX Complete Interrupt Enable 0) bitSet(UCSR0B, TXCIE0); // enable TXC int. } else { digitalWrite(LED_pin, LOW); } // if (Serial.available()) { ... interrupts(); // leave critical section // sleep until interrupt occurred sleep_mode(); } // void loop()