bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

Java Sound でMIDI機器を操る

KORG nanoKEY を買ったので Java Sound API (MIDI) がどの様にMIDI機器を扱っているのか調べています。どこの家庭にもあるカシオの電子キーボードとかがなにげにMIDI入出力に対応していたりするので、この辺がわかると何か面白い事が出来るかもです。

KORG SLIM-LINE USB KEYBOARD nanoKEY

KORG SLIM-LINE USB KEYBOARD nanoKEY

Mac OS X での注意事項

nanoKEY 特有の問題か他のMIDI入力機器もなのかよくわからないのですが、そのままの状態だとJava Sound 経由でデバイスが見えないという現象に悩まされました。以下の方法でサクっと解決しました。インターネットって素晴らしいですね。

Java から MIDI ループバックデバイスを利用する (Mac OS X)
http://d.hatena.ne.jp/misky/20080417/1208444371

Java Sound の基本

Java Sound は アクセス可能なすべての MIDI機器を javax.sound.midi.MidiDevice というインターフェイスで表現します(以下 javax.sound.midi. は省略)。
http://sdc.sun.co.jp/java/docs/j2se/1.4/ja/docs/ja/api/javax/sound/midi/MidiDevice.html

MidiSystem.getMidiDevices() という static な関数で 利用可能な MidiDevice(のインターフェイスが実装されたもの) の一覧を取得することができます。この関数で得られる MidiDevice は以下の3種類に分類できます。

Sequencer

MIDIメッセージを組み立てる装置。MIDIは3バイトのMIDIメッセージの集合で、機材によっては命令を受け取ったらすぐに音が鳴ってしまいます。たくさんのチャンネルを持った楽曲を正確に楽譜通りに演奏するために、Sequencer を使って楽曲を組み立て(もしくはMIDIファイルを読み込んだり)、Synthesizer に送り出します。

Sequencer に音を足して行って start() すると音が鳴るので、これ自身が音を出しているように見えますが、この場合は Java Sound Synthesizer というデフォルトのSynthesizerが実際の音楽の再生に使用されます。非常に便利ですが全体の構成をわかりにくくしている気がしますね。

Synthesizer

Synthesizerは音が鳴る装置の事です。これはちょっと名前が持つ印象と違うので困惑ポイントですね。普通「シンセサイザー」といったら上記のSequencerの方のイメージが強い気がします。DataSpeaker とかの方がよかったんじゃないかなあと。。

往年のSC-88のようなサンプリングされた音色データが詰め込まれているMIDI音源と呼ばれる装置や、デフォルトになっている Java Sound Synthesizer のようなソフトウェア音源がこれにあたります。

その他の機器

MIDI機器の中には Sequencer でも Synthesizer でも無い物もあります。例えば今回の KORG nanoKEY のような単なるMIDIキーボードの場合がそうです。音を組み立てたり録音したり実際にならしたりすることは出来ないが、入力は出来るよ、みたいな機器ですね。

自分の環境を調べる

上記をふまえて自分の環境を見てみます。

    public static ArrayList<MidiDevice> getDevices() {
        ArrayList<MidiDevice> devices = new ArrayList<MidiDevice>();

        MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
        for (int i = 0; i < infos.length; i++) {
            MidiDevice.Info info = infos[i];
            MidiDevice dev = null;
            try {
                dev = MidiSystem.getMidiDevice(info);
                devices.add(dev);
            } catch (SecurityException e) {
                System.err.println(e.getMessage());
            } catch (MidiUnavailableException e) {
                System.err.println(e.getMessage());
            }
        }
        return devices;
    }

自分の環境におけるMIDI機器情報の一覧取得には MidiSystem.getMidiDeviceInfo() を使います。MidiDevice.Info はその機器の名前やベンダー情報等が含まれています。その後 MidiDevice dev = MidiSystem.getMidiDevice(info) で実際の MidiDevice を取り出す事が可能です。簡単ですね。

