ALSAでMIDIをループバックさせたい――ただしALSA RawMidi APIで!

ALSA C libraryのAPIを使用してMIDIの入出力を行うLinuxアプリがあるのだが、出力したMIDIをループバックさせて入力として読み込みたい――という話である。

macOSなら標準のIACドライバを、Windowsならサードパーティの仮想MIDIケーブル*1を使うパターンである。

今回のポイントは、ALSAのRawMidi interfaceのAPIを使用しているアプリである、という点だ。

ALSAMIDI APIは、RawMidi interfaceとSequencer interfaceの2種類が存在する。RawMidi interfaceはベタにMIDIバイスのポートを読み書きするためのOSS互換のAPIだ。一方のSequencer interfaceは高水準のAPIで、MIDIバイスだけでなくTimidity++のようなアプリケーション間でもMIDIの入出力を行えるようにデザインされている。そのため、MIDIシーケンサという抽象化されたオブジェクトのポートを読み書きするようになっている。

Linux上でのMIDIの取り扱いについての解説資料――例えばThe Linux MIDI-HOWTOとかLinuxビンボ道〜明日がALSA設定編とかArchLinuxのWikiに書かれているTimidity++の話題などは、Sequencer interfaceを使用しているアプリ向けの話が大半だ。

Sequencer interfaceを使用しているアプリなら、カーネルモジュールsnd-seq-dummyをロードすることで使用可能となるMidi Throughという仮想MIDIシーケンサを使用して、MIDIをループバックさせることができる。「Midi Through」という名前の通り、入力ポートに書き込まれたMIDIをそのまま出力ポートから読み出せるMIDIシーケンサで、macOSのIACドライバやWindowsの仮想MIDIケーブルと似たような要領で扱うことができる。

# snd-seq-dummyの組み込み方。
# 末尾の「ports=1」は、ループバック用のポートの数を指定する。
sudo modprobe snd-seq-dummy ports=1

UbuntuやRaspbian(Raspberry Pi)ではデフォルトで組み込まれているので、再度ロードする必要はない。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$ _

上記の例では、入力・出力ポートともに14:0を指定すればよい。

しかしRawMidi interfaceを使用しているアプリはMidi Throughを使えない。RawMidi interfaceで扱えるのはMIDIバイスだが、Midi Throughは仮想MIDIシーケンサだ。厄介なことに、RawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサは全く別の概念で、APIからして全く異なるのである。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # aplaymidi -lでは、Sequencer interfaceで扱うことが可能な、
$ # MIDIシーケンサの入力ポートが表示される。
$ aplaymidi -l
 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
$ # Midi Throughが見つかる。
$
$ # amidi -lでは、RawMidi interfaceで扱うことが可能な、
$ # MIDIデバイスの入力ポートが表示される。
$ amidi -l
Dir Device    Name
$ # Midi Throughはおろか、何のMIDIデバイスの入力ポートも見つからない。
$ _

ではどうすればよいか? カーネルモジュールsnd-virmidiを組み込んで仮想MIDIバイスを使用できるようにした上で、aconnect(1)で仮想MIDIバイスの入力ポートを出力ポートを接続すればよい。これで、仮想MIDIバイスの入力ポートに書き込んだMIDIコマンドを出力ポートから読み出すことが可能となる。

# snd-virmidiの組み込み方。
#
# 末尾の「index=1」は、仮想MIDIデバイスに付与するカード番号である
# (RawMidiのデバイスは「カード番号,デバイス番号,サブデバイス番号」で指定する仕組み)。
# カード番号は0から始まる通し番号で、
# システムが認識したMIDIデバイスごとに自動的に付与されている。
# 「amidi -l」などでシステムが認識しているMIDIデバイスを確認したうえで、
# それらと重複しない番号を割り当てること。
sudo modprobe snd-virmidi index=1

snd-virmidiを組み込むと、既定では4個の仮想MIDIバイスが使用可能となる。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # snd-virmidiをロードする前。
$ amidi -l
Dir Device    Name
$
$ # snd-virmidiをロード。
$ sudo modprobe snd-virmidi index=1
$
$ # snd-virmidiをロードした後。
$ amidi -l
Dir Device    Name
IO  hw:1,0    Virtual Raw MIDI (16 subdevices)
IO  hw:1,1    Virtual Raw MIDI (16 subdevices)
IO  hw:1,2    Virtual Raw MIDI (16 subdevices)
IO  hw:1,3    Virtual Raw MIDI (16 subdevices)
$ # 「index=1」を指定したのでhw:1に割り当てられている
$ # (もし「index=2」だったらhw:2,0〜hw:2,3が割り当てられただろう)。
$ # 「16 subdevices」なので、例えばhw:1,0,0〜hw:1,0,15が使用できる。
$ _

興味深いことに、snd-virmidiの仮想MIDIバイスは、RawMidi interfaceで扱えるMIDIバイスであると同時に、Sequencer interfaceで扱えるMIDIシーケンサとしても振る舞う。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$ _

上記の例では、RawMidi interfaceのデバイスhw:1,0がSequencer interfaceのポート20:0に、hw:1,1が21:0に――という具合に対応している。

仮想MIDIバイス自体はMIDIをループバックする機能を持たないが、aconnect(1)を使用してSequencer interfaceの入力ポートと出力ポートを連結させると、RawMidi interface側でも有効となる。

例えばaconnect(1)で入力ポート20:0を出力ポート20:0に連結すると:

aconnect 20:0 20:0

入力ポート20:0に書き込んだMIDIを出力ポート20:0から読み出せるだけでなく、デバイスhw:1,0の入力ポートに書き込んだMIDIをhw:1,0の出力ポートから読み出せるようになる*2

あとは、RawMidi interfaceのAPIを使用するアプリケーションにて、デバイスhw:1,0を使用すればよい。同じサブデバイス番号同士(例えば入出力ともにhw:1,0,0)にてMIDIがループバックされる。

*1:昔はMIDI YokeHubi's Loopback MIDI Driverを使っていたものだが、64bit Windowsを使うようになってからは縁遠くなった。最近はloopMIDIを使えばよいのだろうか?

*2:試してはいないが、おそらく入力ポート20:0に書き込んだMIDIをhw:1,0の出力ポートから読み出すことや、hw:1,0の入力ポートに書き込んだMIDIを出力ポート20:0から読み出すことも可能なはずだ。というのも、The Linux MIDI-HOWTOを読む限り、元々snd-virmidiはRawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサを橋渡しするために開発されたと推測できるからだ。