CCMapper(RtMidi/python3)

CCMapper(RtMidi/python3)

2022/12/21
Macの記述を追加した。

2022/12/18++
初版

概要

以前の記事でpython3のCCMapperは以前作成したが、そのときは、ゲーム用ライブラリpygameを使用した。これを実績のあるRtMidiのpythonラッパーライブラリに切り替える。
以前の記事:re.corder/ElefueをCCMapper経由で外部音源(Aria/Windows)と接続する(WIDI_Bud_Pro使用) 

本機能は、[WIDI Bud Pro]経由でMIDIデータをリアルタイムでCC#2またはCC#11を受信して、CC#を任意のもの(複数、AT、PPを含む)に変更して音源に送信する。Pitch_Bendは,そのまま接続されている音源に送られる。

準備

1.linuxの場合
仮想MIDIデバイスとして、既存の[Midi Through port]を利用する。

MIDI信号の流れとしては以下のようになる:

[wind_controler(re.corder/Elfue etc)]→[WIDI Bud Pro]→(CCMapper)→[Midi Through Port]→ [PC音源]

2.windowsの場合
仮想MIDIデバイスとして、[loopMIDI]を利用する。

MIDI信号の流れとしては以下のようになる:

[wind_controler(re.corder/Elfue etc)]→[WIDI Bud Pro]→(CCMapper)→[loopMIDI]→ [PC音源]

3.Macの場合
仮想MIDIデバイスとして、IACドライバを設定する。名前はWindowsに合わせて「loopMIDI」とする。 参照:MacのAudio MIDI設定でMIDI情報をアプリケーション間で送信する

MIDI信号の流れとしては以下のようになる:

[wind_controler(re.corder/Elfue etc)]→(CCMapper)→[loopMIDI(IAC)]→ [PC音源]

準備

1.linuxの場合
以下を実行してライブラリをインストールする:


sudo apt install python3-rtmidi

pythonでありがちなpipでないので注意のこと。

2.windowsの場合
以下を実行してインストールする:
# scoopで必要なコマンドがインストール済みの前提

基本的には以下の方法であるが、エラーが出たので一部変更している:  
https://spotlightkid.github.io/python-rtmidi/install-windows.html  

python -m pip install -U virtualenv
python.exe -m pip install --upgrade pip

python -m virtualenv rtmidi
rtmidi\Scripts\activate
ここで、仮想環境rtmidiにはいる

pip install -U pip setuptools

pip download python-rtmidi
tar -xzf python-rtmidi-1.4.9.tar.gz
cd python-rtmidi-1.4.9
python setup.py install

以上でライブラリのインストールは完了する
以下は、動作確認になる:

(rtmidi) PS C:\Users\xxxx> python
Python 3.10.1 (tags/v3.10.1:2cd268a, Dec  6 2021, 19:10:37) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import rtmidi
>>> rtmidi.API_WINDOWS_MM in rtmidi.get_compiled_api()
True
>>> midiout = rtmidi.MidiOut()
>>> midiout.get_ports()
['VirtualMIDISynth #1 0', 'Microsoft GS Wavetable Synth 1', 'loopMIDI Port 1 2']
上のデバイスポート名は、動作環境に依存する
>>>

仮想環境に該当ライブラリをインストールしたので、 rtmidiライブラリを使用する場合は、 以下を実行して仮想環境に入ること。

.\rtmidi\Scripts\activate

3.Macの場合
以下を実行してインストールする:
# brewでpython3がインストール済みの前提

pip install python-rtmidi

実際の実行にあたり、python3は、コマンド名として、Macではpython3になっているので、 以下のように実行すること
(これに対して、linux/windowsの場合、python)

python3 CCMapper_RtMidi.py

CCMapper

CCMapperのプログラムとしては以下になる:

CCMapper_RtMidi.py

#!/usr/bin/env python
#
# CCMapper_RtMidi.py
#
# written by: xshige
# 2022/12/19

import sys
import time

