読者です 読者をやめる 読者になる 読者になる

うさがにっき

読書感想文とプログラムのこと書いてきます

Androidを使ってMIDIを操作する

概要

Android Mから追加されたMIDI操作APIについてまとめる

詳細

MIDI APIには大きく分けて二つの機能カテゴリが存在する

  • MIDIデバイスを使用するためのAPI

MIDIを利用したアプリケーションを開発す場合に使う

  • MIDIデバイスサービスを実装するためのAPI

MIDIソフトウェアシンセサイザーなどを公開して、他のアプリケーションで使用してもらう場合使う

どちら側のAPIにも言えることだが、MIDI APIは「MIDIデバイス」が複数の入出力ポートを持ち、それらを開いて「MIDIメッセージ」を送受信する

MIDIデバイスを使用するためのAPI

次のような流れになる

  • MIDIデバイスのリストを取得して、そのひとつをオープン
  • そのデバイスのMIDI入力ポートあるいはMIDI出力ポートをオープン
  • MIDI入力デバイスを使用するなら、入力ポートからのMIDIメッセージを処理
  • MIDI出力デバイスを使用するなら、音を鳴らしたい(なんらかのイベントを発火させたい)タイミングに合わせてMIDIメッセージを送信する

Android Mで追加されたMIDI APIは、あくまでMIDIデバイスの接続を可能にしたもので、高水準の処理を行う処理(標準MIDIファイルの再生)などは存在しない、それらはアプリ開発者の手に委ねられている

MIDIデバイスのリストを取得して、そのひとつをオープン

現在接続されているデバイスの「デバイス情報」の配列を取得する

        MidiManager manager = (MidiManager) getSystemService(Context.MIDI_SERVICE);
        MidiDeviceInfo[] deviceInfos = manager.getDevices();

上記の方法ではgetDeveices()を呼び出した時点でのデバイスのリストが返却されるのみだが、registerDeviceCallback (MidiManager.DeviceCallback callback, Handler handler)でMidiManagerを使用して後から接続されたデバイスを検出したりすることもできる

MidiDeviceInfoには、そのMIDIデバイスの詳細情報が含まれている
MIDI楽器には、伝統的に製造者や製品名などのメタ情報が存在している
以下のメソッドでメタ情報と、そのデバイスのポート情報の配列が取得できる
ポート情報にはポート種別(input, output)ポート名、ポート番号が格納されている

        for(MidiDeviceInfo midiDeviceInfo : deviceInfos) {
            Bundle bundle = midiDeviceInfo.getProperties();
            // MidiDeviceInfo.PROPERTY*でいろいろとれる
            bundle.getString(MidiDeviceInfo.PROPERTY_NAME);
            bundle.getString(MidiDeviceInfo.PROPERTY_PRODUCT);
            // port情報
            MidiDeviceInfo.PortInfo[] portInfos = midiDeviceInfo.getPorts();
        }

MidiDeviceInfoの情報をもとに使用するデバイスが決まったら、以下のようMidiDeviceInfoをパラメータにデバイスをopenする

            manager.openDevice(midiDeviceInfo, new MidiManager.OnDeviceOpenedListener() {
                @Override
                public void onDeviceOpened(MidiDevice device) {

                }
            }, null);

デバイスをオープンする処理を非同期で実行し、自身はすぐ終了する
デバイスがオープンされたらonDeviceOpenedが呼び出され接続状態になったMidiDeviceオブジェクトが利用可能となる
もしこのcallbackを特定のスレッドコンテキストで呼び出したい場合は第3引数でHandlerで指定する

そのデバイスのMIDI入力ポートあるいはMIDI出力ポートをオープン

使えるMidiDeviceが手に入ったら次は以下のようにそのDeviceのポートを選択してオープンする

                    MidiInputPort inPort = device.openInputPort(inPortNumber);
                    MidiOutputPort outPort = device.openOutputPort(outPortNumber);

注意したいのは端末が受け取ったデータがMidiOutputPortとして受け取られ、
送信したデータがMidiInputPortとして扱われること
端末から考えると逆なんだけど、仕様としてそうなってるので間違えないように注意

MIDIメッセージの送受信

一旦ポートを開いてしまえば以下のメソッドを使いデータのやり取りを行う

MidiInputPort.onSend (byte[] msg, int offset, int count, long timestamp)
MidiOutputPort.onConnect (MidiReceiver receiver)

