Web MIDI API

レガシーなChromeでは設定を変更して, Web MID APIを有効にする必要があります.

以下の手順で, まずはWeb MIDI APIを有効にしましょう.

  1. 1. Chromeのアドレスバーにchrome://flagsを入力する
  2. 2. Web MIDI APIを有効にするの項目を探し, 有効にする
  3. 3. Chromeを再起動する

Web MIDI APIを有効にすると, navigatorオブジェクトのrequestMIDIAccessメソッドを利用することが可能になります. requestMIDIAccessは, Web MIDI APIを利用するための起点となるメソッドです.

requestMIDIAccessメソッドは引数を1つとり, MIDIOptions型のオブジェクトを指定します. MIDIOptionsは, sysexプロパティをもつオブジェクトで, 値はブーリアン型です. これは, システムエクスクルーシブメッセージを使用するかしないかのオプションです.

そして, requesetMIDIAccessの戻り値は, Promiseオブジェクトです. Promiseオブジェクトは, 非同期処理を同期処理のように記述することを可能にするためのものです. Promise策定前から, jQueryではそのような仕組みを実装していましたが, それをブラウザのネイティブ機能として実装しようという流れで, 多くのモダンブラウザでは既に実装済です (Promiseの詳細はこちらを参照).

requesetMIDIAccessの戻り値であるPromiseオブジェクトのthenメソッドの第1引数には, MIDIデバイスへのアクセスが成功した場合に実行されるコールバック関数を, 第2引数には, MIDIデバイスへのアクセスが失敗した場合に実行されるコールバック関数を指定します.

サンプルコード 01


/**
 * @param {MIDIAccess} midiAccess
 */
var successCallback = function(midiAccess) {
    // do something ....
};

/**
 * @param {DOMException} error
 */
var errorCallback = function(error) {
    // do something ....
};

navigator.requestMIDIAccess({sysex : true}).then(successCallback, errorCallback);

第1引数のコールバック関数には, MIDIAccessインスタンスが渡されます. 第2引数のコールバック関数には, DOMExceptionインスタンスが渡されます.

そして, MIDIAccessインスタンスには, MIDIInputMapインスタンスであるinputsプロパティとMIDIOutputMapインスタンスであるoutputsプロパティが定義されています. MIDIデバイスにアクセスするためには, この2つのプロパティが重要となります.

そのためには, MIDIInputMap / MIDIOutputMapクラスに定義されたvaluesメソッドでイテレーターを取得し, イテレーターからアクセス可能なMIDIデバイスを抽象化したMIDIInput / MIDIOutputインスタンスを取得する必要があります.

サンプルコード 02


/**
 * @param {MIDIAccess} midiAccess
 */
var successCallback = function(midiAccess) {
    /** @type {Array.<MIDIInput>} */
    var inputs  = [];

    /** @type {Array.<MIDIOutput>} */
    var outputs = [];

    if (Object.prototype.toString.call(midiAccess) === '[object Function]') {
        // Legacy Chrome
        inputs  = midiAccess.inputs();
        outputs = midiAccess.outputs();
    } else {
        // Chrome 39 and later
        var inputIterator  = midiAccess.inputs.values();
        var outputIterator = midiAccess.outputs.values();

        for (var i = inputIterator.next(); !i.done; i = inputIterator.next()) {
            inputs.push(i.value);
        }

        for (var o = outputIterator.next(); !o.done; o = outputIterator.next()) {
            outputs.push(o.value);
        }
    }
};

/**
 * @param {DOMException} error
 */
var errorCallback = function(error) {
    // do something ....
};

navigator.requestMIDIAccess({sysex : true}).then(successCallback, errorCallback);

以上で, MIDIメッセージを送受信する準備ができました.

セキュリティの観点からsysexをtrueにすると, ブラウザがMIDIデバイスへアクセスする許可を求めるダイアログを表示します. アクセスしても問題がなければ, 「許可」を選択するようにしてください.

デバイスへアクセスする許可を求めるダイアログ

図4 - 3 - a. デバイスへアクセスする許可を求めるダイアログ

デモ 03

MIDIメッセージの受信

MIDIメッセージを受信するためには, MIDIInputインスタンスのonmidimessageイベントハンドラを設定して, そのイベントオブジェクト (MIDIMessageEventインスタンス) にアクセスすることが必要です.

サンプルコード 03


/**
 * @param {MIDIAccess} midiAccess
 */
var successCallback = function(midiAccess) {
    /** @type {Array.<MIDIInput>} */
    var inputs  = [];

    /** @type {Array.<MIDIOutput>} */
    var outputs = [];

    if (Object.prototype.toString.call(midiAccess) === '[object Function]') {
        // Legacy Chrome
        inputs  = midiAccess.inputs();
        outputs = midiAccess.outputs();
    } else {
        // Chrome 39 and later
        var inputIterator  = midiAccess.inputs.values();
        var outputIterator = midiAccess.outputs.values();

        for (var i = inputIterator.next(); !i.done; i = inputIterator.next()) {
            inputs.push(i.value);
        }

        for (var o = outputIterator.next(); !o.done; o = outputIterator.next()) {
            outputs.push(o.value);
        }
    }

    if (inputs.length > 0) {
        /**
         * @param {MIDIMessageEvent} event
         */
        inputs[0].onmidimessage = function(event) {
            // do something ....
        };
    }
};