from rtmidi.midiconstants import (ALL_NOTES_OFF, ALL_SOUND_OFF, BALANCE, BANK_SELECT_LSB,
                                  BANK_SELECT_MSB, BREATH_CONTROLLER, CHANNEL_PRESSURE,
                                  CHANNEL_VOLUME, CONTROL_CHANGE, DATA_ENTRY_LSB, DATA_ENTRY_MSB,
                                  END_OF_EXCLUSIVE, EXPRESSION_CONTROLLER, FOOT_CONTROLLER,
                                  LOCAL_CONTROL, MIDI_TIME_CODE, MODULATION, NOTE_OFF, NOTE_ON,
                                  NRPN_LSB, NRPN_MSB, PAN, PITCH_BEND, POLY_PRESSURE,
                                  PROGRAM_CHANGE, RESET_ALL_CONTROLLERS, RPN_LSB, RPN_MSB,
                                  SONG_POSITION_POINTER, SONG_SELECT, TIMING_CLOCK)

from rtmidi.midiutil import open_midioutput
from rtmidi.midiutil import open_midiinput

def midiin_callback(event, data=None):
    global curNote
    message, deltatime = event
    if message[0] & 0xF0 == NOTE_ON:
        status, note, velocity = message
        channel = (status & 0xF) + 1
        print("NoteOn note:%d velocity:%d" % (note, velocity))
        curNote = note
        midiout.send_message(message)
    elif message[0] & 0xF0 == NOTE_OFF:
        status, note, velocity = message
        channel = (status & 0xF) + 1
        print("NoteOff note:%d velocity:%d" % (note, velocity))
        #curNote = note
        midiout.send_message(message)
        # PATCH All Sound Off
        #msg2 = [ALL_SOUND_OFF + channel - 1]
        #msg2.append(120)
        #msg2.append(0)
        #midiout.send_message(msg2)           
    elif message[0] & 0xF0 == CONTROL_CHANGE:
        status, control, value = message
        channel = (status & 0xF) + 1
        print("CONTROL_CHANGE control:%d value:%d" % (control, value))
        if ((control == 2)|(control == 11)):
            message[1] = 1
            midiout.send_message(message)
            message[1] = 2
            midiout.send_message(message)
            message[1] = 7
            midiout.send_message(message)
            message[1] = 11
            midiout.send_message(message)
            message[1] = 74
            midiout.send_message(message)
            message[1] = 26
            midiout.send_message(message)
            # AT
            msg2 = [CHANNEL_PRESSURE + channel - 1]
            msg2.append(value & 0x7F)
            midiout.send_message(msg2)
            # PP
            msg2 = [POLY_PRESSURE + channel - 1]
            msg2.append(curNote & 0x7F)
            msg2.append(value & 0x7F)
            midiout.send_message(msg2)
            if (value == 0):
                # PATCH All Sound Off
                msg2 = [ALL_SOUND_OFF + channel - 1]
                msg2.append(120)
                msg2.append(0)
                midiout.send_message(msg2)
                return           
            return
    elif message[0] & 0xF0 == CHANNEL_PRESSURE:
        value = message[0]
        channel = (status & 0xF) + 1
        print("AT(CHANNEL_PRESSURE) value:%d" % (value))
    elif message[0] & 0xF0 == POLY_PRESSURE:
        status, note, value = message
        channel = (status & 0xF) + 1
        print("PP(POLY_PRESSURE) note:%d value:%d" % (note, value))
    elif message[0] & 0xF0 == PITCH_BEND:
        value = message[0]
        print("PITCH_BEND value:%d" % (value))
        midiout.send_message(message)
        return
    else:
         print("********** unexpected message **********")

#---------------------------------------------------------

def main(args=None):
    
    global midiout
    #global curNote

    curNote = 0

    midiout, outport = open_midioutput(0) # Midi Through
    print("output port: '%s'" % outport)

    #midiin, port_name = open_midiinput(args[0] if args else None)
    midiin, inport = open_midiinput(1)
    print("input port: '%s'" % inport)
    midiin.set_callback(midiin_callback)

    try:
        while True:
            time.sleep(0.01)
    except KeyboardInterrupt:
        print("Bye.")
        pass
    finally:
        midiin.close_port()
        del midiin
        midiout.close_port()
        del midiout 


if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv[1:]) or 0)

#=====================================-

CCMapperを起動したら、次に音源を立ち上げて入力MIDIデバイスを
linuxの場合、「Midi Through port」(windowsの場合は、「loopMIDI」)に設定する。

ここで、wind_controlerで吹くと音が出る。

なお、色々な音源で必要と思われるCC#を有効にしているので、linuxでは以下の音源が、そのまま利用できる。

  1. Surge XT
    SURGE XT - Free & Open Source Hybrid Synthesizer
    Surge XT User Manual
  2. Vital
    VITAL - Spectral warping wavetable synth
    Get Vital(フリー版もある)

