ムライボックス (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()