取得した情報をstdoutに出力してみます。

    public static void dumpDeviceInfo() {
        ArrayList<MidiDevice> devices = getDevices();

        for (int i = 0; i < devices.size(); i++) {
            MidiDevice device = devices.get(i);
            MidiDevice.Info info = device.getDeviceInfo();
            System.out.println("[" + i + "] devinfo: " + info.toString());
            System.out.println("  name:"        + info.getName());
            System.out.println("  vendor:"      + info.getVendor());
            System.out.println("  version:"     + info.getVersion());
            System.out.println("  description:" + info.getDescription());
            if (device instanceof Synthesizer) {
                System.out.println("  SYNTHESIZER");
            }
            if (device instanceof Sequencer) {
                System.out.println("  SEQUENCER");
            }
            System.out.println("");
        }
    }

僕の環境だとこんな感じで出ます。

[0] devinfo: nanoKEY - KEYBOARD
  name:nanoKEY - KEYBOARD
  vendor:KORG INC.
  version:1.0
  description:KEYBOARD

[1] devinfo: nanoKEY - CTRL
  name:nanoKEY - CTRL
  vendor:KORG INC.
  version:1.0
  description:CTRL

[2] devinfo: Real Time Sequencer
  name:Real Time Sequencer
  vendor:Sun Microsystems
  version:Version 1.0
  description:Software sequencer
  SEQUENCER

[3] devinfo: Java Sound Synthesizer
  name:Java Sound Synthesizer
  vendor:Sun Microsystems
  version:Version 1.0
  description:Software wavetable synthesizer and receiver
  SYNTHESIZER

nanoKEY もちゃんと見えてますね。なんで2つの機器として認識されてるのかよくわからないです。左側の小さいボタンの部分が別のものと認識されてるのかな。。[2] と [3] は Java Sound が用意している sequencer と synthesizer ですね。どちらも抽象的なソフトウェア装置です。それぞれがちゃんと Sequencer, Synthesizer として認識されているのがわかります。これは device instanceof (Sequencer|Synthesizer) で確認出来ます。

僕の場合は機材が少ないのでこれだけですが、電子ピアノとかを持っている人で且つMIDI接続されていれば表示されるはずです。そしてまたほとんどの電子ピアノは Sequencer であり Synthesizer でありその他の入力機器である、という状態になると思います。なぜなら、YAMAHA/Roland/CASIOとかのよくある電子ピアノの場合、それ自体で「鍵盤の入力を受け付ける」「その音を鳴らす」「内蔵されている音源を再生する/演奏を録音する」というすべての役割の仕事をこなさないといけないからです。

今の環境だと音の出口が「 Java Sound Synthesizer」、入力は「nanoKEY - KEYBOARD」か「Real Time Sequencer」であることがわります。

では nanoKEY への入力をそのまま Java Sound Synthesizer の内蔵音源でならしてみたいと思います。

音を鳴らす

純粋に入力をsynthesizerに割り当てるだけであれば以下のコードだけです。

    static final int DEVICE_IN  = 0;
    static final int DEVICE_OUT = 3;

    public static void main (String[] args) {
        dumpDeviceInfo();

        ArrayList<MidiDevice> devices = getDevices();
        MidiDevice device_input  = devices.get(DEVICE_IN);
        MidiDevice device_output = devices.get(DEVICE_OUT);
        if (!(device_output instanceof Synthesizer)) {
            throw new IllegalArgumentException("not a Synthesizer!");
        }

        try { // connet nanoKEY transmitter to Software synthesizer
            Transmitter trans // nanoKEY's transmitter
                = device_input.getTransmitter();
            Receiver recv  // Java Sound Synthesizer's receiver
                = device_output.getReceiver();

            if (!device_output.isOpen()) {
                device_output.open();
            }
            trans.setReceiver(recv);
        } catch (MidiUnavailableException e) {
            System.err.println(e.getMessage());
            System.exit(0);
        }
    }

