3.6 KiB
Soundboard
A cross-platform desktop soundboard. It plays audio files through a virtual audio device so the sounds show up as a microphone in other apps (Discord, OBS, Teams, …).
- Windows: routes through VB-Cable.
- Linux: routes through a PulseAudio / PipeWire null sink.
Sounds overlap (multiple can play at once), you can add/remove/rename buttons at
runtime, and everything persists to config.json.
Install
pip install -r requirements.txt
You also need:
- ffmpeg on
PATH(for MP3/OGG/M4A decoding via pydub)- Windows:
winget install Gyan.FFmpeg - Debian/Ubuntu:
sudo apt install ffmpeg
- Windows:
- tkinter (bundled with CPython; on Linux you may need
sudo apt install python3-tk)
Run
python main.py
Virtual device setup
Windows (VB-Cable)
- Install VB-Cable from https://vb-audio.com/Cable and reboot.
- In the soundboard, pick "CABLE Input (VB-Audio Virtual Cable)" as the output (it auto-selects if found).
- In Discord / OBS / Teams, set the mic input to "CABLE Output (VB-Audio Virtual Cable)".
Linux (PipeWire / PulseAudio null sink)
- Create a null sink (it appears as an output device named like "Null Output"):
(Works under both PulseAudio and PipeWire's pipewire-pulse.)
pactl load-module module-null-sink sink_name=soundboard \ sink_properties=device.description=Soundboard - In the soundboard, pick the Soundboard / Null Output device (auto-selected if found).
- In your target app, choose the sink's monitor as the microphone: "Monitor of Soundboard".
- To remove the sink later:
pactl unload-module module-null-sink(or unload by index).
Usage
- Add Sound — file picker (
.wav .mp3 .ogg .flac .aiff .m4a .aac). The file is referenced, not copied. - Left-click a button — play it (overlaps with anything already playing). The button turns green while it's sounding.
- Right-click a button — Rename / Stop this sound / Remove.
- Volume slider — master volume, applied live to playing sounds.
- Stop All — halt everything immediately.
All state (device, volume, buttons) is saved to config.json next to main.py.
Architecture
| File | Responsibility |
|---|---|
audio_engine.py |
Device discovery, cross-platform auto-select, decode+normalize, overlapping playback (one thread + OutputStream per sound). No GUI deps. |
config.py |
Defensive JSON load/save (atomic write; corrupt/missing → defaults). Pure. |
board.py |
Button state: add/remove/rename, id allocation, persistence. Pure. |
main.py |
tkinter GUI wiring board + engine; marshals playback callbacks to the UI thread. |
config.py and board.py are import-free of audio/GUI libraries so they're unit-testable
headlessly. audio_engine.py lazily imports sounddevice/soundfile/pydub so the pure
logic and tests run without audio libraries installed.
Notable deviations from the original plan
- The plan's
sd.play()example was a singleton and cannot overlap sounds. Replaced with per-soundOutputStreams on dedicated threads. - Both decode paths are normalized to a common channel layout (
sounddevicewon't resample/remix), preventing errors/pitch-shift on mixed files. - Added a playback-finished signal so the "playing" indicator clears correctly.
Tests
python -m unittest discover -s tests -v
config/board tests need only the stdlib. The audio_engine tests
(match_channels, auto_select_device) require numpy and are skipped if it's absent.