port probe

実際になにが接続されているか確認する場合、以下のプログラムを使用する:

probe_ports.py

#!/usr/bin/env python
#
# probe_ports.py
#
"""Shows how to probe for available MIDI input and output ports."""

from rtmidi import (API_LINUX_ALSA, API_MACOSX_CORE, API_RTMIDI_DUMMY,
                    API_UNIX_JACK, API_WINDOWS_MM, MidiIn, MidiOut,
                    get_compiled_api)

try:
    input = raw_input
except NameError:
    # Python 3
    StandardError = Exception

apis = {
    API_MACOSX_CORE: "macOS (OS X) CoreMIDI",
    API_LINUX_ALSA: "Linux ALSA",
    API_UNIX_JACK: "Jack Client",
    API_WINDOWS_MM: "Windows MultiMedia",
    API_RTMIDI_DUMMY: "RtMidi Dummy"
}

available_apis = get_compiled_api()

for api, api_name in sorted(apis.items()):
    if api in available_apis:
        try:
            reply = input("Probe ports using the %s API? (Y/n) " % api_name)
            if reply.strip().lower() not in ['', 'y', 'yes']:
                continue
        except (KeyboardInterrupt, EOFError):
            print('')
            break

        for name, class_ in (("input", MidiIn), ("output", MidiOut)):
            try:
                midi = class_(api)
                ports = midi.get_ports()
            except StandardError as exc:
                print("Could not probe MIDI %s ports: %s" % (name, exc))
                continue

            if not ports:
                print("No MIDI %s ports found." % name)
            else:
                print("Available MIDI %s ports:\n" % name)

                for port, name in enumerate(ports):
                    print("[%i] %s" % (port, name))

            print('')
            del midi

windowsでの動作

linuxと実行環境が異なるので、probe_ports.pyを実行して、実際のデバイスポートを確認してソースを修正する。
outportが’loopMIDI’,inportが’WIDI Bud Pro’になるように変更する:

変更例:


    midiout, outport = open_midioutput(2) # loopMIDI for windows
    #midiout, outport = open_midioutput(0) # Midi Through for linux
    print("output port: '%s'" % outport)

    #midiin, port_name = open_midiinput(args[0] if args else None)
    midiin, inport = open_midiinput(1) # WIDI Bud Pro for linux/windows
    print("input port: '%s'" % inport)
    midiin.set_callback(midiin_callback)

注意:環境依存なので、このソースのようになるとは限らない。

Macでの動作

linuxと実行環境が異なるので、probe_ports.pyを実行して、実際のデバイスポート(の番号)を確認してソースを修正する。
outportが’loopMIDI’,inportが’接続しているコントローラ名’になるように変更する。

参考情報

os関連:
brewをインストールする/MIDI関連 - Macことはじめ
scoopをインストール - windows10にplatformioをインストールする(scoop版)

python-rtmidi関連:
https://github.com/SpotlightKid/python-rtmidi

loopMIDI関連:
loopMIDI
loopMIDIでつなぐ

WIDI_Bud_Pro関連:
WIDI Bud Pro
WIDI Bud Pro 技術情報

PC音源関連:
[WS音源探訪02] Vital Free Patch for Windsynth つくってみました
wind_controler向けのパッチがあり役に立つ
[WS音源探訪01] Vital
【無料】VitalAudioのWaveTableシンセVitalの紹介
Macことはじめ/仮想MIDIデバイスの設定,Bluetooth_MIDIデバイスの接続
Respiro(VST3)を使ってみる
Lyrihorn 2(VST3)を使ってみる
EVI-NER(VST3)を使ってみる
re.corder/Elefueに外部音源(Aria/Windows)を接続する(WIDI_Bud_Pro経由)
EWI5000をソフト音源(IFW)と接続する

re.corder関連:
owner’s manual re.corder
re.corder Downloads
re.corder Frequently Asked Questions

MIDI関連:
現時点、最強のBluetooth MIDIかも!? 各種BLE-MIDI機器と自動でペアリングしてくれるWIDI Masterがスゴイ!
Midi View

ASIO関連:
asio4all - ASIOドライバーのないオーディオインターフェイスをASIO対応にできるソフト

Aria関連:
EWI MASTER BOOK CD付教則完全ガイド【改訂版】のp100-p119の音色の設定方法がある

以上

Go to Toplevel