Files
soundboard-py/soundboard/README.md
T

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
  • tkinter (bundled with CPython; on Linux you may need sudo apt install python3-tk)

Run

python main.py

Virtual device setup

Windows (VB-Cable)

  1. Install VB-Cable from https://vb-audio.com/Cable and reboot.
  2. In the soundboard, pick "CABLE Input (VB-Audio Virtual Cable)" as the output (it auto-selects if found).
  3. In Discord / OBS / Teams, set the mic input to "CABLE Output (VB-Audio Virtual Cable)".

Linux (PipeWire / PulseAudio null sink)

  1. Create a null sink (it appears as an output device named like "Null Output"):
    pactl load-module module-null-sink sink_name=soundboard \
      sink_properties=device.description=Soundboard
    
    (Works under both PulseAudio and PipeWire's pipewire-pulse.)
  2. In the soundboard, pick the Soundboard / Null Output device (auto-selected if found).
  3. In your target app, choose the sink's monitor as the microphone: "Monitor of Soundboard".
  4. 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-sound OutputStreams on dedicated threads.
  • Both decode paths are normalized to a common channel layout (sounddevice won'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.