|
| 1 | +# MUC Soundboard AI Instructions |
| 2 | + |
| 3 | +## 🏗 Architecture & Data Flow |
| 4 | + |
| 5 | +- **Core Components**: |
| 6 | + - `CLI` (`src/cli.py`): Entry point using `rich-click`. Orchestrates `Soundboard` and `AudioManager`. |
| 7 | + - `Config` (`src/config.py`): Persists state (device ID, volume) to `~/.muc/config.json`. |
| 8 | + - `Soundboard` (`src/soundboard.py`): Manages sound library (scanning `sounds/`) and `pynput` hotkey bindings. |
| 9 | + - `AudioManager` (`src/audio_manager.py`): Low-level audio I/O via `sounddevice` and `soundfile`. |
| 10 | + |
| 11 | +- **Audio Routing Strategy**: |
| 12 | + - The app outputs audio to a **Virtual Audio Cable** (e.g., VB-Cable). |
| 13 | + - **Critical**: The app must output to the "Input" side of the virtual cable (e.g., `CABLE Input`). |
| 14 | + - Games/Apps read from the "Output" side (e.g., `CABLE Output`) as a microphone. |
| 15 | + - `AudioManager.find_virtual_cable()` auto-detects devices with keywords: "cable", "virtual", "vb-audio". |
| 16 | + |
| 17 | +- **Data Flow**: |
| 18 | + `CLI Command/Hotkey` -> `Soundboard` -> `AudioManager.play_audio()` -> `soundfile.read()` -> `numpy` processing (volume/channels) -> `sounddevice.play()` -> `Virtual Device`. |
| 19 | + |
| 20 | +## 🛠 Developer Workflows |
| 21 | + |
| 22 | +- **Dependency Management**: Use `uv` for package management. |
| 23 | + - Install: `uv add muc` or `uv sync` |
| 24 | + - Run: `uv run muc` |
| 25 | +- **Linting**: Run `make lint` to execute pre-commit hooks (ruff, etc.). |
| 26 | +- **Testing Audio**: |
| 27 | + - Use `muc devices` to list IDs. |
| 28 | + - Use `muc play [name]` to test playback without hotkeys. |
| 29 | + - `AudioManager.play_audio(..., blocking=True)` is used for sequential playback (e.g., `muc auto`). |
| 30 | + |
| 31 | +## 🧩 Patterns & Conventions |
| 32 | + |
| 33 | +- **UI/UX**: |
| 34 | + - **ALWAYS** use `rich.console.Console` for output. Never use `print()`. |
| 35 | + - Use `rich.table.Table` for listing data (devices, sounds). |
| 36 | + - Use `rich.panel.Panel` for welcome messages/headers. |
| 37 | + - Style errors with `[red]✗[/red]` and successes with `[green]✓[/green]`. |
| 38 | + |
| 39 | +- **Audio Handling**: |
| 40 | + - **Channel Mapping**: `AudioManager` automatically upmixes mono files to match the output device's channel count using `numpy.tile`. |
| 41 | + - **Volume**: Applied as a scalar multiplication on the numpy array *before* playback. |
| 42 | + - **Non-blocking by default**: `play_audio` is fire-and-forget unless `blocking=True` is passed. |
| 43 | + |
| 44 | +- **Configuration**: |
| 45 | + - Config is auto-loaded on instantiation of `Config()`. |
| 46 | + - `Config.save()` must be called explicitly after modifying settings. |
| 47 | + - Paths are handled with `pathlib.Path`. |
| 48 | + |
| 49 | +- **Error Handling**: |
| 50 | + - Catch `OSError` and `RuntimeError` from `sounddevice`/`pynput`. |
| 51 | + - Display user-friendly messages via `console.print` instead of letting the app crash. |
| 52 | + - Example: |
| 53 | + ```python |
| 54 | + try: |
| 55 | + sd.play(data, samplerate) |
| 56 | + except (OSError, RuntimeError) as e: |
| 57 | + self.console.print(f"[red]Error:[/red] {e}") |
| 58 | + ``` |
| 59 | + |
| 60 | +## 🔑 Key Integration Points |
| 61 | + |
| 62 | +- **Hotkeys**: |
| 63 | + - Uses `pynput.keyboard.GlobalHotKeys`. |
| 64 | + - The listener runs in a daemon thread, but the CLI command (`listen`) must keep the main thread alive (often with a `keyboard.Listener` blocking on `join()`). |
| 65 | +- **File Discovery**: |
| 66 | + - Recursively scans `sounds/` for extensions: `.wav`, `.mp3`, `.ogg`, `.flac`, `.m4a`. |
| 67 | + - Uses `pathlib.Path.rglob("*")`. |
0 commit comments