PIC16F145xを使ったUSB-MIDI I/Fの作り方

前回は回路図などの紹介だけにとどめておいたが、今回はマイクロチップのライブラリを使ってMIDI I/Fを作るために必要なことについてまとめて、今後のための備忘録としておく。

今回利用したライブラリはマイクロチップ社が公開しているMLA:Microchip Libraries for Applicationsで2016-04-16バージョンでUSBライブラリのバージョンは2.14である。これのUSB Device – Audio – MIDIのサンプルプログラムに必要な機能を付け加える形となる。

このサンプルプログラムは、実際に実行させていないので何をするものなのかは知らないが、ソースを見る限り、スイッチを押すと音階を変えながら音を出すプログラムのようである。出すと言ってもホスト側に送っているだけなので、接続されているパソコンの音源を鳴らすと言うことになるだろう。このサンプルでは、デバイスからホストへのNOTE ONイベントの送信の仕方しかわからないので、全く参考にならない。とは言え、参考になるような書籍、Webなどほとんど存在していないことも事実で、USBでMIDIをするための情報は少なく、途方に暮れている人も多いのではないかと思う。

唯一使える情報としては、USB MIDI Devices 1.0(midi10.pdf)と言われる1999年発行のUSB-MIDIデバイスのための仕様書で、USB.orgからダウンロードできる。結局、この資料とUSB機器に関する基礎的な内容と組み合わせることでプログラムを作成することとなってしまった。MIDI自体はかなり古い規格なので一般的には使われることが少なくなってきたことも事実であるが、音楽をやる人間にとっては必要不可欠な規格であるので、もう少し情報があってもいいような気がする。

1.USB-MIDIインタフェースの仕組み

パソコンとUSBケーブルによってやりとりされる情報はパケットによって運ばれる。エンドポイントやら何やらいろいろややこしい話は割愛して、上り下りともに最大64バイトのパケットに乗せてやりとりされる。そして、この64バイトのパケットを4バイト毎のイベントパケットに分割して処理されることになる。この4バイトのイベントパケットは1バイトのヘッダと3バイトのメッセージに分かれていて、この3バイトは基本的にはMIDIメッセージの3バイトである。ヘッダの1バイトは上位4ビットがケーブル番号で1つのデバイスで複数のMIDI IN/OUTを扱うための数字となる。つまり、1つのデバイスで16のMID INと16のMIDI OUTの端子を設ける事ができる。下位4ビットは、データの種別で、続く3バイトのデータの種類をあらわしている。詳しいことはmidi10.pdfのp16-17に記載してある。

2.USB-MIDIインタフェースの処理の流れ

ホストからUSBを通じてMIDI OUTするまで:先 述のようにホストから送信(MIDI OUT)されたデータは最大64バイトのパケットとしてデバイスに届けられる。デバイスでは、この最大64バイトのパケットを4バイトのイベントパケット 毎に分割しながら、個々のパケットの内容に従って処理しながらMIDI OUT用のシリアルインタフェースへ出力する。ここでシリアルに送らずに何らかの処理をすれば音源などのデバイスを作ることができよう。前回の記事で MIDIのシリアルは遅いということを紹介したが、受けとったイベントパケットをその場でシリアルに送信すると効率が悪い。そこで、受信だけではなく送信 も割り込みを使用する。送信用バッファを用意し、イベントパケットの内容はまず送信用バッファに格納する。シリアルの送信割り込みで送信用バッファ内の データを順次MIDI OUT端子から出力する。

MIDI INからUSBを通じてホストまで:シリアルの受信割り込みによって受信したデータは、割り込み内でMIDIメッセージ解析し、メッセージ毎に受信バッファに格納する。受信バッファのデータは、そのままUSB用の4バイトのイベントパケットに格納してUSBでホストに送信する。

3.MLAにおける処理の実装

MLAでは、USBに関するややこしい処理はすべてハードウエアとライブラリによって行われているため、ユーザが実装すべき処理は、起動時の初期設定とデータの送受信時の処理の2つとなる。これに、必要に応じて割り込み処理を記述すればUSBデバイスを作成することができる。以前のバージョンのライブラリではmain.cにほぼすべての処理が記述されていたのでややこしかったが、今のライブラリではユーザが記述すべき部分が別ソースファイルに抜き出されているのでわかりやすくなっている。基本的に記述すべき事項が存在するファイルは次の通りとなる。

  • app_device_audio_midi.c,h 初期化処理、主処理が格納されている
  • system.c,h 割り込み処理が記述されている
  • io_mapping.h LEDやSWについての定義が宣言されている
  • app_led_usb_status.c io_mappingで宣言したLEDをどう使用するかが記述されている
  • usb/usb_descriptors.c デバイスのデスクリプタが定義されている

 

4.実際のソースコードの追加、修正

USB-MIDIインタフェースのプログラムを制作するためには次の2つの知識が不可欠となる。

  • PICマイコンのシリアルインタフェースの割り込みによる送受信プログラムを作成するための知識
  • MIDIメッセージに関する知識(システムエクスクルーシブに関することを含む)