コードの断片になってしまって見づらいですが、、dumpDeviceInfo() は先ほど定義したデバイス一覧表示の関数です。どのデバイスを使うかは実際にはなんらかの形で選択できるようにすると思うのですが、この例では決め打ちしてます。全体のコードは以下になります。
http://gist.github.com/83068

上記の流れを理解するためには、Transmitter, Receiver について知る必要があります。

Transmitter と Receiver

上記の例を図解すると以下のようになります。

f:id:bonar:20090322173433p:image

Java Sound では MidiDevice は Transmitter と Receiver という2つの入出力を持つ、という設計になっています。簡単にいうと Transmitter はMIDIメッセージ送信時の処理、ReceiverはMIDIメッセージ受信時の処理を担当します。

例えば今の例で言うと、「nanoKEYの入力を Java Sound Synthesizerで鳴らしたい」ということなので、Java Sound 的な表現だとこれは「nanoKEY の Transmitter に Java Sound Synthesizer のReceiver を登録する」になります。MIDIメッセージの発生源(送信者)に、誰に伝えればいいのかを教えるイメージです。これによって、nanoKEY への入力が Java Sound Synthesizer に送られます。以下の部分がその処理です。

        try { // connet nanoKEY transmitter to Software synthesizer
            Transmitter trans // nanoKEY's transmitter
                = device_input.getTransmitter();
            Receiver recv  // Java Sound Synthesizer's receiver
                = device_output.getReceiver();

            if (!device_output.isOpen()) {
                device_output.open();
            }
            trans.setReceiver(recv);

非常に簡素ですね。これによって nanoKEY で入力を行うと、ハードウェアがそれをMIDIメッセージに変換して、Transmitterに渡す、Transmitterが登録されたReceiver(device_output.getReceiver())にそれを渡して後はそのReceiverが受け取ったMIDIメッセージをうまい事処理して音が鳴ります。もちろんそのReceiverがSynthesizerから取り出されたものである必要があります。

Receiver を自作する

せっかくMIDI入力をキャッチして音を鳴らす事が出来たので、自然とそれに何らかの操作を加えたくなります。MIDIメッセージを途中で書き換えたりできると面白そうです。上記の例では 出力先のSynthesizerから取得したReceiverをそのまま使用していましたが、ここには自作のReceiverを与える事も出来ます。

javax.sound.midi.Receiver

イメージ的には以下のような感じです。

f:id:bonar:20090322173431p:image

Transmitter は送信するべきMIDIメッセージの準備が出来ると、登録されたReceiverにそのMIDIメッセージを与えます。この挙動を確保するために、自作のReceiverは javax.sound.midi.Receiver インターフェイスを実装している必要があります。Receiverインターフェイスは以下の2つのシグネチャのメソッドを要求します。

public void send(MidiMessage message, long timeStamp)
public void close()

MIDIメッセージを作り、登録されたReceiverを取り出してそのインスタンスの send に作ったメッセージを与える、という流れですね。close() は終了時の処理です。

MidiChannel

ここまでは非常に簡単なのですが、実際に自分でReceiverを書くとなると、Synthesizerから取り出されたReceiverがやっているような処理を自分で書かなくてはいけなくなるのでやっかいです。通常Synthesizerは内部に16個のチャンネル(MidiChannel)を持っており、そのどれかのチャンネルに NOTE_ON というMIDIメッセージを送る事で鍵盤が押されたことを伝え、NOTE_OFF で指が鍵盤から離れたことを伝えます。こういった演奏情報(ボイスメッセージ)意外にも、再生/停止を制御するようなシステムメッセージもあり、まじめにやるならそれらすべてを正確にハンドリングする必要があります。

send MidiMessage

send() に渡される message は実際には以下のサブクラスのどれかになります。

MetaMessage 各種メタイベント
ShortMessage システムエクスクルーシブとメタイベントを除くすべて
SysexMessage システムエクスクルーシブメッセージ

MidiMessage
http://java.sun.com/javase/ja/6/docs/ja/api/javax/sound/midi/MidiMessage.html

今回は通常の打鍵アクション時に発生する ShortMessage の中のNOTE_ON/NOTE_OFF(鍵盤を押す、離す)だけに絞って実装を行おうとおもいます。まず入力が nanoKEY だけなので、送信先 Synthesizer のどこか単独のチャンネルにMIDIメッセージをリレーしてみようと思います。

コンストラクタで送信先のMidiDeviceを受け取って、そのSynthesizerの最初のチャンネルをデフォルトの送信チャンネルに設定します。

    public MyReceiver(MidiDevice device_out) {
        if (!(device_out instanceof Synthesizer)) {
            throw new IllegalArgumentException(
               "device is not a Synthesizer");
        }
        this.synth = (Synthesizer)device_out;

        // get first channel of the device
        MidiChannel[] channels = this.synth.getChannels();
        if (0 == channels.length) {
            throw new IllegalStateException("no channels available");
        }
        this.defaultChannel = channels[0];
    }

肝心のsendメソッドですが、

    public void send(MidiMessage message, long timeStamp) {
        if (message instanceof ShortMessage) {
            ShortMessage sm = ((ShortMessage)message);
            switch(sm.getCommand()) {
                case ShortMessage.NOTE_ON:
                    this.defaultChannel.noteOn(
                        minorize(sm.getData1()), sm.getData2());
                    break;
                case ShortMessage.NOTE_OFF:
                    this.defaultChannel.noteOff(
                        minorize(sm.getData1()), sm.getData2());
                    break;
            }
        }
    }

まず、ShortMessage以外はすべて無視してます。ShortMessageの場合getCommand() でそのMIDIメッセージの種類がわかり、ShortMessageクラスで定義されている定数と照合することで何のイベントなのかが判別できます。あとは送信先デバイスの目的のチャンネルに適切な操作(noteOn, noteOff)を行えばOKです。

MIDIメッセージは3バイト1セットのメッセージで、最初のバイトがステータス(何のメッセージなのか)、その後2バイトがデータ領域となっていす。noteOn, noteOff 命令の場合2バイト目が音階(ドレミ)、3バイト目がベロシティ(音の強さ)になります。

短調変換

せっかくフィルタするので何か変化が無いと寂しいかなと思って、minorize() という処理を行っています。

    private int minorize(int origin) {
        int minorized = origin;
        int note = (origin % 12);
        if (note == 4 || note == 9 || note == 11) {
                minorized--;
        }
        return minorized;
    }

(3|6|7)度の音の場合に半音下げる、ということをしていて、つまりハ長調をイ短調に変換しています。

全体のソース
http://gist.github.com/83095

デモ

最初の方の単純にSynthesizerに送るだけのもの(SimpleInput)と、自作のフィルタでスケールを変換するもの(FilteredInput)を実際に弾いてみました。同じ鍵盤を弾いているのに出ている音が違う事がわかります。

まとめ

Java Sound は本当にいたれりつくせりで、これ以上無いくらいにシンプルで分かりやすく出来ていると思います。慣れていないと分かりづらい単語や概念があったりしますが、全体的に洗練されていると思います。MidiSystem の名前空間の中に getReceiver や getSynthesizer のようなデバイスを自動選択してReceiverオブジェクト等を返す便利なメソッドが多数存在し、それはそれでいいのですが、それが逆に詳細を分かりにくくしている(このレシーバってどこのレシーバ?)気もしました。

ただ、見てきたように非常にシンプルにMIDI機器の入力をハンドリングできるので、キーボードだけではなくペタルや音源ユニット、スイッチ等いろいろなものを使って応用が出来るかもしれません。

オリジナルの楽器を作ってるみたいで楽しいですね。