見出し画像

midoで3バイトを超えるコントロールチェンジをするハック


  • midoのバリデーションで、3バイトを超えるコントロールチェンジが扱えないようだ

  • 一方、SAM2695では、3バイトを超えるコントロールチェンジを特殊メッセージとして受け付けるようだ

  • midoとpython-rtmidiを編集して、3バイトを超えるコントロールチェンジが送信できるようにしてみる


以下のつづき


環境

  • Windows

  • Python 3.12

    • mido 1.3.3

    • python-rtmidi 1.5.8


全体の流れ

  • python-rtmidiのバリデーションを緩くする

  • RtMidi(python-rtmidiに同梱)のコントロールチェンジ時のバッファ確保処理の変更

  • midoにおいて、bytes型(unsinged char型の配列)でデータを送信できるようなメソッドを追加

python-rtmidi

ソースの取得

  • 以下のURLから、Source Distribution python_rtmidi-1.5.8.tar.gz をダウンロード

    • GitHubからcloneしてもよいと思う

https://pypi.org/project/python-rtmidi/
  • ファイルを展開

  • コマンドプロンプトなどで、展開したディレクトリを開く

バージョン番号の変更

  • meson.build を開いてバージョン番号を変更する

    • 正式なリリースバージョンと勘違いように、特殊なバージョンであることわかるようにする

  • 好きな文字列を指定したかったが、有効なバージョン名である必要があるようだ

  • 以下のように、dev1を付与した

    • 本当は、mod_by_me など付けたかったが…

version: '1.5.8.dev1'

python-rtmidi側のソースの編集

  • 本来は、「src/_rtmidi.pyx」を編集して、Cythonで、C++のファイルに変換するのが、まっとうな手段だと思われるが、

  • 「以下のようなコマンドで筆者がCythonによって変換したC++ファイル」と「変換済みのC++ファイル」で、400行くらい不一致が出た(Cythonのバージョンによるもの?)

cython -3 --cplus -o _rtmidi_mod.cpp _rtmidi.pyx
  • 精査するのが手間なので、C++のコードを直接、編集してバリデーションを取り除く、という暴挙に出る

  • _rtmidi.cpp を開く

    • GitHubにはコミットされていなそう

  • 以下のエラー時の文字列で、検索すればすぐに該当箇所がみつかる

    • Python(Cython)のコードがC++のコメントで残る、親切設計

'message' longer than 3 bytes but does not
  • 正直、何をしているか、読めていないが、

  • バリデーションをして、エラー処理をしているであろうコードを、ごっそりコメントアウトしてみる

    • 「空データ」と「0xF0からはじまらない3バイトを超えるメッセージ」をエラーとする処理

// __pyx_t_6 = __Pyx_PyObject_Call(__pyx_builtin_ValueError, __pyx_tuple__7, NULL); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 1103, __pyx_L1_error)
// __Pyx_GOTREF(__pyx_t_6);
// __Pyx_Raise(__pyx_t_6, 0, 0, 0);
// __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0;
// __PYX_ERR(0, 1103, __pyx_L1_error)
elif msg_v.size() > 3 and msg_v.at(0) != 0xF0:
    raise ValueError("'message' longer than 3 bytes but does not "
                     "start with 0xF0.")

RtMidi側のソースの編集

  • RtMidi.cppを開き、void MidiOutWinMM :: sendMessage()メソッドを変更する(今回は、WindowsでWinMMのみが対象)

  • コントロールチェンジは、3バイト前提で、固定長の処理になっている

  • かなり強引ではあるが、システムエクスクルーシブのときの処理を流用してみる

    • エラーメッセージはシステムエクスクルーシブ前提になっているが…

  • MidiOutWinMM :: sendMessage()にある、以下のif文を変更して、3バイトを超えるコントロールチェンジでも可変長のメッセージを送信するようにする(変更前は、システムエクスクルーシブのときのみ可変長の処理になる)

変更前

if ( message[0] == 0xF0 ) { // Sysex message

変更後

if ( message[0] == 0xF0 || ((message[0] & 0xF0) == 0xB0 && nBytes > 3)  ) { // Sysex message or Long Control Chnage

ビルドの準備

  • ビルドに必要なパッケージのインストール

pip install build installer

ビルド

  • ドキュメントでお勧めされている PEP 517 build method でやってみる

    • ビルドシステムは、Mesonのようなので、慣れている方は、Mesonも選択できる

  • 以下のようにコマンドを実行すると何も問題なくビルドでき、

  • distフォルダに、whlファイルが生成された

python -m build --wheel

インストール

  • ドキュメントにある以下の方法だと

python -m installer dist/*.whl
  • PermissionErrorが出てしまう

  • おそらく、pyenvを使って環境を作っていなく、インストール先が正しくなく

  • ファイルを書き込めないと思われる
    C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python という存在しないフォルダに書き込もうとしているようだ

PermissionError: [WinError 5] アクセスが拒否されました。
  • pipでwhlファイルをインストールしてみると、インストールできた

pip install python_rtmidi-1.5.8.dev1-cp312-cp312-win_amd64.whl

インストールの確認

  • パッケージのリストを表示してみると、

  • 無事に、dev1と付与された(3バイトを超えるコントロールチェンジができる) python-rtmidi がインストールされた

> pip list

Package            Version
------------------ -----------
mido               1.3.3
python-rtmidi      1.5.8.dev1

mido

  • Pythonのライブラリフォルダを開く

  • WindowsアプリのPythonの場合、以下のようなパスになるはず

    • pyenvの場合は異なる

%userprofile%\AppData\Local\Packages\PythonSoftwareFoundation.Python{VERSION}.{ID}\LocalCache\local-packages\Python{VERSION}\site-packages\mido\backends\
  • rtmidi.py を開く

  • def send(self, msg) の後などに、def send_bytes(self, bytes) を追加する

  • message型ではなく、bytes型でメッセージを送信するメソッド

def send(self, msg):
    """Send a message on the port."""
    with self._send_lock:
        self._rt.send_message(msg.bytes())

def send_bytes(self, bytes):
    """Send bytes on the port."""
    with self._send_lock:
        self._rt.send_message(bytes)

実験

  • 以下のmidoで音を出すコードで実験

  • コントロールチェンジ前は、通常の音色で

  • コントロールチェンジ後は、フィルタが適用された音になった

import mido
import time

port = mido.open_output("MIDI Output Poart name")

# reset
port.send(mido.Message.from_bytes([0xFF]))

# Before
msg = mido.Message('note_on', note=60)
port.send(msg)
time.sleep(1.0)

# cutoff frequency
port.send_bytes([0xb0, 0x63, 0x01, 0x62, 0x20, 0x06, 0x00])

# resonance
port.send_bytes([0xb0, 0x63, 0x01, 0x62, 0x21, 0x06, 0x00])

# After
msg = mido.Message('note_on', note=60)
port.send(msg)
time.sleep(1.0)




疑問点

if ( nBytes > 3 ) { // Sysex message
  • データ長が3バイトを超えると、可変長の処理がされるように変更されている

  • midoは最新とは異なるRtMidiを使っている(rtmidi @ f6ea40d

    • mido用のブランチがあり、そちらは3バイトを超えるコントロールチェンジはできないようになっている




  • 需要がないと(自分もそれほど求めていないと)感じつつも、ついつい時間をかけて調査してしまった

  • とはいえ、色々、学べることはあった(試行錯誤してゴールする達成感を感じた)

  • そして、RtMidiの実装が、3バイトを超えるコントロールチェンジを許容するように変更されていることを考えると、まったく需要がないというわけではないのかもしれない



いいなと思ったら応援しよう!