🎶

Jetson Orinで外部USB-MIDI音源を使う簡単な方法

に公開

Jetson Orinで色々実験しています。Jetson Orinの概要とセットアップに関しては以下記事参照ください。

https://zenn.dev/karaage0703/articles/04ca258a89a50e

今回は、MIDI再生を試そうとしたところハマったりしたので記事を書きます。外部MIDI音源(具体的にはUSB-MIDI EDIROL UM-1 EX経由でKANTAN Play(かんぷれ))を再生しようとしたら以下のようなエラーに遭遇しました。

_rtmidi.SystemError: MidiInAlsa::initialize: error creating ALSA sequencer client object.

以前Jetson NnanoでMIDIを使おうとしたときに、カーネルビルドをした悲しい過去があるので「またカーネルビルドが必要なのかな?」と思いつつ調べていると、(主に生成AIの力で)外部USB-MIDI音源なら簡単に使えることが判明しました。

MIDI接続の2つの方法と従来の問題

MIDIデバイスを鳴らす場合、通常ALSAシーケンサーを使います。今回は、ALSAシーケンサを使わず、直接アクセスする方法をとっています。

Pythonアプリ → ALSAシーケンサー → MIDIデバイス

よくあるエラーの正体

JetsonでMIDI関連のPythonライブラリ(rtmidi、mido等)を使おうとすると、以下のエラーが発生します:

ALSA lib seq_hw.c:466:(snd_seq_hw_open) open /dev/snd/seq failed: No such file or directory
_rtmidi.SystemError: MidiInAlsa::initialize: error creating ALSA sequencer client object.

このエラーの原因:

  • rtmidimidoデフォルトでALSAシーケンサーを使おうとする
  • でもJetsonにはsnd-seqモジュール(ALSAシーケンサー)が入ってない
  • だから「ALSAシーケンサーが見つからない」エラーが出る

Jetsonの現状確認

# シーケンサーモジュールを確認
$ lsmod | grep seq
nvvrs_pseq_rtc         16384  0  # ←NVIDIA関連のみ
nvidia_vrs_pseq        16384  0

# /dev/snd/seq デバイスも存在しない
$ ls -la /dev/snd/seq
ls: cannot access '/dev/snd/seq': No such file or directory

確かに、MIDI用のシーケンサーモジュールは入っていません。

解決策:直接アクセス

USB-MIDIデバイスの接続確認

USB-MIDIインターフェースを接続すると、ファイルとして認識されます:

# USB接続確認
$ lsusb
Bus 001 Device 008: ID 0582:009d Roland Corp. EDIROL UM-1  # ←認識されている

# ALSAカードとして登録されているか確認
$ cat /proc/asound/cards
 0 [UM1            ]: USB-Audio - UM-1
                      EDIROL UM-1 at usb-3610000.usb-2.2, full speed

# MIDIデバイスファイルの確認
$ ls -la /dev/snd/ | grep midi
crw-rw----+  1 root audio 116,  2  615 12:02 midiC0D0  # ←重要!

/dev/snd/midiC0D0が作成されていれば、準備完了です。

MacとJetsonの両方に対応したスクリプト

MIDIデバイスファイルを直接指定する以下のようなコードで音を出すことができました。

"""
MCPサーバーを使わずに直接MIDI信号を連続送信するテスト
Mac (mido) と Jetson (raw MIDI) の両方に対応
"""

import time
import platform
import os


class UniversalMIDI:
    def __init__(self):
        self.is_jetson = self._detect_jetson()
        self.midi_device = None

    def _detect_jetson(self):
        """Jetson環境かどうかを判定"""
        return platform.machine().startswith("aarch64") and os.path.exists("/dev/snd/midiC0D0")

    def get_output_names(self):
        """利用可能なMIDI出力ポート名を取得"""
        if self.is_jetson:
            if os.path.exists("/dev/snd/midiC0D0"):
                return ["/dev/snd/midiC0D0 (UM-1)"]
            else:
                return []
        else:
            import mido

            return mido.get_output_names()

    def open_output(self, port_name):
        """MIDI出力ポートを開く"""
        if self.is_jetson:
            self.midi_device = open("/dev/snd/midiC0D0", "wb")
            return self
        else:
            import mido

            self.midi_device = mido.open_output(port_name)
            return self.midi_device

    def send_message(self, msg_type, channel=0, note=None, velocity=None):
        """MIDIメッセージを送信"""
        if self.is_jetson:
            if msg_type == "note_on":
                midi_bytes = bytes([0x90 + channel, note, velocity])
            elif msg_type == "note_off":
                midi_bytes = bytes([0x80 + channel, note, velocity])
            else:
                return

            self.midi_device.write(midi_bytes)
            self.midi_device.flush()
        else:
            import mido

            msg = mido.Message(msg_type, channel=channel, note=note, velocity=velocity)
            self.midi_device.send(msg)

    def close(self):
        """ポートを閉じる"""
        if self.midi_device:
            self.midi_device.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()


