Webからハードを操作する環境を素早くつくりたい!
Webエンジニアです!
普段仕事でプロトタイピングすることも多く、雑にWebからハードを操作するシステムを試作することも多いです。
今回紹介するのは、つい先日のお話です。
下記のような要件のシステム(プロトタイプ)を作りました。
- 操作用のPCやスマホから別の場所にあるPCとそのPCに接続されたハードを制御する
- ハードが接続されたPCはWindows PCの上で動作させる想定だがOSはLinuxにするかもしれない。さらに開発はMacで行いたい
- PCとハードの接続にはUSBケーブルを用いる
- ハードは制御コマンドに基づいて動く。コマンド体系はいくつかのコマンド+ON/OFF程度で単純
- 数台作れれば良い。さくっと明日までにプロトタイプを1台つくれ。的なスピード感(実際は2-3日猶予がある)
まー、よくある話ですよね。
速度優先のプロトタイピングなので、下記のように作りました。
No. | 機能 | 実装方法 |
---|---|---|
1 | 操作用PC用アプリ | Webアプリケーション。UIを提供。普通にHTML/CSS/JSで書く |
2 | ハードウエア | USB-MIDIで制御コマンドを受信後ハードを制御する |
3 | HTTP API | HTTPリクエストはnode.js+expressで処理する |
4 | MIDI Client | APIが叩かれたらMIDIメッセージに変換してデバイスに送信する |
最終的には、4. の実装方法が少し上記図から変わりました。
1. 操作用PC用アプリ
これは普通のWebアプリです。UIを提供してボタン等を押すとAPIを呼び出します。
UIも単純なのでHTML/CSS/JSで普通に書くだけでしたので、説明は割愛。
2. ハードウエア
ハードウエアのIF部分をUSB MIDIに対応させるため、Arduino環境で開発可能なマイコンを利用しました。
ArduinoでUSB MIDIデバイスを作るというのも、豊富なOSSのおかげで簡単です。
主要なマイコンとライブラリの組み合わせを書いておきます。
No. | マイコン | ライブラリ | 参考情報 |
---|---|---|---|
1 | Arduino UNO | Moco LUFA | 導入方法 |
2 | Arduino micro/Arduino Leonardo | MIDIUSB | 使い方 |
3 | Raspberry Pi Pico/Seeed XIAO RP2040 | Adafruit Tiny USB | ←/examples にMIDIの例がある |
4 | ESP32 | --- | USB機能が無いのでUSB-MIDIデバイスには向かない。Bluetooth MIDI Deviceであれば作れる |
5 | Arduino Pro mini/Arduino Nano | --- | USB機能が無いのでUSB-MIDIデバイスには向かない |
プロジェクトでは、上記2と3を利用しました。
ボード入手できれば(ATmega32u4の開発ボードであれば概ね同じように使えます)、一番簡単なのはたぶん 2. ですが、2022年12月現在 半導体不足や円安の影響でATmegaの価格が高騰しており、互換ボードの入手性もかなり悪くなっています。多少環境にクセはありますが、現時点で入手性の良いRP2040搭載ボードにも保険で対応しておくことにしました。
MIDIメッセージで伝えられたコマンドに基づいて、ハードウエア(例えばサーボモーターなどのアクチュエータ)を制御することになりますが、ここでは詳細は割愛します。
3. HTTP API
最近は FastAPI に押され気味な気もしますが、慣れてるから!という理由で、node.js + express を採用しました。こちらも特に書くことはないので、リンクだけ置いておきます。
4. MIDI Client(node.js から MIDIを呼ぶ方法)
node.js から MIDI を扱うライブラリはいくつかありますが、今回実は採用しませんでした。
その理由を共有するのが本記事の目的です。
早くいろいろ治れば嬉しいな!
4-1. node-midi
node.js の npm モジュールで一番最初に見つけたのがコレです。
RtMidiというクロスプラットフォームMIDI実装のnode.jsラッパーです。
OSX / Windows / Linux に対応していて素晴らしいのですが、ちょっと依存環境が多くインストールになかなかに苦労します。
https://www.npmjs.com/package/midi
にさらりと記載されている依存関係は下記の通りです。(そのまま引用)
OSX
- Some version of Xcode (or Command Line Tools)
- Python (for node-gyp)
Windows
- Microsoft Visual C++ (the Express edition works fine)
- Python (for node-gyp)
Linux
- A C++ compiler
- You must have installed and configured ALSA. Without it this module will NOT build.
- Install the libasound2-dev package.
- Python (for node-gyp)
開発環境だけに入れれば良いわけではなく、実行環境でもこれらの環境を作る必要があります。
npm i midi
一発で環境構築できないのは辛いので、Windows環境でのインストールに手こずった段階で、今回は見送りました。
(これ普通にLinuxでも苦労するだろ。やってないけど。というのもあり。)
ようするに、各プラットフォーム用のビルド環境が必要なので Winodwsの場合は
npm install --global --production windows-build-tools
でビルドツールを事前に入れておけばOKのようです。
4-2. webmidi
まさかの node-midi 不採用でもう一度 npm を探して次に見つけたのがこちら webmidi
です。
各種ブラウザとnode.jsもマルチプラットフォームに対応しています。
- GNU/Linux
- macOS
- Windows
- Raspberry Pi
ブラウザの対応状況からブラウザ版はWeb MIDI APIのラッパーかな?と予想しますが、node.js 版は下記記載の通り、JZZに依存しているようです。
Support for the Node.js environment has been made possible in large part by the good folks of Jazz-Soft via their JZZ module.
JZZがどうやら Web MIDI API の node.js 版 Polyfillのようです。なるほど。そっち使ってもいいな。
で、これを使おうとしたんですが、なぜか動かない!(Macbook Air 2021 M1 Ver.で開発しています)
JZZの方も動かない!
いろいろ調べたら原因はコレ。
var path='./bin/';
var v=process.versions.node.split('.');
if (v[0]<=10) path+='10_15/';
else if (v[0]<=11) path+='11_15/';
if(process.platform=="win32"&&process.arch=="ia32") path+='win32/jazz';
else if(process.platform=="win32"&&process.arch=="x64") path+='win64/jazz';
else if(process.platform=="darwin"&&process.arch=="x64") path+='macos64/jazz'; // <- ? arm版がない!
else if(process.platform=="linux"&&process.arch=="x64") path+='linux64/jazz';
else if(process.platform=="linux"&&process.arch=="arm") path+='linuxa7/jazz';
module.exports=require(path);
module.exports.package=require(__dirname + '/package.json');
JZZやwebmidiが依存してる jazz-midiがまだ Appleシリコンに対応しておらず、Appleシリコン版のバイナリがないようです。残念!
開発者の開発環境で動かないモノは非採用!ということで。
4-3. 最終手段!ブラウザを使う。
今回残念ながら、node-midi、webmidiやJZZを見送ることになりました。
大丈夫か? node.js!(少し不安に。。。)
あまりこの手は使いたくなかったのですが、非常に簡単に導入できて、クロスプラットフォームなMIDI環境があります。そう。それはChrome(と共通のコアを使ったブラウザ群)。
下記のように対応しました。
- WebSocketメッセージを受信したらWeb MIDI API を使ってMIDIメッセージを送信するWebページを作る。
- node.js の API 起動時に WebSocketサーバーも起動しつつ、その後 puppeteer を使い、1. の Webページを表示しておく。
- expressでHTTPリクエストが来たら、WebSocketで 1.のページにWebSocketメッセージを送る。
APIの先でWebページに連携という面白アーキテクチャですが、npm i
一発で環境構築できます。最高。
注意点 1. ヘッドレスモードでの Web MIDI API の制限
Web MIDI APIは Chromeのheadlessモードだとそのままだと動作しません。
puppeteer
を用いたヘッドレスブラウザ起動時に対策が必要です。
動かないコード
browser = await puppeteer.launch({headless: true, args: ['--no-sandbox']});
page = await browser.newPage();
await page.goto('http://yourdomain/sample/sample.html');
このコードは、navigator.requestMIDIAccess()
に失敗するようです。
下記のようにヘッドレスモードでなく普通にブラウザを起動すると動作します。
browser = await puppeteer.launch({headless: false, args: ['--no-sandbox']});
page = await browser.newPage();
await page.goto('http://yourdomain/sample/sample.html');
が、ブラウザが常に起動/表示されているのはかっこ悪いです。
MIDIが動かないのはヘッドレスモードでの動作を意図的に制限していることが原因とわかりました。
ドメインにpermissionをつけてあげれば動くようです。
改良したコードは下記のようになりました。
動くコード
browser = await puppeteer.launch({headless: true, args: ['--no-sandbox']});
await browser.defaultBrowserContext().overridePermissions('http://yourdomain', ['midi']); // <-追加
page = await browser.newPage();
await page.goto('http://yourdomain/sample/sample.html');
注意点 2. Windowsは 複数のアプリから同時にVirtual MIDIポートがオープンできない
これ、ユースケースによっては結構致命的だと思うんですけど、Windowsの場合そういうもののようです。
例えば、Chromeで Web MIDI API を使うサイトを開いてる状態で、他のブラウザでも Web MIDI API を利用しようとしたら失敗するようです。
RtMidi
などブラウザ以外のMIDIポートオープンでも同様の問題が発生するので、OSの仕様? なんでしょうか?
MidiOutWinMM::openPort: error creating Windows MM MIDI output port.
上記のようなエラーメッセージ(上記は RtMidiの例)を見つけたら、他にMIDIポートをつかんでるアプリケーションが無いか?チェックしてみる必要がありそうです。
まとめ
MIDIを使ってWebからハードを操作する環境を作りました。
ただ、今回最初想定した実装とは少し違う結果になりました。
- MIDIを使えば手軽にUSBを使ったPC→ハードウエア制御が実現できる(ただし簡単なコマンド体系のみ。大容量データの伝送等には向かないので、ユースケース次第)
- 2022/11/30現在、MIDIのクロスプラットフォーム環境で一番手軽なのはブラウザ(chrome)
- ブラウザはサーバーのようなヘッドレス環境でも利用可能
おまけ
Web MIDI API を使ったWebアプリを超手抜きで作ってみたい人向けラッパー(ブロードキャストがデフォルト動作という曲者)