これらのうちのシリアルに関する情報は書籍、Webで溢れるほど存在するので割愛し、ここでは主にMIDIメッセージをどう扱うかについて説明する。

MLAでは、デフォルトではポーリングによってUSBの処理を行っており、その処理はapp_device_audio_midi.cのAPP_DeviceAudioMIDITasks()に実装されている。そして、ホストからとホストへの2方向のパケットの処理がメインとなる。まずはホストからのデータ処理について、受信しているか判断してからの処理は以下のような形になる。説明をわかりやすくするためにランニングステータスやパケットの分割処理は抜いてある。

    d=ReceivedDataBuffer; // ホストからのデータはReceivedDataBufferに格納されている
    head=*d;    // イベントパケットの1バイト目はヘッダ
    d++;
    switch(midi&0x0f){
        case MIDI_CIN_3_uint8_t_MESSAGE:    // 3bytes
        case MIDI_CIN_SYSEX_START:
        case MIDI_CIN_SYSEX_ENDS_3:
        case MIDI_CIN_NOTE_OFF:
        case MIDI_CIN_NOTE_ON:
        case MIDI_CIN_POLY_KEY_PRESS:
        case MIDI_CIN_CONTROL_CHANGE:
        case MIDI_CIN_PITCH_BEND_CHANGE:
            MObuf[MO_in++]=*d++;MO_in&=MOBUF_MAX;    // シリアル送信バッファへ
        // Down Through
        case MIDI_CIN_2_uint8_t_MESSAGE:    // 2bytes
        case MIDI_CIN_SYSEX_ENDS_2:
        case MIDI_CIN_PROGRAM_CHANGE:
        case MIDI_CIN_CHANNEL_PREASURE:
            MObuf[MO_in++]=*d++;MO_in&=MOBUF_MAX;
        // Down Through
        case MIDI_CIN_1_uint8_t_MESSAGE:    // 1byte
        case MIDI_CIN_SINGLE_uint8_t:
            MObuf[MO_in++]=*d++;MO_in&=MOBUF_MAX;
            TXIE=1;    // 送信割り込みを可にする
        // Down Through
        case MIDI_CIN_MISC_FUNCTION_RESERVED:    // unknown
        case MIDI_CIN_CABLE_EVENTS_RESERVED:
            break;    // ignore this packet
    }

処理すべき内容はイベントパケットのヘッダを見て、何バイトのメッセージかを見て各々の処理を行うだけである。そのままシリアルの送信バッファに格納するだけで、最後に送信割り込みを可にすると、あとは自動的に割り込み処理がMIDI OUTするはずである。受信したパケットは最大64バイトなのでこれを受信したバイト数分処理を行い、最後に次の受信の準備を行う。この処理はMLAのライブラリに元々記載してある。

MIDI INからの処理は簡単で、シリアル受信割り込みですでにMIDIメッセージの解析(後述)は終わっているので受信バッファのデータをそのままホストに送るだけとなる。ホストへの送信が可か確認後の処理としては以下のようになる。

    if(mbuf_inpos!=mbuf_outpos){    // メッセージ受信
        midiData.CableNumber = 0;
        midiData.CodeIndexNumber = mbuf[mbuf_outpos].cin;
        midiData.DATA_0 = mbuf[mbuf_outpos].dat0;
        midiData.DATA_1 = mbuf[mbuf_outpos].dat1;
        midiData.DATA_2 = mbuf[mbuf_outpos].dat2;
        mbuf_outpos++;
        mbuf_outpos&=MBUF_MAX;
        USBTxHandle = USBTxOnePacket(USB_DEVICE_AUDIO_MIDI_ENDPOINT,(uint8_t*)&midiData,4);
    }

必要な主な処理の残りはシリアルから受信したMIDIデータをバッファに格納する処理である。この処理は割り込みルーチンからの呼び出しになる。各変数の説明は冒頭に示しているが、これらの変数はstaticで宣言する必要がある。つまり、このルーチンは連続して送られてくるバイト列を1バイトずつ処理するためのものなので、複数バイトのメッセージは複数回喚ばれることを前提としていることに注意する必要がある。処理の前半はエクスクルーシブの処理、後半が通常メッセージの処理になる。エクスクルーシブはバイト数が固定長ではないので処理がややこしくなる。もちろん、通常のMIDIメッセージも2バイトと3バイトのものがあるので、それぞれについての処理が必要となる。これにランニングステータスの処理が追加されるので、これだけの処理が必要になる。

