MIDI Remoteを使って、自分のMIDIデバイスをCubaseに対応させる

この記事は [ここのえ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/99no_exit) Day 15の記事です。

:::note warn
### 注意
この記事はDTMの音楽的な話ではなく、バリバリにコーディングする話です。
JavaScriptに対してある程度知識がある人を対象にしています。
:::

# Introduction

下手の横好きで、趣味DTMをしているここのえです。

メインDAWとしてCubaseを使っているのですが、Cubase 12から **MIDI Remote** という機能が追加されました。

こんな名前ですがBluetoothでリモートで動かすとかではなく、簡単に言えば「**Cubaseでサードパーティ製のMIDIコントローラを使えるようにするAPI**」です。

https://steinberg.help/cubase_ai/v12/ja/cubase_nuendo/topics/midi_remote/midi_remote_c.html

MIDI自体は共通規格ですが、ミキサー関係であれば `Mackie Control Protocol` などに対応しているDAWが多いものの、[**AKAI FIRE**](http://akai-pro.jp/fire/) や [**Ableton Push**](https://www.ableton.com/ja/push/) のようにコントロールが特定のDAWに依存しているハードウェアも多々あります。

自分も [**Launchpad Pro Mk3**](https://novationmusic.com/products/launchpad-pro-mk3) を使っていますが、例に漏れずコイツも **Logic** と **Live** 専用のデバイスです。

再生/録音ボタンが使えなかったりするのが特に不便で今まで困っていたのですが、MIDI Remoteのおかげで対応させられそうなので、それを目標に実装してみます。

:::note info
MIDI Remoteのコーディングに関する情報は本当に皆無なので、この記事がDTMer兼プログラマーの皆さんの一助になれれば幸いです。
:::

# CubaseのMIDI Remoteとは？

まず最初に、MIDI Remoteについて詳細を掘っていきます。

MIDI Remoteの機能は大きく分けて3つに分けられます。

## MIDIコントロールサーフィスのエディタ

CubaseのGUIエディタ上で、直感的にデバイスの配置を定義することができます。後述の `MIDI マッピングアシスタント` と併用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/8f2c1343-79a4-c1a1-cba3-17592febfc32.png)

https://steinberg.help/cubase_artist/v12/ja/cubase_nuendo/topics/midi_remote/midi_remote_controller_surface_editor_r.html

## MIDI Remote マッピングアシスタント

`MIDIコントロールサーフィスのエディタ` で設定したボタンに対して、Cubaseの特定の機能を割り当てるために使うアシスタントです。

一般的なデバイスのユースケースであれば、ノーコードでこれだけ設定すれば問題ないでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/e8039f33-98b7-95bc-73a7-4fe71cc6d4e6.png)

https://steinberg.help/cubase_ai/v12/ja/cubase_nuendo/topics/midi_remote/midi_remote_mapping_pages_r.html

## MIDI Remote API

今回の本題です。

![MIDI Remote API全体像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/28c0acb4-aed0-c3e0-24b1-e30596a32be1.png)

MIDI Remote APIは、前述のようなCubaseの MIDI Remote の機能をスクリプトによって制御することができるAPIです。

`MIDI コントロールサーフィスのエディタ`のデバイスのボタン配置の定義や、`MIDI Remote マッピングアシスタント` の各MIDI操作とCubaseの操作の対応などが、スクリプト上で可能になります。

またスクリプト上でデバイスの動作を定義するため、MIDI Remote マッピングアシスタントで出来ないような**複雑な挙動を定義**したり、逆にCubase側からMIDIを送り、**デバイスのLEDを光らせたりする**ことが可能です。

<br/>

MIDI Remote APIの使用言語は `Javascript` で、`ES5` までの記法が使えます。

そうです、 **ES5までしか使えません**。 `let, const`, `() => {}`, `class/extends`, `デフォルト引数`, `...array`, これらは **全部諦めてください**。 

:::note info
今回は本題から外れてしまうので避けますが、ES5を生で書くのは流石に面倒すぎるので、Babelとかでトランスパイルする記事をそのうち書こうと思います……
:::

# 実装

実際にMIDI Remote APIを使って、LaunchpadからCubaseが操作できるようなコードを実装していきます。

今回の最終目標は、以下の機能の実装を目指します。

- 再生ボタン
    - 再生と停止
    - 再生中は緑色に点灯させ、停止中は消灯する
- 録音ボタン
    - 録音モードのON/OFF切り替え
    - 録音モードだったら赤色に点灯、そうでなければ消灯する

基本的にLaunchpad用のコードで解説を進めていきますが、しょせんMIDIを弄っているだけです。
CCやノート番号だけ読み替えて頂ければどんなデバイスでも対応できます。

:::note warn
### Launchpad Pro Mk3でテストする人へ
今回はシンプルにするため、Launchpad Pro Mk3のプログラマーモード（外部から制御するモード）を対象に実装します。必要があれば、詳細はマニュアルを確認してください。

https://downloads.novationmusic.com/novation/launchpad-mk3/launchpad-pro-mk3-0
:::



## 開発環境の準備

まず開発環境を構築します。
VSCodeの場合は特に気にせず、公式ドキュメント通りやれば大丈夫です。

