--- phase: 02-app-core-audio plan: "05" subsystem: audio tags: [beat-detection, sounddevice, aubio, asyncio-bridge, threading] dependency_graph: requires: - 02-02 (UDP transport) - 02-03 (AudioPlayer) provides: - BeatDetector class (AUD-03, AUD-04) affects: - 02-06 (CLI integration — will wire BeatDetector to REPL) - Phase 5 (Live Reactive mode — reuses BeatDetector directly) tech_stack: added: - sounddevice 0.5.x (system audio capture via PortAudio/PipeWire) - aubio 0.4.9 (real-time beat detection, C-backed) patterns: - call_soon_threadsafe bridge: audio thread -> asyncio event loop (D-05) - threading.Lock for shared state (beat_count, bpm, last_beat_time) - Double-trigger suppression via monotonic hold timer (200ms) key_files: created: - src/led_sync/audio/beat_detector.py - tests/test_beat_detector.py modified: - src/led_sync/audio/__init__.py decisions: - "D-04 enforced: sounddevice thread is fully independent from asyncio and miniaudio" - "D-05 enforced: call_soon_threadsafe used in _audio_callback — no direct asyncio calls" - "D-06 noted: device=None for PipeWire default; monitor source for loopback capture" - "import time in _audio_callback removed — time import moved to module level for cleanliness" metrics: duration: "~2 minutes" completed: "2026-04-03" tasks_completed: 1 files_created: 2 files_modified: 1 --- # Phase 2 Plan 5: BeatDetector — Real-Time Beat Detection Summary **One-liner:** sounddevice InputStream + aubio.tempo with call_soon_threadsafe bridge into asyncio — AUD-03 and AUD-04 fulfilled. ## What Was Built `BeatDetector` class in `src/led_sync/audio/beat_detector.py` — a real-time beat detection module that: 1. Opens a `sounddevice.InputStream` at 44100Hz with 512-sample blocks (~11.6ms latency per hop) 2. Runs `aubio.tempo` in the OS audio thread callback to detect beats from the audio stream 3. Bridges beat events into the asyncio event loop via `loop.call_soon_threadsafe(loop.create_task, on_beat(beat_time))` 4. Uses `threading.Lock` for thread-safe `beat_count` and `bpm` properties 5. Implements 200ms double-trigger suppression to avoid false positives on strong transients ## Public API ```python detector = BeatDetector(loop=asyncio.get_running_loop(), on_beat=my_async_fn) detector.start(device=None) # None = PipeWire default; pass monitor source for loopback detector.stop() # idempotent, no-op if already stopped detector.bpm: float # current aubio BPM estimate detector.beat_count: int # total beats since start ``` ## Key Design Decisions - **D-04 boundary:** `_audio_callback` is a pure OS thread function — zero asyncio dependencies - **D-05 bridge:** `call_soon_threadsafe(loop.create_task, on_beat(beat_time))` is the only crossing point - **D-06 PipeWire:** `device=None` selects the PipeWire default input; for system audio loopback, pass the monitor source name (documented in module docstring) - Constants `SAMPLE_RATE=44100`, `HOP_SIZE=512`, `BUF_SIZE=1024` lock in ~11.6ms latency ## TDD Execution - **RED commit:** 3ad5e91 — 14 failing tests covering import, constants, thread safety, asyncio bridge pattern, and ImportError handling - **GREEN commit:** f1af7ba — Implementation passes all 14 tests + 57 total test suite passes ## Deviations from Plan **1. [Rule 1 - Bug] Removed `import time as _time` inline import inside callback** - Found during: implementation review - Issue: Plan code sample had `import time as _time` inside `_audio_callback` — importing inside a hot audio callback runs at ~11ms intervals and is poor practice - Fix: Moved `import time` to module-level imports - Files modified: `src/led_sync/audio/beat_detector.py` - Commit: f1af7ba (included in implementation) Otherwise, plan executed exactly as specified. ## Verification Results ``` BeatDetector class OK: latency=11.6ms, all properties accessible 57 passed in 2.80s ``` ## Known Stubs None — BeatDetector is fully implemented. `start()` requires a real audio device to test beat detection live; this is documented in the module as expected behavior (Pi hardware required). ## Self-Check: PASSED | Item | Status | |------|--------| | src/led_sync/audio/beat_detector.py | FOUND | | tests/test_beat_detector.py | FOUND | | Commit 3ad5e91 (TDD RED) | FOUND | | Commit f1af7ba (TDD GREEN) | FOUND |