#!/usr/bin/env python3 """soundboard.py -- click-to-play board for the BT soundbank (content/AUDIO/*.wav). Run: python tools/soundboard.py (from the repo root, or anywhere) - Click a button to play that sample (stdlib winsound, async -- click again to restart, click STOP to silence). - The label shows [bank:patch] from game/reconstructed/audiopresets.cpp so an identified sound maps straight back to the game's (bankID, patchID). - Type in the filter box to narrow (substring, case-insensitive). - "Play All" steps through the filtered list one per second (ESC/STOP to halt). """ import os, re, sys, threading, time import tkinter as tk from tkinter import ttk import winsound HERE = os.path.dirname(os.path.abspath(__file__)) AUDIO = os.path.normpath(os.path.join(HERE, "..", "content", "AUDIO")) PRESETS_CPP = os.path.normpath(os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp")) def load_mapping(): """file -> (bank, patch) from audiopresets.cpp (allPresets[b][p] ... file="X.wav").""" mapping = {} try: text = open(PRESETS_CPP, encoding="utf-8", errors="replace").read() # blocks look like: allPresets[0][12].samples[0]; ... s.file = "LaserAFire01.wav"; for m in re.finditer(r'allPresets\[(\d+)\]\[(\d+)\]\.samples\[0\];.*?s\.file = "([^"]+)";', text, re.S): bank, patch, fname = int(m.group(1)) + 1, int(m.group(2)), m.group(3) mapping[fname] = (bank, patch) except OSError: pass return mapping class SoundBoard(tk.Tk): def __init__(self): super().__init__() self.title("BT411 Soundboard -- " + AUDIO) self.geometry("1150x760") self.mapping = load_mapping() self.files = sorted(f for f in os.listdir(AUDIO) if f.lower().endswith(".wav")) self.playing_all = False top = ttk.Frame(self); top.pack(fill="x", padx=6, pady=4) ttk.Label(top, text="Filter:").pack(side="left") self.filter_var = tk.StringVar() ent = ttk.Entry(top, textvariable=self.filter_var, width=32) ent.pack(side="left", padx=4) self.filter_var.trace_add("write", lambda *a: self.rebuild()) ttk.Button(top, text="STOP", command=self.stop).pack(side="left", padx=8) ttk.Button(top, text="Play All (filtered)", command=self.play_all).pack(side="left") self.status = tk.StringVar(value=f"{len(self.files)} samples loaded") ttk.Label(top, textvariable=self.status, foreground="#06c").pack(side="left", padx=12) # scrollable button grid wrap = ttk.Frame(self); wrap.pack(fill="both", expand=True) self.canvas = tk.Canvas(wrap, highlightthickness=0) sb = ttk.Scrollbar(wrap, orient="vertical", command=self.canvas.yview) self.canvas.configure(yscrollcommand=sb.set) sb.pack(side="right", fill="y"); self.canvas.pack(side="left", fill="both", expand=True) self.grid_frame = ttk.Frame(self.canvas) self.canvas.create_window((0, 0), window=self.grid_frame, anchor="nw") self.grid_frame.bind("", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) self.canvas.bind_all("", lambda e: self.canvas.yview_scroll(-1 * (e.delta // 120), "units")) self.bind("", lambda e: self.stop()) self.rebuild() def label_for(self, f): bp = self.mapping.get(f) tag = f"[{bp[0]}:{bp[1]}] " if bp else "[-:-] " return tag + f[:-4] def rebuild(self): for w in self.grid_frame.winfo_children(): w.destroy() flt = self.filter_var.get().lower() shown = [f for f in self.files if flt in f.lower()] cols = 4 for i, f in enumerate(shown): b = ttk.Button(self.grid_frame, text=self.label_for(f), width=38, command=lambda ff=f: self.play(ff)) b.grid(row=i // cols, column=i % cols, padx=2, pady=1, sticky="w") self.status.set(f"{len(shown)} shown / {len(self.files)} total") self.canvas.yview_moveto(0) def play(self, f): self.playing_all = False path = os.path.join(AUDIO, f) winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC) bp = self.mapping.get(f) self.status.set(f"PLAYING {f}" + (f" (bank {bp[0]}, patch {bp[1]})" if bp else "")) def stop(self): self.playing_all = False winsound.PlaySound(None, winsound.SND_PURGE) self.status.set("stopped") def play_all(self): flt = self.filter_var.get().lower() shown = [f for f in self.files if flt in f.lower()] self.playing_all = True def run(): for f in shown: if not self.playing_all: return self.after(0, lambda ff=f: self.status.set("PLAYING " + ff)) winsound.PlaySound(os.path.join(AUDIO, f), winsound.SND_FILENAME | winsound.SND_ASYNC) time.sleep(1.0) self.playing_all = False threading.Thread(target=run, daemon=True).start() if __name__ == "__main__": if not os.path.isdir(AUDIO): print("AUDIO dir not found:", AUDIO); sys.exit(1) SoundBoard().mainloop()