https://steinbergmedia.github.io/midiremote_api_doc/getting-started

今回はWebstormで開発環境を構築してみます。
`スタジオ` -> `MIDI Remote マネージャー` を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/4394dab1-c215-ac15-7dca-daf327c4bd53.png)

MIDI Remoteの画面が出てきたら、上部ツールバーを右クリックし、`スクリプトツール` にチェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/49f9012e-93ce-9c00-cff4-77ba24b4ae45.png)

フォルダのマークをクリックして、スクリプトの格納フォルダを開きます。このパスをWebStormでOpenします。
![スクリーンショット 2023-11-16 152948.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/53acba58-9921-791b-3d37-b733df0ec9a4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/8b65148d-ceae-a143-2aba-5e5ef4697b77.png)

以下のフォルダおよび、ファイル名のルールでスクリプトを作成します。
```bash
# 規則
Local/<vendor>/<device>/<vendor>_<device>.js

# Launchpadの例
Local/Novation/Launchpad_Pro_Mk3/Novation_Launchpad_Pro_Mk3.js
```

WebStormはプロジェクト内のJSDocを勝手に読んでくれるので、スクリプトを開くと既に補完が動くはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/c8948418-dea5-361e-c7d4-c20e6f46473d.png)

このままだと`const`やアロー関数をうっかり使ってしまいそうなので、JavascriptのLanguage versionの指定も行っておきます。
WebStormの場合、`Setting -> Languages & Frameworks -> Javascript`にあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/bb9be5cd-b156-8b37-926d-03340e8a94bf.png)

## デバイスのセットアップ

コーディングを始める前に、先述のスクリプトツールの左から二番目、コンソールのボタンを押して **MIDI Remote スクリプトコンソール** を開きます。
ここでCubase側にスクリプトをリロードさせたり、MIDIポートの識別名を確認したりできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/a3b70ec5-37d4-9ad5-304f-36644afc3f11.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/2f5abad9-4746-cc55-98f9-1770c75b5157.png)

:::note info
### Tips: デバッグ方法
普通に`console.log()`が使えます。MIDI Remote スクリプトコンソールに表示されます。
```js
console.log(newValue.toString())
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/268f7ad4-2585-ce71-84bb-c51141bdb181.png)

:::

デバイスのセットアップを行うコードを書きます。

```js
// MIDI Remote API
var api = require('midiremote_api_v1')

// デバイスのオブジェクトを作成
// makeDeviceDriver('メーカー', 'デバイス名', 'スクリプトの作成者')
var device = api.makeDeviceDriver("Novation", "Launchpad Pro Mk3", "Kokonoe")

// MIDIポートの参照を拾う
var midiIn = device.mPorts.makeMidiInput()
var midiOut = device.mPorts.makeMidiOutput()

// MIDIデバイスの識別名と対応させる
// NOTE: デバイスによってはWindowsとMacで名称が異なる場合アリ。要注意！
device.makeDetectionUnit().detectPortPair(midiIn, midiOut)
    .expectInputNameEquals('LPProMK3 MIDI')
    .expectOutputNameEquals('LPProMK3 MIDI')
```

## レイアウトの定義

サーフィスのレイアウトを定義します。先程のセットアップのコードに引き続いて書いていきます。

```js
// ボタンの定義
// makeButton(x座標, y座標, width, height)
var play = device.mSurface.makeButton(0, 0, 1, 1)
var rec = device.mSurface.makeButton(1, 0, 1, 1)

// サーフィスとMIDI信号のバインド
play.mSurfaceValue.mMidiBinding
    .setInputPort(midiIn).bindToControlChange(0, 20)

rec.mSurfaceValue.mMidiBinding
    .setInputPort(midiIn).bindToControlChange(0, 10)
```

`device.mSurface`に対して、各ボタンを作成していきます。
`makeButton` 以外にも `makeKnob` など、`MIDI コントロールサーフィスのエディタ` で設定できるサーフィスと同様のものがあります。

`mSurfaceValue.mMidiBinding`以下を使って、サーフィス要素とMIDI信号のバインドを行えます。

今回は再生ボタンが `CC 20`、録音ボタンが `CC 10`なのでそれぞれ割り当てています。

:::note alert
### LEDを取り扱うときの注意

公式の [**Simple Device**](https://steinbergmedia.github.io/midiremote_api_doc/examples/simpledevice/) のサンプルコードでは、`setOutputPort(midiOut)` も使って、MIDI OUTもバインドしています。
```js
play.mSurfaceValue.mMidiBinding
    .setInputPort(midiIn)
    .setOutputPort(midiOut)
    .bindToControlChange(0, 20)
```

MIDI OUTをバインドしてしまうと、ボタンが押されたときにCubaseがデータを勝手に送ってしまいます。

加えて`setOutputPort` のMIDI信号が `sendMidi()` メソッドで送信するMIDIを上書きしてしまうので、自分でLEDが制御できなくなってしまいます。
**LEDを使うときは、OUTはバインドしないようにしましょう**。
:::

## Cubaseへの操作割当、LED情報の送信

一番コアな部分の実装です。実際に操作を割り当てていきます。
加えて操作に変更があった時、MIDI信号を送ってLEDを光らせます。

```js
// マッピングページの作成
var page = device.mMapping.makePage("Default Page")

