Made this in python to change programs with the plus and minus keys on a qwerty keyboard. The easiest way to use is run the linux x86 build on github that I packaged together with pyinstall so you dont need to manually deal with dependencies. Otherwise if your own win/mac heres the python.
#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk, messagebox
import mido
from pynput import keyboard
# ============================================================
# GENERAL MIDI INSTRUMENTS
# ============================================================
GM_INSTRUMENTS = [
"Acoustic Grand Piano",
"Bright Acoustic Piano",
"Electric Grand Piano",
"Honky-tonk Piano",
"Electric Piano 1",
"Electric Piano 2",
"Harpsichord",
"Clavinet",
"Celesta",
"Glockenspiel",
"Music Box",
"Vibraphone",
"Marimba",
"Xylophone",
"Tubular Bells",
"Dulcimer",
"Drawbar Organ",
"Percussive Organ",
"Rock Organ",
"Church Organ",
"Reed Organ",
"Accordion",
"Harmonica",
"Tango Accordion",
"Acoustic Guitar (nylon)",
"Acoustic Guitar (steel)",
"Electric Guitar (jazz)",
"Electric Guitar (clean)",
"Electric Guitar (muted)",
"Overdriven Guitar",
"Distortion Guitar",
"Guitar Harmonics",
"Acoustic Bass",
"Electric Bass (finger)",
"Electric Bass (pick)",
"Fretless Bass",
"Slap Bass 1",
"Slap Bass 2",
"Synth Bass 1",
"Synth Bass 2",
"Violin",
"Viola",
"Cello",
"Contrabass",
"Tremolo Strings",
"Pizzicato Strings",
"Orchestral Harp",
"Timpani",
"String Ensemble 1",
"String Ensemble 2",
"SynthStrings 1",
"SynthStrings 2",
"Choir Aahs",
"Voice Oohs",
"Synth Voice",
"Orchestra Hit",
"Trumpet",
"Trombone",
"Tuba",
"Muted Trumpet",
"French Horn",
"Brass Section",
"SynthBrass 1",
"SynthBrass 2",
"Soprano Sax",
"Alto Sax",
"Tenor Sax",
"Baritone Sax",
"Oboe",
"English Horn",
"Bassoon",
"Clarinet",
"Piccolo",
"Flute",
"Recorder",
"Pan Flute",
"Blown Bottle",
"Shakuhachi",
"Whistle",
"Ocarina",
"Lead 1 (square)",
"Lead 2 (sawtooth)",
"Lead 3 (calliope)",
"Lead 4 (chiff)",
"Lead 5 (charang)",
"Lead 6 (voice)",
"Lead 7 (fifths)",
"Lead 8 (bass + lead)",
"Pad 1 (new age)",
"Pad 2 (warm)",
"Pad 3 (polysynth)",
"Pad 4 (choir)",
"Pad 5 (bowed)",
"Pad 6 (metallic)",
"Pad 7 (halo)",
"Pad 8 (sweep)",
"FX 1 (rain)",
"FX 2 (soundtrack)",
"FX 3 (crystal)",
"FX 4 (atmosphere)",
"FX 5 (brightness)",
"FX 6 (goblins)",
"FX 7 (echoes)",
"FX 8 (sci-fi)",
"Sitar",
"Banjo",
"Shamisen",
"Koto",
"Kalimba",
"Bagpipe",
"Fiddle",
"Shanai",
"Tinkle Bell",
"Agogo",
"Steel Drums",
"Woodblock",
"Taiko Drum",
"Melodic Tom",
"Synth Drum",
"Reverse Cymbal",
"Guitar Fret Noise",
"Breath Noise",
"Seashore",
"Bird Tweet",
"Telephone Ring",
"Helicopter",
"Applause",
"Gunshot",
]
# ============================================================
# CHANNEL 10 DRUM KITS
# ============================================================
DRUM_KITS = {
1: "Standard Drum Kit",
9: "Room Drum Kit",
17: "Power Drum Kit",
25: "Electric Drum Kit",
26: "Rap TR808 Drums",
33: "Jazz Drum Kit",
41: "Brush Kit",
}
DRUM_PROGRAMS = list(DRUM_KITS.keys())
# ============================================================
# MIDI CC
# ============================================================
REVERB_CC = 91
# ============================================================
# APPLICATION
# ============================================================
class MidiProgramChanger:
def __init__(self, root):
self.root = root
self.root.title("MIDI Program Changer")
self.port = None
self.keyboard_listener = None
self.program = 1
self.channel = 1
self.reverb = 0
self.matrix_var = tk.IntVar(value=self.program)
self.reverb_var = tk.IntVar(value=self.reverb)
self.matrix_buttons = []
self.create_gui()
self.refresh_ports()
self.start_keyboard_listener()
self.root.protocol(
"WM_DELETE_WINDOW",
self.close
)
# ========================================================
# GUI
# ========================================================
def create_gui(self):
main = ttk.Frame(
self.root,
padding=10
)
main.pack(
fill="both",
expand=True
)
# ----------------------------------------------------
# MIDI OUTPUT
# ----------------------------------------------------
port_frame = ttk.LabelFrame(
main,
text="MIDI Output",
padding=8
)
port_frame.pack(
fill="x",
pady=(0, 8)
)
self.port_var = tk.StringVar()
self.port_combo = ttk.Combobox(
port_frame,
textvariable=self.port_var,
state="readonly",
width=55
)
self.port_combo.pack(
side="left",
fill="x",
expand=True
)
ttk.Button(
port_frame,
text="Refresh",
command=self.refresh_ports
).pack(
side="left",
padx=(5, 0)
)
ttk.Button(
port_frame,
text="Connect",
command=self.connect_port
).pack(
side="left",
padx=(5, 0)
)
# ----------------------------------------------------
# CHANNEL
# ----------------------------------------------------
channel_frame = ttk.Frame(main)
channel_frame.pack(
fill="x",
pady=(0, 8)
)
ttk.Label(
channel_frame,
text="MIDI Channel:"
).pack(side="left")
self.channel_var = tk.IntVar(value=1)
self.channel_spin = tk.Spinbox(
channel_frame,
from_=1,
to=16,
textvariable=self.channel_var,
width=5,
command=self.channel_changed
)
self.channel_spin.pack(
side="left",
padx=(5, 0)
)
self.channel_spin.bind(
"",
lambda event: self.channel_changed()
)
self.channel_spin.bind(
"",
lambda event: self.channel_changed()
)
# ----------------------------------------------------
# PROGRAM CONTROLS
# ----------------------------------------------------
control_frame = ttk.Frame(main)
control_frame.pack(
fill="x",
pady=(0, 8)
)
ttk.Button(
control_frame,
text="Previous",
command=self.previous_program
).pack(side="left")
self.program_var = tk.IntVar(value=1)
self.program_spin = tk.Spinbox(
control_frame,
from_=1,
to=128,
textvariable=self.program_var,
width=6,
command=self.program_spin_changed
)
self.program_spin.pack(
side="left",
padx=8
)
self.program_spin.bind(
"",
lambda event: self.program_spin_changed()
)
self.program_spin.bind(
"",
lambda event: self.program_spin_changed()
)
ttk.Button(
control_frame,
text="Next",
command=self.next_program
).pack(side="left")
# ----------------------------------------------------
# INSTRUMENT NAME
# ----------------------------------------------------
self.instrument_label = ttk.Label(
main,
text="1 Acoustic Grand Piano",
anchor="center",
font=("TkDefaultFont", 12, "bold")
)
self.instrument_label.pack(
fill="x",
pady=(0, 8)
)
# ----------------------------------------------------
# REVERB
# ----------------------------------------------------
reverb_frame = ttk.LabelFrame(
main,
text="Reverb (MIDI CC 91)",
padding=8
)
reverb_frame.pack(
fill="x",
pady=(0, 8)
)
self.reverb_knob = tk.Scale(
reverb_frame,
from_=0,
to=127,
orient="horizontal",
variable=self.reverb_var,
command=self.reverb_changed,
showvalue=True,
resolution=1,
length=300
)
self.reverb_knob.pack(
fill="x"
)
# ----------------------------------------------------
# SCROLLABLE PROGRAM MATRIX
# ----------------------------------------------------
matrix_frame = ttk.LabelFrame(
main,
text="Program",
padding=5
)
matrix_frame.pack(
fill="both",
expand=True
)
# Canvas
self.canvas = tk.Canvas(
matrix_frame,
highlightthickness=0
)
self.canvas.pack(
side="left",
fill="both",
expand=True
)
# Vertical scrollbar
scrollbar = ttk.Scrollbar(
matrix_frame,
orient="vertical",
command=self.canvas.yview
)
scrollbar.pack(
side="right",
fill="y"
)
self.canvas.configure(
yscrollcommand=scrollbar.set
)
# Frame inside canvas
self.matrix_inner = ttk.Frame(
self.canvas
)
self.canvas_window = self.canvas.create_window(
(0, 0),
window=self.matrix_inner,
anchor="nw"
)
# Update scroll region when frame changes
self.matrix_inner.bind(
"",
self.update_scroll_region
)
# Make inner frame follow canvas width
self.canvas.bind(
"",
self.resize_matrix
)
# Mouse wheel
self.canvas.bind_all(
"",
self.mousewheel
)
# Linux mouse wheel
self.canvas.bind_all(
"",
self.mousewheel_linux_up
)
self.canvas.bind_all(
"",
self.mousewheel_linux_down
)
# ----------------------------------------------------
# CREATE MATRIX
# ----------------------------------------------------
for program in range(1, 129):
row = (program - 1) // 8
col = (program - 1) % 8
button = ttk.Radiobutton(
self.matrix_inner,
text=str(program),
value=program,
variable=self.matrix_var,
command=lambda p=program:
self.matrix_program_selected(p)
)
button.grid(
row=row,
column=col,
padx=2,
pady=2,
sticky="ew"
)
self.matrix_buttons.append(button)
for col in range(8):
self.matrix_inner.columnconfigure(
col,
weight=1
)
self.update_program_controls()
self.update_matrix()
self.update_label()
# ========================================================
# SCROLLING
# ========================================================
def update_scroll_region(self, event=None):
self.canvas.configure(
scrollregion=self.canvas.bbox("all")
)
def resize_matrix(self, event):
self.canvas.itemconfigure(
self.canvas_window,
width=event.width
)
def mousewheel(self, event):
self.canvas.yview_scroll(
int(-1 * (event.delta / 120)),
"units"
)
def mousewheel_linux_up(self, event):
self.canvas.yview_scroll(
-3,
"units"
)
def mousewheel_linux_down(self, event):
self.canvas.yview_scroll(
3,
"units"
)
# ========================================================
# MIDI PORTS
# ========================================================
def refresh_ports(self):
ports = mido.get_output_names()
self.port_combo["values"] = ports
if ports:
self.port_combo.current(0)
else:
self.port_var.set("")
def connect_port(self):
name = self.port_var.get()
if not name:
messagebox.showwarning(
"MIDI",
"No MIDI output port selected."
)
return
try:
if self.port:
self.port.close()
self.port = mido.open_output(name)
self.send_program()
self.send_reverb()
except Exception as e:
self.port = None
messagebox.showerror(
"MIDI Error",
str(e)
)
# ========================================================
# MIDI PROGRAM CHANGE
# ========================================================
def send_program(self):
if self.port is None:
return
midi_channel = self.channel - 1
midi_program = self.program - 1
try:
self.port.send(
mido.Message(
"program_change",
channel=midi_channel,
program=midi_program
)
)
except Exception as e:
print("MIDI error:", e)
# ========================================================
# REVERB CC 91
# ========================================================
def send_reverb(self):
if self.port is None:
return
midi_channel = self.channel - 1
try:
self.port.send(
mido.Message(
"control_change",
channel=midi_channel,
control=REVERB_CC,
value=self.reverb
)
)
except Exception as e:
print("MIDI reverb error:", e)
def reverb_changed(self, value):
try:
self.reverb = int(float(value))
except ValueError:
return
self.send_reverb()
# ========================================================
# CHANNEL
# ========================================================
def channel_changed(self):
try:
channel = int(
self.channel_var.get()
)
if channel < 1 or channel > 16:
raise ValueError
except ValueError:
self.channel_var.set(
self.channel
)
return
self.channel = channel
# Channel 10 only supports the selected drum kits.
if self.channel == 10:
if self.program not in DRUM_KITS:
self.program = DRUM_PROGRAMS[0]
self.program_var.set(
self.program
)
self.matrix_var.set(
self.program
)
self.update_program_controls()
self.update_matrix()
self.update_label()
self.send_program()
self.send_reverb()
# ========================================================
# PROGRAM
# ========================================================
def set_program(self, program):
if self.channel == 10:
if program not in DRUM_KITS:
return
if program < 1 or program > 128:
return
self.program = program
self.program_var.set(
program
)
self.matrix_var.set(
program
)
self.update_label()
self.update_matrix()
self.send_program()
def program_spin_changed(self):
try:
program = int(
self.program_var.get()
)
except ValueError:
self.program_var.set(
self.program
)
return
if self.channel == 10:
if program not in DRUM_KITS:
program = min(
DRUM_PROGRAMS,
key=lambda p:
abs(p - program)
)
else:
if program < 1:
program = 1
if program > 128:
program = 128
self.set_program(program)
# ========================================================
# PREVIOUS
# ========================================================
def previous_program(self):
if self.channel == 10:
current_index = DRUM_PROGRAMS.index(
self.program
)
new_index = (
current_index - 1
) % len(DRUM_PROGRAMS)
self.set_program(
DRUM_PROGRAMS[new_index]
)
else:
if self.program <= 1:
new_program = 128
else:
new_program = self.program - 1
self.set_program(
new_program
)
# ========================================================
# NEXT
# ========================================================
def next_program(self):
if self.channel == 10:
current_index = DRUM_PROGRAMS.index(
self.program
)
new_index = (
current_index + 1
) % len(DRUM_PROGRAMS)
self.set_program(
DRUM_PROGRAMS[new_index]
)
else:
if self.program >= 128:
new_program = 1
else:
new_program = self.program + 1
self.set_program(
new_program
)
# ========================================================
# MATRIX
# ========================================================
def matrix_program_selected(self, program):
self.set_program(program)
def update_matrix(self):
if not self.matrix_buttons:
return
for program, button in enumerate(
self.matrix_buttons,
start=1
):
if self.channel == 10:
if program in DRUM_KITS:
button.configure(
text=f"{program}\n"
f"{DRUM_KITS[program]}",
state="normal"
)
else:
button.configure(
text=str(program),
state="disabled"
)
else:
button.configure(
text=str(program),
state="normal"
)
# ========================================================
# PROGRAM SPINBOX
# ========================================================
def update_program_controls(self):
if self.channel == 10:
self.program_spin.configure(
values=tuple(
DRUM_PROGRAMS
)
)
else:
self.program_spin.configure(
from_=1,
to=128,
values=()
)
# ========================================================
# LABEL
# ========================================================
def update_label(self):
if self.channel == 10:
name = DRUM_KITS.get(
self.program,
"Unknown Drum Kit"
)
else:
name = GM_INSTRUMENTS[
self.program - 1
]
self.instrument_label.configure(
text=f"{self.program} {name}"
)
# ========================================================
# GLOBAL KEYBOARD
# ========================================================
def start_keyboard_listener(self):
self.keyboard_listener = keyboard.Listener(
on_press=self.key_pressed
)
self.keyboard_listener.start()
def key_pressed(self, key):
try:
if key.char == "-":
self.root.after(
0,
self.previous_program
)
elif key.char == "=":
self.root.after(
0,
self.next_program
)
except AttributeError:
pass
# ========================================================
# CLOSE
# ========================================================
def close(self):
if self.keyboard_listener:
self.keyboard_listener.stop()
if self.port:
self.port.close()
self.root.destroy()
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
root = tk.Tk()
app = MidiProgramChanger(root)
root.mainloop()
No comments:
Post a Comment