ALSA C libraryのAPIを使用してMIDIの入出力を行うLinuxアプリがあるのだが、出力したMIDIをループバックさせて入力として読み込みたい――という話である。
macOSなら標準のIACドライバを、Windowsならサードパーティの仮想MIDIケーブル*1を使うパターンである。
今回のポイントは、ALSAのRawMidi interfaceのAPIを使用しているアプリである、という点だ。
ALSAのMIDI 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 YokeやHubi'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シーケンサを橋渡しするために開発されたと推測できるからだ。