mirror of https://github.com/probonopd/MiniDexed
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
163 lines
6.2 KiB
163 lines
6.2 KiB
2 weeks ago
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
# Send MIDI data to a MIDI device using the `python-rtmidi` library.
|
||
|
|
||
|
try:
|
||
|
import rtmidi
|
||
|
except ImportError:
|
||
|
print("Please install the python-rtmidi library: pip install python-rtmidi")
|
||
|
exit(1)
|
||
|
import time
|
||
|
import sys
|
||
|
import argparse
|
||
|
import os
|
||
|
import signal
|
||
|
import atexit
|
||
|
|
||
|
def signal_handler(sig, frame):
|
||
|
print("\nExiting...")
|
||
|
sys.exit(0)
|
||
|
signal.signal(signal.SIGINT, signal_handler)
|
||
|
|
||
|
def cleanup_all_notes_off():
|
||
|
try:
|
||
|
midiout = rtmidi.MidiOut()
|
||
|
ports = midiout.get_ports()
|
||
|
if ports:
|
||
|
midiout.open_port(0)
|
||
|
all_notes_off(midiout, verbose=True)
|
||
|
time.sleep(0.5)
|
||
|
midiout.close_port()
|
||
|
except Exception as e:
|
||
|
print(f"Cleanup error: {e}")
|
||
|
|
||
|
atexit.register(cleanup_all_notes_off)
|
||
|
|
||
|
# Ask the user which MIDI port to use
|
||
|
def get_midi_port(midiout):
|
||
|
print("Available MIDI output ports:")
|
||
|
ports = midiout.get_ports()
|
||
|
for i, port in enumerate(ports):
|
||
|
print(f"{i}: {port}")
|
||
|
while True:
|
||
|
try:
|
||
|
choice = int(input("Select a port by number: "))
|
||
|
if 0 <= choice < len(ports):
|
||
|
return choice
|
||
|
else:
|
||
|
print("Invalid choice. Please select a valid port number.")
|
||
|
except ValueError:
|
||
|
print("Invalid input. Please enter a number.")
|
||
|
|
||
|
def play_chord(midiout, notes, velocity=100, channel=0, delay=0.25, verbose=True):
|
||
|
print("Playing chord:", notes)
|
||
|
for note in notes:
|
||
|
midiout.send_message([0x90 | channel, note, velocity])
|
||
|
time.sleep(delay)
|
||
|
time.sleep(0.5)
|
||
|
for note in notes:
|
||
|
midiout.send_message([0x80 | channel, note, 0])
|
||
|
|
||
|
def send_sysex(midiout, msg, verbose=True, label=None):
|
||
|
midiout.send_message(msg)
|
||
|
if verbose:
|
||
|
if label:
|
||
|
print(f"Sent: {label} {msg}")
|
||
|
else:
|
||
|
print(f"Sent: {msg}")
|
||
|
|
||
|
def all_notes_off(midiout, verbose=True):
|
||
|
# Send All Notes Off (CC 123) and individual Note Off for all notes 0-127 on all 16 MIDI channels
|
||
|
for channel in range(16):
|
||
|
midiout.send_message([0xB0 | channel, 120, 0])
|
||
|
if verbose:
|
||
|
print(f"Sent: All Sound Off (CC120) on channel {channel+1}")
|
||
|
|
||
|
def send_modwheel(midiout, value, channel=0, verbose=True):
|
||
|
# Modulation wheel is CC 1
|
||
|
midiout.send_message([0xB0 | channel, 1, value])
|
||
|
if verbose:
|
||
|
print(f"Sent: ModWheel CC1 value {value} on channel {channel+1}")
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
parser = argparse.ArgumentParser(description="Send MIDI messages to a device.")
|
||
|
parser.add_argument("-p", "--port", type=str, help="MIDI output port name or number")
|
||
|
# Always verbose, so remove the verbose switch and set verbose to True
|
||
|
args = parser.parse_args()
|
||
|
verbose = True
|
||
|
|
||
|
midiout = rtmidi.MidiOut()
|
||
|
ports = midiout.get_ports()
|
||
|
|
||
|
if args.port is not None:
|
||
|
# Allow port to be specified by number or name
|
||
|
try:
|
||
|
port_index = int(args.port)
|
||
|
except ValueError:
|
||
|
# Try to find by name
|
||
|
port_index = next((i for i, p in enumerate(ports) if args.port in p), None)
|
||
|
if port_index is None:
|
||
|
print(f"Port '{args.port}' not found.")
|
||
|
sys.exit(1)
|
||
|
else:
|
||
|
port_index = get_midi_port(midiout)
|
||
|
|
||
|
if verbose:
|
||
|
print(f"Using MIDI output port: {ports[port_index]}")
|
||
|
|
||
|
try:
|
||
|
midiout.open_port(port_index)
|
||
|
chord_notes = [60, 64, 67]
|
||
|
# Send all notes off before starting the test program
|
||
|
all_notes_off(midiout, verbose=verbose)
|
||
|
while True:
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x10, 0x04, 0x05, 0x00, 0xF7], verbose, "Portamento Time 0")
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x10, 0x04, 0x05, 0x02, 0xF7], verbose, "Portamento Time 2")
|
||
|
time.sleep(1)
|
||
|
|
||
|
# Test Detune
|
||
|
detune_msgs = [
|
||
|
([0xF0, 0x43, 0x10, 0x04, 0x40, 0x2F, 0xF7], "Detune TG1"),
|
||
|
([0xF0, 0x43, 0x11, 0x04, 0x40, 0x37, 0xF7], "Detune TG2"),
|
||
|
([0xF0, 0x43, 0x12, 0x04, 0x40, 0x40, 0xF7], "Detune TG3"),
|
||
|
([0xF0, 0x43, 0x13, 0x04, 0x40, 0x48, 0xF7], "Detune TG4"),
|
||
|
]
|
||
|
for msg, label in detune_msgs:
|
||
|
send_sysex(midiout, msg, verbose, label)
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
for ch in range(0x10, 0x14):
|
||
|
send_sysex(midiout, [0xF0, 0x43, ch, 0x04, 0x40, 0x40, 0xF7], verbose, f"Detune TG{ch-0x0F} reset")
|
||
|
time.sleep(1)
|
||
|
|
||
|
# Test Poly and Mono modes
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x13, 0x04, 0x02, 0x00, 0xF7], verbose, "TG4 Poly")
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x13, 0x04, 0x02, 0x01, 0xF7], verbose, "TG4 Mono")
|
||
|
time.sleep(1)
|
||
|
|
||
|
# Test ModWheel sensitivity
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x13, 0x04, 0x09, 0x01, 0xF7], verbose, "TG4 ModWheel Sensitivity ON")
|
||
|
send_modwheel(midiout, 127, channel=0, verbose=verbose)
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
send_modwheel(midiout, 0, channel=0, verbose=verbose)
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x13, 0x04, 0x09, 0x00, 0xF7], verbose, "TG4 ModWheel Sensitivity OFF")
|
||
|
send_modwheel(midiout, 127, channel=0, verbose=verbose)
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
send_modwheel(midiout, 0, channel=0, verbose=verbose)
|
||
|
time.sleep(1)
|
||
|
|
||
|
# Disable all Tone Generators except TG1
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x13, 0x04, 0x08, 0x00, 0xF7], verbose, "Disable TG2, TG3, TG4")
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
# Enable all Tone Generators
|
||
|
send_sysex(midiout, [0xF0, 0x43, 0x13, 0x04, 0x08, 0x01, 0xF7], verbose, "Enable TG2, TG3, TG4")
|
||
|
play_chord(midiout, chord_notes, verbose=verbose)
|
||
|
time.sleep(1)
|
||
|
|
||
|
# Send all notes off after each test program loop
|
||
|
all_notes_off(midiout, verbose=verbose)
|
||
|
except Exception as e:
|
||
|
print(f"Error: {e}")
|
||
|
sys.exit(1)
|