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.
Wednesday, September 9, 2026
gba gsm audio player improved
m8 tracker is ios native
40$ cad
been thinking about picking up
amber opl instead but its ipad only and I already have deflemask.
Friday, September 4, 2026
Midi Program Changer
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()
Sunday, August 30, 2026
Saturday, August 29, 2026
fixing vgm2gbs for linux
once you make a gbs you can convert it to a gb rom using (or just do gb export in deflemask and skip all this)
gbs-master vgm2gbs githubconvert.bat is great if your on windows for drag and drop but 2 problems
1 - vgm headers this script used are old and incompatable with most newer deflemask versions
2 - because im running it in wine and requires python its broken
THE FIX
Replace src/gd3.py with:
make a linux.sh and chmod +x it
#!/bin/bash
cd "$(dirname "$0")"
python3 src/main.py "$1"
import struct
class GD3:
version: int
track_name: str
track_name_orig: str
game_name: str
game_name_orig: str
system_name: str
system_name_orig: str
author: str
author_orig: str
release_date: str
ripper: str
notes: str
def __init__(self, data: bytes):
assert data[0:4] == b'Gd3 '
self.version, data_length = struct.unpack_from(" str:
return f'{self.track_name} - {self.author} - {self.game_name} ({self.ripper})'
making lsdj roms with lsdpack kit
make RGBDS="$HOME/Downloads/rgbds-0.9.1-linux-x86_64/"
rgbds I needed the older version to compile
lsdj rom repo
I use v 8-5-1 stable the newer versions output a garbled mess
in the code_config.mk file i commented out
# ASFLAGS += -h
this projects cool because it lets you plop in your 160x144 album pics with your lsdj songs to make the rom for an interactive album
Friday, August 28, 2026
logitech f310 macros, shutdown, copy paste
from evdev import InputDevice, UInput, ecodes
import subprocess
import threading
import time
GAMEPAD = "/dev/input/by-id/usb-Logitech_Logitech_Dual_Action_DF5CB332-event-joystick"
device = InputDevice(GAMEPAD)
# Virtual keyboard
ui = UInput({
ecodes.EV_KEY: [
ecodes.KEY_LEFTCTRL,
ecodes.KEY_C,
ecodes.KEY_V,
]
}, name="Gamepad Virtual Keyboard")
button_292 = False
button_293 = False
shutdown_timer = None
lock = threading.Lock()
def ctrl_key(key):
print(f"Sending Ctrl+{key}", flush=True)
ui.write(ecodes.EV_KEY, ecodes.KEY_LEFTCTRL, 1)
ui.write(ecodes.EV_KEY, key, 1)
ui.write(ecodes.EV_KEY, key, 0)
ui.write(ecodes.EV_KEY, ecodes.KEY_LEFTCTRL, 0)
ui.syn()
def shutdown():
time.sleep(1)
with lock:
if button_292 and button_293:
print("\nBoth buttons held for 1 second — SHUTTING DOWN!", flush=True)
subprocess.run(["systemctl", "poweroff"])
print(f"Gamepad: {device.name}", flush=True)
print("291 = Ctrl+C", flush=True)
print("290 = Ctrl+V", flush=True)
print("292 + 293 for 1 second = Shutdown", flush=True)
print("Waiting for input...\n", flush=True)
for event in device.read_loop():
if event.type != ecodes.EV_KEY:
continue
with lock:
# BTN_TOP = 291 -> Ctrl+C
if event.code == 291 and event.value == 1:
print("291: Ctrl+C", flush=True)
ctrl_key(ecodes.KEY_C)
# BTN_THUMB2 = 290 -> Ctrl+V
elif event.code == 290 and event.value == 1:
print("290: Ctrl+V", flush=True)
ctrl_key(ecodes.KEY_V)
# BTN_TOP2 = 292
elif event.code == 292:
button_292 = event.value == 1
print(
f"292: {'PRESSED' if button_292 else 'RELEASED'}",
flush=True
)
# BTN_PINKIE = 293
elif event.code == 293:
button_293 = event.value == 1
print(
f"293: {'PRESSED' if button_293 else 'RELEASED'}",
flush=True
)
# Start shutdown timer
if button_292 and button_293 and shutdown_timer is None:
print("Both shoulder buttons held — 1 second timer started.", flush=True)
shutdown_timer = threading.Thread(
target=shutdown,
daemon=True
)
shutdown_timer.start()
# Cancel shutdown
elif not (button_292 and button_293):
shutdown_timer = None
Launching Gambatte Gameboy Emulator From Terminal
retroarch -L
// if installed on linux mint using
// sudo apt install libretro-gambatte
// then it will probably be at:
/usr/lib/x86_64-linux-gnu/libretro/gambatte_libretro.so
// full command for me:
retroarch -L /usr/lib/x86_64-linux-gnu/libretro/gambatte_libretro.so /home/bweew/Documents/Syncthing/Lsdj/lsdj.gb
f for full screen f1 for settings like changing the key maps
You can make it a bash script
#!/bin/bash
retroarch -L /usr/lib/x86_64-linux-gnu/libretro/gambatte_libretro.so "$HOME/Documents/Syncthing/Lsdj/lsdj.gb"
chmod +x ~/lsdj.sh
//on raspberry pi 3 64 bit os
#!/bin/bash
retroarch -L /usr/lib/aarch64-linux-gnu/libretro/gambatte_libretro.so lsdj.gb
Wednesday, August 26, 2026
maj7b5 and 7sus4b5 inversions
1 4 b5 b7
1 b9 4 5
1 3 b5 7
1 2 5 b6
1 3 b5 7
1 2 5 b6
1 4 b5 b7
1 b9 4 5
Tuesday, August 25, 2026
python gba compilation builder scripts
python scripts to build gba roms, emulators not included
Saturday, August 22, 2026
mediaboy - great for making music roms on gameboy easily
the music export is the most useful.
says it can do .mod and .uge but its not working for me
the gif and video exports work in sameboy but not on my flashcart or in gearboy
pcm samplerate settings:
9198 hz -GBVP2 parity yeilds best quality
DMG gives clearer images than colour it seems to mangle the image with blocky pixels
to
use that with gameboy slideshow generator instead of that because it screws up the audio
making gb and gbc videos worked better flashed directly with gb flasher to a perfect dark cart instead of on the superchis gba sd cart also sameboy and bgb are more accurate usually but goomba breaks
renaming png and jpgs as numbers using bash
i=1; for f in *.png *.jpg *.jpeg; do [ -f "$f" ] || continue; mv -- "$f" "$i.${f##*.}"; ((i++)); done
Friday, August 21, 2026
How to make full size lsdj videos like aquallex
tommitytom, the maker of retroplug made this web player that lets you view all the screens of lsdj at once.
finally a linux native retroplug for lsdj
update has a standalone, vst2, vst3, clap and commandline version
also works with nes
use retroplug-cli lsdj-rom --split channels flag foroutput
Thursday, August 20, 2026
new renoise2 mod linux native with terminal use
might need to chmod after moving
the github mainpage for more infoflags for different cli options
renoise2mod song.xrns --type xm
renoise2mod song.xrns --type mod --ptmode hardware --ntsc
renoise2mod --help
keyd - key remapping on linux
I wanted to be able to switch layers or something to map keys in a different localization to be able to use extra shortcuts.
--renoise doesn't detect f13 - 24 making this whole thing useless for my case. but its still a nice key remapper
Installing
git clone https://github.com/rvaiya/keyd
cd keyd
make && sudo make install
sudo systemctl enable --now keyd
Finding Keystrokes
keyd monitor
Listing Available Keys
keyd list-keys
Editing The Config
sudo vim /etc/keyd/default.conf
reload config
sudo keyd reload
emergency reset
sudo keyd reload
emergency reset
backspace+escape+enter
Wednesday, August 12, 2026
gb studio custom waveforms
GB Studio: Advanced Custom Waveform Design for GBT Player
for making custom waves lsdj or hugetracker
sine: 79 bc de ef ff ee dc b9 75 43 21 10 00 11 23 45
tri: fe dc ba 98 76 54 32 10 01 23 45 67 89 ab cd ef