def test_direct_midi_rapid_fire():
    """遅延なしでMIDI信号を連続送信"""
    try:
        midi = UniversalMIDI()

        # 利用可能な出力ポートを確認
        output_names = midi.get_output_names()
        print(f"利用可能なMIDI出力ポート: {output_names}")
        print(f"環境: {'Jetson' if midi.is_jetson else 'Mac/PC'}")

        if not output_names:
            print("MIDI出力ポートが見つかりません")
            return

        # 最初のポートを使用
        port_name = output_names[0]
        print(f"使用するポート: {port_name}")

        with midi.open_output(port_name):
            # テストシーケンス: C4, D4, E4, F4, G4
            notes = [60, 62, 64, 65, 67]

            print("\n=== 遅延なし連続送信テスト ===")
            for note in notes:
                # Note On
                if midi.is_jetson:
                    midi.send_message("note_on", channel=0, note=note, velocity=64)
                else:
                    midi.send_message("note_on", channel=0, note=note, velocity=64)
                print(f"Note {note} ON送信")

                # 極短時間待機(ハードウェアの処理時間)
                time.sleep(0.01)

                # Note Off
                if midi.is_jetson:
                    midi.send_message("note_off", channel=0, note=note, velocity=0)
                else:
                    midi.send_message("note_off", channel=0, note=note, velocity=0)
                print(f"Note {note} OFF送信")

                # 次のノートまでの最小間隔
                time.sleep(0.05)

            print("\n=== 高速BPMシミュレーション (BPM 240) ===")
            # BPM 240 = 250ms/beat
            beat_duration = 60.0 / 240
            note_duration = beat_duration * 0.3  # 75ms
            note_gap = beat_duration * 0.1  # 25ms

            print(f"ノート長: {note_duration * 1000:.1f}ms, 間隔: {note_gap * 1000:.1f}ms")

            for note in notes:
                if midi.is_jetson:
                    midi.send_message("note_on", channel=0, note=note, velocity=64)
                else:
                    midi.send_message("note_on", channel=0, note=note, velocity=64)
                print(f"Note {note} ON (高速)")

                time.sleep(note_duration)

                if midi.is_jetson:
                    midi.send_message("note_off", channel=0, note=note, velocity=0)
                else:
                    midi.send_message("note_off", channel=0, note=note, velocity=0)
                time.sleep(note_gap)

            print("\n=== 同時和音テスト ===")
            chord = [60, 64, 67]  # Cメジャー

            # 和音ON
            for note in chord:
                if midi.is_jetson:
                    midi.send_message("note_on", channel=0, note=note, velocity=64)
                else:
                    midi.send_message("note_on", channel=0, note=note, velocity=64)
                print(f"Chord note {note} ON")

            time.sleep(1.0)  # 1秒間和音を鳴らす

            # 和音OFF
            for note in chord:
                if midi.is_jetson:
                    midi.send_message("note_off", channel=0, note=note, velocity=0)
                else:
                    midi.send_message("note_off", channel=0, note=note, velocity=0)
                print(f"Chord note {note} OFF")

            print("\nテスト完了")

    except Exception as e:
        print(f"エラー: {e}")
        import traceback

        traceback.print_exc()


if __name__ == "__main__":
    test_direct_midi_rapid_fire()

実行結果

実行結果です。

https://x.com/karaage0703/status/1934149740968739097

$ python3 test_direct_midi.py
利用可能なMIDI出力ポート: ['UM-1']
環境: Mac/PC
使用するポート: UM-1

=== 遅延なし連続送信テスト ===
Note 60 ON送信
Note 60 OFF送信
Note 62 ON送信
Note 62 OFF送信
Note 64 ON送信
Note 64 OFF送信
Note 65 ON送信
Note 65 OFF送信
Note 67 ON送信
Note 67 OFF送信

=== 高速BPMシミュレーション (BPM 240) ===
ノート長: 75.0ms, 間隔: 25.0ms
Note 60 ON (高速)
Note 62 ON (高速)
Note 64 ON (高速)
Note 65 ON (高速)
Note 67 ON (高速)

=== 同時和音テスト ===
Chord note 60 ON
Chord note 64 ON
Chord note 67 ON
Chord note 60 OFF
Chord note 64 OFF
Chord note 67 OFF

音が正常に出力されます!

MIDIデバイスファイル名の固定

このままだとUSB-MIDIデバイス、USBを接続するたびに異なるMIDIデバイスファイル名になるので不便です。udev使うことで固定できます。

https://zenn.dev/karaage0703/articles/d6759ea297dbf8

以下はrulesファイルの例です。

To Do(後で追記します)。

まとめ

Jetson Orinで外部USB-MIDI音源を使用する方法について紹介しました。Claudeには、わざわざカーネルビルドをするのは完全にアホと罵られましたが、MIDIデバイスが増えていくと、直接指定は結構大変なので、思い切ってカーネルビルドもよいのではないかなと思ったりしています。

この記事がJetsonでMIDI使いたい人の参考になれば幸いです。

余談

AIに完全にアホよばわりされながら、試行錯誤してました。

参考リンク

関連記事

https://qiita.com/karaage0703/items/9bef6aeec9ad24f647c6

https://protopedia.net/prototype/6790

Discussion