/**
 * @param {DOMException} error
 */
var errorCallback = function(error) {
    // do something ....
};

navigator.requestMIDIAccess({sysex : true}).then(successCallback, errorCallback);

MIDIMessageEventインスタンスには, dataプロパティとreceivedTimeプロパティが定義されています.

receivedTimeプロパティはイベントが発生したときのタイムスタンプを格納しています. イベントオブジェクトに定義されているtimeStampプロパティよりも精度が高いので, タイムスタンプを利用する場合には, receivedTimeプロパティを利用するようにしましょう.

そして, 受信したMIDIメッセージはこのdataプロパティに格納されています. dataプロパティはUint8Array型の配列です.

MIDIメッセージの種類は多く, そのすべてを解説するのは難しいので, このセクションでは, 最低限の解説として, 音を発生させる, 音を停止するためのMIDIメッセージの処理について解説します.

音を発生させる (ノートオン), 音を停止する (ノートオフ) 場合, dataプロパティは要素数が3のUint8Array型の配列となっています.

表4 - 2 - a. ノートオンの場合のdataプロパティ
IndexDescription
09n hex (0xf0とのマスク値)
1ノートナンバー (0 〜 127)
2ベロシティ (0 〜 127)
表4 - 2 - b. ノートオフの場合のdataプロパティ
IndexDescription
08n hex (0xf0とのマスク値)
1ノートナンバー (0 〜 127)
2ベロシティ (0 〜 127)

第1バイト (インデックス0) を除けば, ノートオンもノートオフも同じ意味のメッセージが格納されています.

ノートナンバーとは, 簡単に表現すればどの鍵盤が押されたのか, あるいは, 離されたのかを表す値で, 0から127までの値をとります. 値が大きいほど, ピッチが高くなります. ちなみに, ピアノ88鍵に対応するノートナンバーの範囲は, 21 〜 108です.

ベロシティとは, 簡単に表現すれば鍵盤を弾くときの強弱を表す値で, 0から127までの値をとります. 値が大きいほど, 音が強くなります.

ちなみに, ほとんどのMIDIメッセージは最大で3バイトです (例外は, システムエクスクルーシブメッセージです).

表4 - 2 - c. MIDIメッセージの例
1st byte2nd byte3rd byte
8n hex (ノートオフ)ノートナンバーベロシティ
9n hex (ノートオン)ノートナンバーベロシティ
An hex (ポリフォニック キープレッシャー)ノートナンバープレッシャー値
Bn hex (コントロールチェンジ)コントロールナンバーコントロール値
Cn hex (プログラムチェンジ)プログラムナンバー使用しない
Dn hex (チャンネルプレッシャー)プレッシャー値使用しない
En hex (ピッチベンド)ピッチベンド値MSBピッチベンド値LSB
F0 hex (システムエクスクルーシブ開始)F7 hexを受信すると終了する
F7 hex (システムエクスクルーシブの終了)第2, 第3バイトをもたない
F8 hex (MIDIクロック)使用しない使用しない

音を発生させる, 音を停止する場合の実際のコードは以下のようになるでしょう.

サンプルコード 04


/**
 * @param {MIDIAccess} midiAccess
 */
var successCallback = function(midiAccess) {
    /** @type {Array.<MIDIInput>} */
    var inputs  = [];

    /** @type {Array.<MIDIOutput>} */
    var outputs = [];

    if (Object.prototype.toString.call(midiAccess) === '[object Function]') {
        // Legacy Chrome
        inputs  = midiAccess.inputs();
        outputs = midiAccess.outputs();
    } else {
        // Chrome 39 and later
        var inputIterator  = midiAccess.inputs.values();
        var outputIterator = midiAccess.outputs.values();

        for (var i = inputIterator.next(); !i.done; i = inputIterator.next()) {
            inputs.push(i.value);
        }

        for (var o = outputIterator.next(); !o.done; o = outputIterator.next()) {
            outputs.push(o.value);
        }
    }

    if (inputs.length > 0) {
        /**
         * @param {MIDIMessageEvent} event
         */
        inputs[0].onmidimessage = function(event) {
            switch (event.data[0] & 0xf0) {
                case 0x90 :
                    noteOn(event.data[1], event.data[2]);
                    break;
                case 0x80 :
                    noteOff(event.data[1], event.data[3]);
                    break;
                default :
                    break;
            }
        };
    }
};

/**
 * @param {DOMException} error
 */
var errorCallback = function(error) {
    // do something ....
};

/**
 * @param {number} noteNumber
 * @param {number} velocity
 */
var noteOn = function(noteNumber, velocity) {
    // Start sound by Web Audio API
};

/**
 * @param {number} noteNumber
 * @param {number} velocity
 */
var noteOff = function(noteNumber, velocity) {
    // Stop sound by Web Audio API
};