// ボタンとCubase操作のマッピング
page.makeValueBinding(play.mSurfaceValue, page.mHostAccess.mTransport.mValue.mStart).setTypeToggle()
page.makeValueBinding(rec.mSurfaceValue, page.mHostAccess.mTransport.mValue.mRecord).setTypeToggle()

// LEDの処理
play.mSurfaceValue.mOnProcessValueChange = function (context, newValue) {
    if(newValue === 1) {  // 再生中の時
        midiOut.sendMidi(context, [0xb0, 20, 25])
    } else {  // 停止中の時
        midiOut.sendMidi(context, [0xb0, 20, 0])
    }
}

rec.mSurfaceValue.mOnProcessValueChange = function (context, newValue) {
    if(newValue === 1) {  // 再生中の時
        midiOut.sendMidi(context, [0xb0, 10, 5])
    } else {  // 停止中の時
        midiOut.sendMidi(context, [0xb0, 10, 0])
    }
}
```

一つずつ見ていきます。

### マッピングページ

```js
// マッピングページの作成
var page = device.mMapping.makePage("Default Page")
```

MIDI Remoteにはマッピングページという概念があり、これを切り替えることによって各ボタンの挙動を変更することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/bf2eb64a-4adf-8d82-626b-4d497dfb0ca3.png)

裏を返せば、各ボタンの挙動はマッピングページをベースにしているため、マッピングページを作成しないとボタンに動作を設定できません。

このコードでは `MR_DeviceDriver.mMapping.makePage("ページ名")` でページを作成しています。

### ボタンとCubase操作のマッピング

```js
// makeValueBinding(サーフィスのvalue, Cubase操作のvalue)
page.makeValueBinding(play.mSurfaceValue, page.mHostAccess.mTransport.mValue.mStart).setTypeToggle()
```

`makeValueBinding`でボタンとCubaseの操作をバインドすることができます。
バインドされるとMIDI Remoteのサーフィス表示も自動的に変更されます。今回は再生・録音の2つのボタンを用意しているので、それぞれに対象のアイコンが表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/d375cb73-42f0-995b-b156-9e7287b2e802.png)

また再生・録音についてはON/OFFのトグル式なので、`.setTypeToggle()` を使って押すたびに切り替わるように設定します。

### LEDの処理 (MIDI信号の送信)

```js
// LEDの処理
// context: device
play.mSurfaceValue.mOnProcessValueChange = function (context, newValue) {
    if(newValue === 1) {  // 再生中の時
        midiOut.sendMidi(context, [0xb0, 20, 25])
    } else {  // 停止中の時
        midiOut.sendMidi(context, [0xb0, 20, 0])
    }
}
```

`mSurfaceValue.mOnProcessValueChange` に `function` を当てることで、ボタンにバインドされた値が変更されたときにcallbackが走るようになります。
第一引数には対象のデバイスの参照、第二引数に変更された後の値が返ってくるので、これを使って処理を行います。

上記の例では再生を割り当てていますが、再生時は`1`、停止時は`0` が返ってきます。フェーダーやパンの値をバインドしていれば、それに対応したnumber型の値が返ってきます。

今回はLEDを光らせるために、Launchpadの仕様に基づいてデータを送信しています。

:::note info
### `sendMidi()`の送信内容について
`sendMidi()` の第二引数ですが、そのまま生のMIDIメッセージを書いて送る必要があります。
第二引数のarray(3)がMIDIメッセージの第1, 第2, 第3バイトにあたります。

今回の例では、`channel 0`の`CC 20`に`25(緑色)`を送信しています。

- 第1バイト `0xb0` => `b` = CC, `0` MIDI Channel
- 第2バイト `20` => CC番号
- 第3バイト `25` => 対象のCCに送るValue

詳細はこちらのページが分かりやすいです。
https://www.g200kg.com/jp/docs/dic/controlchange.html
:::

<br/>
<br/>

# まとめ

MIDI Remote APIの情報についてはほとんどネット上に落ちておらず、基本的には公式のドキュメント頼りだったので、折角なので書いてみました。

Cubase 12以降しか使えないこともあり、対応したCubaseを持っている方が少ないという問題はありますが、コーディングしてCubaseを自由自在に操れるのはとても魅力的です。感覚として近いところだと、自作キーボードの実装やってる時と同じようなワクワク感があります。

最近発売された `Cubase 13` では大幅にUI・操作性の改善も入りました。プログラマーかつDTMerかつ`"Cubase": "^12.0"`なユーザに限られてしまいますが、ぜひ一度触ってみてください！

# おまけ

今回使ったコードのGistです。

https://gist.github.com/nonuplet/9079f343b9a5954842ba0df4a17deff8

# 参考

https://steinbergmedia.github.io/midiremote_api_doc/

https://www.jetbrains.com/help/webstorm/javascript-specific-guidelines.html#ws_js_choose_language_version

https://www.g200kg.com/jp/docs/dic/controlchange.html