USB-MIDIなデバイスをRaspberryPiでつなぐ
この記事は Raspberry Pi Advent Calendar 2021 18日目の記事です*1。
昨日の担当はあっきぃさんでした。明日の担当は未定ですね。って書いてたら埋まってた。
背景
どこのご家庭にも以下のようなデバイスが余っていることと思います。
- 流行った時に買ってはみたけど全然使っていない Raspberry Pi
- 憧れて買ってみたけど完全に持て余しているハードウェアMIDI音源
- PC経由しないと使えないのが面倒になったUSB接続のMIDIキーボードとかウインドシンセ
おや……この Raspberry Pi (たぶん1B)にはUSBポートがふたつありますね……こいつで音源とコントローラを接続できるのでは??
やってみる
なんのことはない、こういうのは先駆者がいるわけです。
やってることも超簡単。Raspbian入れてaconnectでつなぐだけです。あまりにも簡単すぎる。名前で指定できるからポート変更にも強い。
$ aconnect EWI-USB SonicCell
これを /etc/rc.local
にでも突っこんでおけば、接続した状態でラズパイの電源を入れるだけで準備完了。
完。
どうせなのでもうちょっと便利にする
遊んでいるうちに色々気になってきました。
- 接続が終わったのかどうか、鳴らしてみないとわからない
- これについては接続できたところで音でも鳴らせばよさそう?ちょうど音源つながってるし。
- 新しいコントローラーを使う時にはスクリプトの修正が必要
aconnect -i -l
で入力ポートを列挙できるので、存在する適当な入力デバイスをつないでもよさそう
- 起動時にしか接続を試行しないので、後からコントローラーだけつなぎ変えると動かない
- udev あたりで接続を検知すればよさそう?
この辺全部盛りこんだスクリプトはこんな感じになりました。
#! /bin/bash ACONNECT_OUT=SonicCell AMIDI_OUT=$(amidi -l | grep -F "${ACONNECT_OUT}" | grep -Eo -m1 "hw:[^ ]+") ACONNECT_IN=$(aconnect -l -o | grep -Fv "'${ACONNECT_OUT}'" | grep -Fm1 "card=" | sed -E "s/^client [^:]+: '(.+)' .+$/\1/") NOTES_CONNECT=(45 49 4C) NOTES_ERROR=(45 45 45) NOTES_DISCONNECT=(45 45 45) function play_notes() { for nn in $*; do amidi -p $AMIDI_OUT -S 90${nn}78 sleep 0.01 amidi -p $AMIDI_OUT -S 80${nn}78 done } function reset_controller() { # 7Bh(オールノートオフ) 79h(リセットオールコントローラー) amidi -p $AMIDI_OUT -S B07B00B07900 } if [ -z "$AMIDI_OUT" ]; then echo "$0: no output ($ACONNECT_OUT)" exit 1 fi if [ -n "$ACONNECT_IN" ]; then echo "$0: connecting $ACONNECT_IN -> $ACONNECT_OUT" aconnect -x reset_controller aconnect "$ACONNECT_IN" "$ACONNECT_OUT" if [ $? -eq 0 ]; then # 音を鳴らすために一旦切断する(?) aconnect -x play_notes 45 49 4C aconnect "$ACONNECT_IN" "$ACONNECT_OUT" else play_notes 45 45 45 fi else echo "$0: no input found" aconnect -x play_notes 4C 49 45 fi
音を鳴らす
ALSAでMIDI OUTポートから音を鳴らすのは、この辺が簡単そうです。
- aplaymidi でSMFファイルを再生する
例:aplaymidi -p 24:0 ff_fanfare.mid
- amidi で直接MIDIメッセージを流す
例:for nn in {45,49,4C}; do amidi -p hw:2,0,0 -S 90${nn}7F; sleep 0.125; amidi -p hw:2,0,0 -S 80${nn}7F; done
……なんでポートの指定方法違うの。
接続するたびに勝利のファンファーレが鳴るというのも楽しそうではありますが、ここはWindowsのデバイス接続音を真似て、ドミソの3音で適当にしました。
# 接続時 for nn in {45,49,4C}; do amidi -p $AMIDI_PORT -S 90${nn}78; sleep 0.01; amidi -p $AMIDI_PORT -S 80${nn}78; done # 切断時(正確には、入力ポートが見つからなかった時) for nn in {4C,49,45}; do amidi -p $AMIDI_PORT -S 90${nn}78; sleep 0.01; amidi -p $AMIDI_PORT -S 80${nn}78; done # エラー時 for nn in {45,45,45}; do amidi -p $AMIDI_PORT -S 90${nn}78; sleep 0.01; amidi -p $AMIDI_PORT -S 80${nn}78; done
MIDIメッセージについては https://www.g200kg.com/jp/docs/tech/midi.html とかを参考に頑張ります。
なお aconnect でつないでいる出力ポートには amidi でメッセージを送ることができません。そういえばMIDIは出力ポートを共有できない子でしたね……。仕方ないので、接続に成功したら一旦切断して音を鳴らすことにします(???)。
デバイスを自動選択する
aconnect -l -i
で見つかったデバイスから aconnect -l -o
で見つかったデバイスに流せばいいんでしょ?簡単じゃん。
pi@raspberrypi:~ $ aconnect -l -i client 0: 'System' [type=kernel] 0 'Timer ' 1 'Announce ' client 14: 'Midi Through' [type=kernel] 0 'Midi Through Port-0' client 24: 'SonicCell' [type=kernel,card=2] 0 'SonicCell MIDI 1' 1 'SonicCell MIDI 2' client 28: 'EWI-USB' [type=kernel,card=3] 0 'EWI-USB MIDI 1 ' pi@raspberrypi:~ $ aconnect -l -o client 14: 'Midi Through' [type=kernel] 0 'Midi Through Port-0' client 24: 'SonicCell' [type=kernel,card=2] 0 'SonicCell MIDI 1' 1 'SonicCell MIDI 2' client 28: 'EWI-USB' [type=kernel,card=3] 0 'EWI-USB MIDI 1 '
見分けがつかん……!
仕方ないので少し妥協して、OUT側のデバイスだけ固定して、IN側のデバイスはOUTに使うデバイス以外から自動選択、ということにしました。
udevで自動接続
なにを基準にしようかと考えながら udevadm monitor -p
を眺めてみると、 SUBSYSTEM=snd_seq
といういい感じのがいますね。
UDEV [7822.779437] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/sound/card3/seq-midi-3-0 (snd_seq) ACTION=add DEVPATH=/devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/sound/card3/seq-midi-3-0 SUBSYSTEM=snd_seq SEQNUM=1496 USEC_INITIALIZED=7822778562
冒頭のスクリプトが /usr/local/bin/update-aconnect.sh
に置かれているとして、 /etc/udev/rules.d/10-usbmidi.rules
あたりにこんなファイルを生やします。
ACTION=="add", SUBSYSTEM=="snd_seq", RUN+="/usr/local/bin/update-aconnect.sh" ACTION=="remove", SUBSYSTEM=="snd_seq", RUN+="/usr/local/bin/update-aconnect.sh"
あとは sudo udevadm control --reload
で再読み込みして、抜き差しして音が鳴れば成功です。
再接続で色々リセットする
ここまで作って気づいたんですが、単純に aconnect で接続しなおすだけだと、色々な状態が残ったままになっています。発音中のノートだったり、変更されたCCだったり。コントローラーをつなぎ変えた時はそれらをリセットしてくれた方がいいような気がしてきました*2。
というわけで、毎回オールノートオフとリセットオールコントローラーを流しておきましょう。
余談: 電源問題
ところでラズパイを動かすにも電源がいるわけですが、おや……この音源、なにやらUSB端子をもうひとつ持っていますね? ……試しにつないでみましょうか。 動いていますね。
この子の給電能力がどれぐらいかわかりませんが、とりあえず予定していたMIDIコントローラーをつないでも動いている*3ので、いいんじゃないでしょうか。多分。
最終的に、音源用のAC電源があれば一式動くようになりました。なかなかいい感じ。
PC不要なら演奏するのかって?それはまた別の話よ。