- SUMMARY.md: TDD execution documented, deviation noted (module-level time import) - STATE.md: advanced to plan 3/6, 90% progress, decision logged - ROADMAP.md: 5/6 summaries complete for phase 02 - REQUIREMENTS.md: AUD-03 and AUD-04 marked complete
4.3 KiB
4.3 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-app-core-audio | 05 | audio |
|
|
|
|
|
|
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:
- Opens a
sounddevice.InputStreamat 44100Hz with 512-sample blocks (~11.6ms latency per hop) - Runs
aubio.tempoin the OS audio thread callback to detect beats from the audio stream - Bridges beat events into the asyncio event loop via
loop.call_soon_threadsafe(loop.create_task, on_beat(beat_time)) - Uses
threading.Lockfor thread-safebeat_countandbpmproperties - Implements 200ms double-trigger suppression to avoid false positives on strong transients
Public API
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_callbackis 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=Noneselects 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=1024lock 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 _timeinside_audio_callback— importing inside a hot audio callback runs at ~11ms intervals and is poor practice - Fix: Moved
import timeto 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 |