buf,    // データ1バイト目保存用
ex_flg,    // エクスクルーシブデータ受信中に1
ex_pos,    // エクスクルーシブデータ受信バイト数
ex1,ex2,// エクスクルーシブデータ保存用
s_flg,    // メッセージ受信中(残り受信バイト数)
status,    // ステータスバイト保存用
cin;    // USB-MIDI EventパケットのCode Index No.保存用

    if(dat>=0xf0){    // System Messageの処理
        status=dat;
        switch(dat){
            case QFRAME: case SONG_SEL:    // 2 bytes Msg
                cin=0x02;    // 2byte System Message
                status=dat;
                s_flg=1;    // 残り1バイト
                return;
            case SONG_POS:                // 3 bytes Msg
                cin=0x03;    // 3byte System Message
                status=dat;
                s_flg=2;    // 残り2バイト
                return;
            case EX_SEND:    // Exclusive Send:0xf0
                ex1=dat;ex_pos=1;    // エクスクルーシブバッファに格納
                ex_flg=1;    // のこりバイト読み込み
                return;
            case EX_END:        // Exclusive End:0xf7
                switch(ex_pos){    //    既に読み込み済みバイト数によって処理
                    case 0:
                        mbuf[mbuf_inpos].cin=0x05;    // 1byte SysEx ends
                        mbuf[mbuf_inpos].dat0=EX_END;
                        mbuf[mbuf_inpos].dat1=0;
                        mbuf[mbuf_inpos++].dat2=0;
                        break;
                    case 1:
                        mbuf[mbuf_inpos].cin=0x06;    // 2byte SysEx ends
                        mbuf[mbuf_inpos].dat0=ex1;
                        mbuf[mbuf_inpos].dat1=EX_END;
                        mbuf[mbuf_inpos++].dat2=0;
                        break;
                    case 2:
                        mbuf[mbuf_inpos].cin=0x07;    // 3byte SysEx ends
                        mbuf[mbuf_inpos].dat0=ex1;
                        mbuf[mbuf_inpos].dat1=ex2;
                        mbuf[mbuf_inpos++].dat2=EX_END;
                        break;
                }
                mbuf_inpos&=MBUF_MAX;
                ex_pos=0;
                ex_flg=0;        // end of exclusive
                break;
            default:                    // 1 byte Msg
                mbuf[mbuf_inpos].cin=0x0f;    // 1byte System Msg
                mbuf[mbuf_inpos].dat0=dat;
                mbuf[mbuf_inpos].dat1=0;
                mbuf[mbuf_inpos++].dat2=0;
                mbuf_inpos&=MBUF_MAX;
                return;        // 1バイトメッセージはエクスクルーシブに割り込めるのでそのままリターン
        }
        ex_pos=0;
        return;
    }
    if(dat<0x80){        // データ受信(0x00-0x7f)
        if(ex_flg!=0){        // エクスクルーシブデータ受信中
            switch(ex_pos){
                case 0:
                    ex1=dat;ex_pos++;
                    return;
                case 1:
                    ex2=dat;ex_pos++;
                    return;
                case 2:
                    mbuf[mbuf_inpos].cin=0x04;    // 3byte SysEx continue
                    mbuf[mbuf_inpos].dat0=ex1;
                    mbuf[mbuf_inpos].dat1=ex2;
                    mbuf[mbuf_inpos++].dat2=dat;
                    mbuf_inpos&=MBUF_MAX;
                // down through
                default:    // 安全のため(実際はex_posは0,1,2以外にはならないはず)
                    ex_pos=0;
            }
            return;
        }
    // 2バイトメッセージ
        if(((status&0xe0)==0xc0)||(cin==0x02)){    // PRG_CNG(0x0c),CH_AFT(0x0d),f1,f3 2byte Msg
            mbuf[mbuf_inpos].cin=cin;
            mbuf[mbuf_inpos].dat0=status;
            mbuf[mbuf_inpos].dat1=dat;
            mbuf[mbuf_inpos++].dat2=0;
            mbuf_inpos&=MBUF_MAX;
            s_flg=0;
            return;
        }
    // 3バイトメッセージ
        if(s_flg==1){    // 最終バイト
            mbuf[mbuf_inpos].cin=cin;
            mbuf[mbuf_inpos].dat0=status;
            mbuf[mbuf_inpos].dat1=buf;
            mbuf[mbuf_inpos++].dat2=dat;
            mbuf_inpos&=MBUF_MAX;
            s_flg=0;
            return;
        }
    //    残り2バイトもしくはラニングステータス処理
        buf=dat;
        s_flg=1;
        return;
    }
    // status byte(0xfx以外の0x80以上のデータ)受信
    ex_flg=0;
    status=dat;
    cin=status>>4;
    if((status>=0xc0)&&(status<0xe0)){
        s_flg=1;    // 2バイトメッセージなので残り1バイト
    }else{
        s_flg=2;    // 3バイトメッセージなので残り2バイト
    }

このMIDIメッセージ解析ルーチンはかなり前に作成したものをUSB用に直したものであり、私のMIDI関係のプログラムで多く使われているものである。これからMIDIを勉強したい人たちの参考になればと思い、本邦初公開する。

今回のソースコードはすべてを公開しているわけではないので、このままコピペしても動作しない。しかし、MIDI関係のキモになる部分である。ここに示した以外の部分は大した処理ではなく、どこにでも手に入る情報だと思われるので、しっかり勉強して独自のプログラムを作成していただきたい。というわけで、上記のルーチンは自由に使用してもらって構わないので、独自のプログラムの作成の参考にしてもらえると私としてもうれしい限りだ。

人のブロガーが「いいね」をつけました。
:)