うならぼ

申し訳程度のアフィリエイトとか広告とか解析とかは/aboutを参照

USB-MIDIなデバイスをRaspberryPiでつなぐ

この記事は Raspberry Pi Advent Calendar 2021 18日目の記事です*1

昨日の担当はあっきぃさんでした。明日の担当は未定ですね。って書いてたら埋まってた。

akkiesoft.hatenablog.jp

背景

どこのご家庭にも以下のようなデバイスが余っていることと思います。

おや……この 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

音を鳴らす

ALSAMIDI 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端子をもうひとつ持っていますね?

USB MEMORY 端子
……試しにつないでみましょうか。
動く…!
動いていますね。

この子の給電能力がどれぐらいかわかりませんが、とりあえず予定していたMIDIコントローラーをつないでも動いている*3ので、いいんじゃないでしょうか。多分。

最終的に、音源用のAC電源があれば一式動くようになりました。なかなかいい感じ。

PC不要なら演奏するのかって?それはまた別の話よ。

*1:Qiitaの Raspberry Pi Advent Calendar 2021 とは別です。見分けがつかないな?

*2:EWIがExpressionを絞ったままで、接続音が鳴らなかったことで気づいたやつ

*3:実はもっと手軽な方法として iPhone × Lightning - USBカメラアダプタ を試していたものの、200mA要求するEWI-USBは使えなかった。噂によると件のアダプタは100mAまでっぽい?