MidiReceiverを取得したMIDIデバイスからのメッセージ取得は少し小難しい
MidiReceiverとはMIDIメッセージを受信するためのクラス、MIDIメッセージを受信したい場合は、このクラスを派生させて、そのonSend()メソッドに自分の行いたい処理を記述する

MidiReceiver midiReceiver = new MidiReceiver() {
            @Override
            public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
                // 受信したデータの処理
            }
        };

MidiReceiverのオブジェクトを作成したらそれをMidiOutputPort.onConnectに渡すと、そのMIDI入力デバイスから送られてきたメッセージがこのMidiReceiver.onSendの呼び出しとして届くという流れ
MIDI出力を処理する実態であるMidiInputPortもMidiReceiverを継承している、そのためMidiOutputPortにMidiInputPortをonConnectメソッドで接続することもできる
MidiOutputに接続したMidiReceiverはdisconnectメソッドで接続解除ができる

MIDIメッセージを送受信する際にはtimestampが送信されてくる
これはMIDIメッセージのレイテンシー(遅延)をプログラムレベルで吸収するために存在している

MIDIデバイスを作成して公開する

前項ではMIDIデバイスに接続して使用するアプリケーション側のAPIについてまとめた
ここからはMIDIデバイスの「提供者」が使用する機能MIDIデバイスサービスについてまとめる
MIDIデバイスサービスのAPIを使用すると、任意のAndroidアプリケーションで使用できる仮想的なMIDIデバイスを作成できる

どのような目的でこのAPI使用すべきか

MIDIデバイスサービスを使用して作成されたMIDIデバイスはAndroidサービスとしてデバイス全体に公開される
もし任意のアプリケーションから使用されることを前提としていないのであれば、MIDIデバイスサービスは使用すべきではない
任意のアプリケーションでだけ使いたいのであれば単にMidiSenderあるいはMidiReceiverの実装を作って同じアプリケーション内で使用するだけで十分
そういう意味では一般的なMIDIデバイスとして振舞うことが期待されているが、特殊なサウンド生成などのアレンジは面白いと思う
あくまで「任意のアプリケ0sy9オンに利用されることが意図している」ことが大事

MIDIデバイスの種類

Android MがサポートするMIDIデバイスの種類

USBで接続したMIDIデバイス
接続すればMidiManager.getDevices()でデバイスを取得できる

BlueToothで接続したMIDIデバイス
注意点としてAndroidシステム上で単にMIDI over BLEのデバイスをペアリングしてもMIDIデバイスとして利用可能にはならない
いったんBluetooth APIでBluetoothDeviceのインスタンスを取得してから、それをMidiManager.openBluetoothDeviceメソッドに渡して、MIDIデバイスとして登録する必要がある
具体的には、以下のような処理が必要

        BluetoothManager manager =
                (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
        BluetoothAdapter bluetoothAdapter = manager.getAdapter();

        final MidiManager.OnDeviceOpenedListener onDeviceOpenedListener 
                = new MidiManager.OnDeviceOpenedListener() {
            @Override
            public void onDeviceOpened(MidiDevice device) {
                // deviceオープンの処理        
            }
        };
        
        ScanCallback scanCallback = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                super.onScanResult(callbackType, result);
                MidiManager midi = (MidiManager)getSystemService(Context.MIDI_SERVICE);
                midi.openBluetoothDevice(result.getDevice(), onDeviceOpenedListener , null);
            }
        };

        BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
        bluetoothLeScanner.startScan(scanCallback);
  • 仮想MIDIデバイス

ソフトウェアレベルで実装される「MIDIデバイス」
MIDIメッセージを受け取って、なんらかの処理を行う」ものとして作成されていれば、MIDIデバイスとして認識させることは可能

仮想MIDIデバイスの作成

仮想MIDIデバイスを公開するために必要な手順

  • MidiDeviceServiceクラスの実装を作成

MidiDeviceServiceを継承し、onGetInputPortReceivers()を実装

  • AndroidManifestにServiceとして記述

MidiDeviceService | Android DevelopersにあるようにAndroidManifestに記載

  • デバイス情報を記録するXMLファイルを作成

マニフェストに記載したandroid:resource="@xml/device_info"にデバイス情報を定義

<device>
  <device manufacture="製造者名" product="製品名">
    <input-port name = "ポート名"/>
    <output-port name="ポート名"/>
  </device>
</device>

あとはMIDI OUTで実装する場合は、onGetInputPortReceivers()の中に実装
MIDI INで実装する場合は、MidiDeviceService内に実装