210 lines
7.0 KiB
Markdown
210 lines
7.0 KiB
Markdown
# Python Soundboard — Claude Code Build Plan
|
||
|
||
## Overview
|
||
|
||
A desktop soundboard application for Windows 11 that routes audio through VB-Cable so it
|
||
appears as a microphone input to other applications (Discord, OBS, Teams, etc.). Users can
|
||
add and remove audio files from the board at runtime via a simple GUI.
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
| Layer | Choice | Reason |
|
||
|---|---|---|
|
||
| Language | Python 3.11+ | Easy iteration, good audio library support |
|
||
| GUI | `tkinter` | Stdlib, no install needed, sufficient for this use case |
|
||
| Audio playback | `sounddevice` + `soundfile` | Device selection by name, supports WAV/FLAC natively |
|
||
| Audio decoding | `pydub` + `ffmpeg` | Converts MP3/OGG/etc. to raw PCM for sounddevice |
|
||
| Config persistence | `json` | Store board layout and device preference between sessions |
|
||
|
||
### Install Requirements (`requirements.txt`)
|
||
```
|
||
sounddevice
|
||
soundfile
|
||
pydub
|
||
numpy
|
||
```
|
||
> ffmpeg must also be installed and on PATH (used by pydub for MP3 decoding).
|
||
> Recommend: `winget install Gyan.FFmpeg`
|
||
|
||
---
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
soundboard/
|
||
├── main.py # Entry point, launches GUI
|
||
├── audio_engine.py # Handles device selection and playback
|
||
├── board.py # Board state: buttons, file mappings
|
||
├── config.py # Load/save config to JSON
|
||
├── config.json # Persisted state (auto-created on first run)
|
||
├── requirements.txt
|
||
└── sounds/ # Optional default folder for audio files
|
||
```
|
||
|
||
---
|
||
|
||
## Feature Spec
|
||
|
||
### 1. Device Selection (Startup)
|
||
- On launch, scan available output devices via `sounddevice.query_devices()`
|
||
- Show a dropdown/listbox to select the output device
|
||
- Default: auto-select first device whose name contains `"CABLE Input"` (VB-Cable)
|
||
- Selection is saved to `config.json` and restored on next launch
|
||
|
||
### 2. Soundboard Grid
|
||
- Display a grid of buttons (default: 4 columns, expandable rows)
|
||
- Each button shows:
|
||
- The audio file's base name (e.g. `airhorn.mp3`)
|
||
- A colored indicator when playing
|
||
- Clicking a button plays that sound through the selected output device
|
||
- Playing a sound does NOT stop other sounds (overlapping play supported)
|
||
- Optional: right-click a button → Stop this sound
|
||
|
||
### 3. Add Audio Files
|
||
- "Add Sound" button opens a file picker dialog
|
||
- Supported formats: `.wav`, `.mp3`, `.ogg`, `.flac`, `.aiff`
|
||
- File is registered on the board (path stored in config); it is NOT copied
|
||
- New button appears immediately in the grid
|
||
- Board layout is saved to `config.json` automatically
|
||
|
||
### 4. Remove Audio Files
|
||
- Right-click any button → context menu with "Remove" option
|
||
- Confirmation prompt before removal
|
||
- Button is removed from the grid; entry deleted from config
|
||
- Does not delete the file from disk
|
||
|
||
### 5. Rename Buttons
|
||
- Right-click → "Rename" option
|
||
- Simple text entry dialog
|
||
- Custom label saved to config (file path unchanged)
|
||
|
||
### 6. Volume Control
|
||
- A global volume slider (0–100%) in the toolbar
|
||
- Scales the numpy audio array before playback
|
||
- Per-button volume is a stretch goal (not in v1)
|
||
|
||
### 7. Stop All
|
||
- "Stop All" button immediately halts all active sounddevice streams
|
||
|
||
### 8. Config Persistence (`config.json` schema)
|
||
```json
|
||
{
|
||
"output_device": "CABLE Input (VB-Audio Virtual Cable)",
|
||
"volume": 80,
|
||
"buttons": [
|
||
{
|
||
"id": "btn_001",
|
||
"label": "Air Horn",
|
||
"file_path": "C:/Users/user/sounds/airhorn.mp3"
|
||
},
|
||
{
|
||
"id": "btn_002",
|
||
"label": "Sad Trombone",
|
||
"file_path": "C:/Users/user/sounds/sad_trombone.wav"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Key Implementation Details for Claude Code
|
||
|
||
### Audio Playback (audio_engine.py)
|
||
|
||
```python
|
||
import sounddevice as sd
|
||
import soundfile as sf
|
||
import numpy as np
|
||
from pydub import AudioSegment
|
||
import io
|
||
|
||
def get_device_id(device_name: str) -> int:
|
||
"""Find output device index by name substring match."""
|
||
devices = sd.query_devices()
|
||
for i, dev in enumerate(devices):
|
||
if device_name.lower() in dev['name'].lower() and dev['max_output_channels'] > 0:
|
||
return i
|
||
raise ValueError(f"Device not found: {device_name}")
|
||
|
||
def load_audio(file_path: str) -> tuple[np.ndarray, int]:
|
||
"""Load any supported audio format, return (samples_array, samplerate)."""
|
||
ext = file_path.lower().split('.')[-1]
|
||
if ext in ('wav', 'flac', 'aiff'):
|
||
data, sr = sf.read(file_path, dtype='float32')
|
||
else:
|
||
# Use pydub for MP3/OGG/etc.
|
||
seg = AudioSegment.from_file(file_path)
|
||
seg = seg.set_channels(2).set_frame_rate(44100)
|
||
samples = np.array(seg.get_array_of_samples(), dtype=np.float32)
|
||
samples = samples.reshape((-1, 2)) / 32768.0
|
||
data, sr = samples, 44100
|
||
return data, sr
|
||
|
||
def play_sound(file_path: str, device_id: int, volume: float = 1.0):
|
||
"""Play audio non-blocking on the specified device."""
|
||
data, sr = load_audio(file_path)
|
||
data = data * volume # apply volume scaling
|
||
sd.play(data, samplerate=sr, device=device_id, blocking=False)
|
||
```
|
||
|
||
### Threading Note
|
||
- `sd.play()` is non-blocking by default but is not thread-safe when called rapidly
|
||
- Use a `threading.Thread` per button press to avoid GUI freezes on slow file loads
|
||
- Keep a list of active streams if stop-all is needed
|
||
|
||
### GUI Layout (main.py / board.py)
|
||
- Use `tkinter.ttk` for slightly nicer widgets
|
||
- Grid of `tk.Button` widgets built dynamically from config
|
||
- `tkinter.filedialog.askopenfilename()` for file picker
|
||
- `tkinter.simpledialog.askstring()` for rename
|
||
- `tkinter.Menu` for right-click context menus
|
||
|
||
---
|
||
|
||
## VB-Cable Setup Instructions (include in README)
|
||
|
||
1. Download and install VB-Cable from https://vb-audio.com/Cable
|
||
2. Reboot after installation
|
||
3. In the soundboard, select **"CABLE Input (VB-Audio Virtual Cable)"** as the output device
|
||
4. In Discord / OBS / Teams, set the microphone input to **"CABLE Output (VB-Audio Virtual Cable)"**
|
||
5. Audio played on the soundboard will now appear as microphone input
|
||
|
||
---
|
||
|
||
## Claude Code Prompt (paste this to start the build)
|
||
|
||
```
|
||
Build a Python soundboard desktop app for Windows 11 using the spec in soundboard_plan.md.
|
||
|
||
Start with this order:
|
||
1. requirements.txt
|
||
2. audio_engine.py — device listing, audio loading, play/stop functions
|
||
3. config.py — load/save JSON config
|
||
4. board.py — board state management (add, remove, rename buttons)
|
||
5. main.py — tkinter GUI that wires everything together
|
||
|
||
Requirements:
|
||
- Output device is selectable from a dropdown at the top (auto-selects VB-Cable if found)
|
||
- Buttons play sounds non-blocking using threads so the GUI doesn't freeze
|
||
- Right-click context menu on each button: Rename, Remove
|
||
- "Add Sound" button opens a file picker (wav, mp3, ogg, flac)
|
||
- Global volume slider 0-100%
|
||
- "Stop All" button
|
||
- All state saved/loaded from config.json automatically
|
||
- No third-party GUI frameworks — tkinter only
|
||
```
|
||
|
||
---
|
||
|
||
## Stretch Goals (v2)
|
||
|
||
- Hotkey support (bind sounds to keyboard shortcuts)
|
||
- Per-button color customization
|
||
- Drag-to-reorder buttons
|
||
- Loop toggle per button
|
||
- Waveform preview thumbnail on each button
|
||
- System tray icon so the window can be minimized
|