navigator.requestMIDIAccess({sysex : true}).then(successCallback, errorCallback);

noteOn / noteOff関数の処理は, Web MIDI APIではなく, Web Audio APIの領域です. 引数に, ノートナンバーとベロシティを受け取っているので, OscillatorNodeのfrequencyプロパティやGainNodeのgainプロパティを設定することができるでしょう. また, それ以外にも, ベロシティに応じたオートワウなどのエフェクターの実装も考えられます.

サンプルコード 05


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// for the instance of OscillatorNode
var oscillator = null

// for legacy browsers
context.createGain = context.createGain || context.createGainNode;

// Create the instance of GainNode
var gain = context.createGain();

var isStop = true;

/**
 * @param {MIDIAccess} midiAccess
 */
var successCallback = function(midiAccess) {
    /** @type {Array.<MIDIInput>} */
    var inputs  = [];

    /** @type {Array.<MIDIOutput>} */
    var outputs = [];

    if (Object.prototype.toString.call(midiAccess) === '[object Function]') {
        // Legacy Chrome
        inputs  = midiAccess.inputs();
        outputs = midiAccess.outputs();
    } else {
        // Chrome 39 and later
        var inputIterator  = midiAccess.inputs.values();
        var outputIterator = midiAccess.outputs.values();

        for (var i = inputIterator.next(); !i.done; i = inputIterator.next()) {
            inputs.push(i.value);
        }

        for (var o = outputIterator.next(); !o.done; o = outputIterator.next()) {
            outputs.push(o.value);
        }
    }

    if (inputs.length > 0) {
        /**
         * @param {MIDIMessageEvent} event
         */
        inputs[0].onmidimessage = function(event) {
            switch (event.data[0] & 0xf0) {
                case 0x90 :
                    noteOn(event.data[1], event.data[2]);
                    break;
                case 0x80 :
                    noteOff(event.data[1], event.data[3]);
                    break;
                default :
                    break;
            }
        };
    }
};

/**
 * @param {DOMException} error
 */
var errorCallback = function(error) {
    // do something ....
};

/**
 * @param {number} noteNumber
 * @param {number} velocity
 */
var noteOn = function(noteNumber, velocity) {
    if (!isStop) {
        oscillator.stop(0);
    }

    var FREQUENCY_RATIO    = Math.pow(2, (1 / 12));  // about 1.059463
    var MIN_A              = 27.5;
    var NOTE_NUMBER_OFFSET = 21;
    var MAX_VELOCITY       = 127;

    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();

    // for legacy browsers
    oscillator.start = oscillator.start || oscillator.noteOn;
    oscillator.stop  = oscillator.stop  || oscillator.noteOff;

    // Set parameters
    oscillator.frequency.value = MIN_A * Math.pow(FREQUENCY_RATIO, (noteNumber - NOTE_NUMBER_OFFSET));
    gain.gain.value            = velocity / MAX_VELOCITY;

    // OscillatorNode (Input) -> GainNode (Volume) -> AudioDestinationNode (Output)
    oscillator.connect(gain);
    gain.connect(context.destination);

    // Start sound
    oscillator.start(0);

    isStop = false;
};

/**
 * @param {number} noteNumber
 * @param {number} velocity
 */
var noteOff = function(noteNumber, velocity) {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    isStop = true;
};

navigator.requestMIDIAccess({sysex : true}).then(successCallback, errorCallback);

デモ 04

MIDIメッセージの送信

MIDIメッセージを送信するためには, MIDIOutputインスタンスのsendメソッドを利用します. このメソッドは, 2つの引数をとり, 第1引数にはMIDIメッセージを格納した数値配列, 第2引数にはMIDIメッセージを送信する時刻 (タイムスタンプ) を指定します. 第2引数は省略可能で, その場合は即時にMIDIメッセージが送信されます.

サンプルコード 06


/**
 * @param {MIDIAccess} midiAccess
 */
var successCallback = function(midiAccess) {
    /** @type {Array.<MIDIInput>} */
    var inputs  = [];

    /** @type {Array.<MIDIOutput>} */
    var outputs = [];

    if (Object.prototype.toString.call(midiAccess) === '[object Function]') {
        // Legacy Chrome
        inputs  = midiAccess.inputs();
        outputs = midiAccess.outputs();
    } else {
        // Chrome 39 and later
        var inputIterator  = midiAccess.inputs.values();
        var outputIterator = midiAccess.outputs.values();

        for (var i = inputIterator.next(); !i.done; i = inputIterator.next()) {
            inputs.push(i.value);
        }

        for (var o = outputIterator.next(); !o.done; o = outputIterator.next()) {
            outputs.push(o.value);
        }
    }

    if (outputs.length > 0) {
        // note on
        outputs[0].send([0x90, 0x45, 0x3f], (window.performance.now() + 0));
    }
};

/**
 * @param {DOMException} error
 */
var errorCallback = function(error) {
    // do something ....
};

navigator.requestMIDIAccess({sysex : true}).then(successCallback, errorCallback);

デモ 05