Friday, September 11, 2026

Best Free Fx Plugins 2026

put down production for a while. digging through the pile these gems from tiagolr, baconpaul, and blepfx stand out.


Tiagolr

time 12
gate12 filtr

Other Tiagolr Plugins I haven't tried yet that look interesting

quadmorph filter qdelay tetra op - 4op fm wavetable synth

Blepfx

filtrr - the best filter

Baconpaul

two-filters

#the jsfx version of tiagolr for reaper
https://raw.githubusercontent.com/tiagolr/tilr_jsfx/master/index.xml

Wednesday, September 9, 2026

gba gsm audio player improved

this version allows album art.
I think it also allows you to drop in wavs? the other music player required sox conversation and I always forget the command maybe this will be easier.

gsm player

m8 tracker is ios native

40$ cad
been thinking about picking up
amber opl instead but its ipad only and I already have deflemask.

gba slideshow maker for windows

gba slideshow from gameboy advance.net

Friday, September 4, 2026

Midi Program Changer

Midi Program Changer github

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()