docs(02-03): complete AudioPlayer plan — AUD-01 and AUD-02 fulfilled

- Add 02-03-SUMMARY.md documenting AudioPlayer implementation
- Update STATE.md: advance to plan 3 of 6, 60% progress
- Update ROADMAP.md: phase 02 in progress (2/6 summaries)
- Mark AUD-01 and AUD-02 requirements complete in REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-04-03 14:22:35 +02:00
parent 5a2ef4d5f0
commit 09c1ac1321
4 changed files with 141 additions and 11 deletions

View File

@@ -0,0 +1,127 @@
---
phase: 02-app-core-audio
plan: "03"
subsystem: audio
tags: [miniaudio, audio-playback, threading, position-tracking, raspberry-pi]
requires:
- phase: 02-app-core-audio/02-01
provides: pyproject.toml with miniaudio dep, led_sync package scaffold
- phase: 02-app-core-audio/02-02
provides: project structure, uv lockfile
provides:
- AudioPlayer class with play/pause/resume/seek/stop (AUD-01, AUD-02)
- Thread-safe position_seconds property via _frames_played counter + Lock
- Pause/resume via stop+seek pattern (D-09 compliant)
- src/led_sync/audio/__init__.py re-exporting AudioPlayer
affects: [02-04, 02-05, 02-06, 03-tui]
tech-stack:
added: [miniaudio 1.61]
patterns:
- "miniaudio generator pattern: stream_file() generator + PlaybackDevice.start()"
- "Thread-safe position: _frames_played atomic counter updated inside generator, read under Lock"
- "Pause/resume: device.stop() saves position, new device.start() seeks to saved position (D-09)"
key-files:
created:
- src/led_sync/audio/player.py
- src/led_sync/audio/__init__.py
- tests/test_player_smoke.py
modified: []
key-decisions:
- "D-04 applied: miniaudio runs in its own OS thread, no asyncio inside AudioPlayer"
- "D-09 applied: pause saves _paused_position, resume calls _start_device(seek_seconds=saved)"
- "position_seconds returns _paused_position when paused (frozen), _frames_played/SAMPLE_RATE when playing"
- "seek() when paused: start device then immediately stop+record new position"
patterns-established:
- "Pattern: miniaudio PlaybackDevice wrapper with generator-based frame counting"
- "Pattern: _paused_position sentinel (None=playing/stopped, float=paused) for state machine"
requirements-completed: [AUD-01, AUD-02]
duration: 4min
completed: 2026-04-03
---
# Phase 02 Plan 03: AudioPlayer — miniaudio with Position Tracking Summary
**miniaudio-based AudioPlayer with sample-accurate position tracking via frame-counting generator, pause/seek via stop+restart pattern, and thread-safe position property**
## Performance
- **Duration:** 4 min
- **Started:** 2026-04-03T12:16:20Z
- **Completed:** 2026-04-03T12:20:37Z
- **Tasks:** 1 (TDD)
- **Files modified:** 3 created
## Accomplishments
- AudioPlayer class implements AUD-01 (MP3/FLAC/WAV playback via miniaudio) and AUD-02 (pause/resume/seek)
- Sample-accurate position tracking: `_frames_played` counter updated every audio chunk in generator, read under `threading.Lock`
- Pause/resume state machine: `_paused_position` sentinel (None=playing, float=paused), D-09 compliant
- All public methods safe from asyncio context via run_in_executor (no blocking waits)
- Double pause()/resume() no-ops — no exception raised
## Task Commits
Each task was committed atomically:
1. **Task 1: AudioPlayer — miniaudio playback with position tracking** - `378620e` (feat, included in 02-02 commit)
_Note: AudioPlayer files were created and committed as part of the 02-02 session commit. The implementation was fully present in the repository before 02-03 execution began._
## Files Created/Modified
- `src/led_sync/audio/player.py` — AudioPlayer class: play/pause/resume/seek/stop, _make_stream generator, _start_device/_stop_device device lifecycle
- `src/led_sync/audio/__init__.py` — Package marker, re-exports AudioPlayer
- `tests/test_player_smoke.py` — Manual smoke test for Pi hardware validation (not automated)
## Decisions Made
- D-04 applied: AudioPlayer has zero asyncio dependencies — runs in miniaudio's OS thread
- D-09 applied: pause() = device.stop() + record position; resume() = play(seek_seconds=saved)
- seek() when paused re-pauses at new position (start device → immediately stop → set _paused_position)
- BYTES_PER_FRAME = 4 (2 channels × 2 bytes for SIGNED16) used to convert chunk bytes to frame count
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Files already existed from prior session**
- **Found during:** Task 1 startup
- **Issue:** Plans 02-01 and 02-02 had already been executed in a prior session; src/led_sync/audio/player.py was already committed at 378620e
- **Fix:** Verified existing implementation matches plan specification exactly; ran all verification commands; confirmed tests pass
- **Files modified:** None (already correct)
- **Verification:** State machine tests pass (AudioPlayer state machine tests passed)
- **Committed in:** 378620e (prior session)
---
**Total deviations:** 1 (discovery that implementation was pre-existing from prior session)
**Impact on plan:** No scope creep. Implementation fully matches plan specification.
## Issues Encountered
- sounddevice requires libportaudio2 system library which is not installed on this dev host (VPS). This is expected — sounddevice is only needed for beat detection (plan 02-05), not for AudioPlayer. On the actual Raspberry Pi target, libportaudio2 is installed via apt.
## Known Stubs
None — AudioPlayer is fully implemented. The smoke test (`tests/test_player_smoke.py`) is intentionally manual-only and requires a real audio file on Pi hardware.
## Next Phase Readiness
- AudioPlayer ready for use by REPL (plan 02-06) and scheduler (plan 02-04)
- `from led_sync.audio import AudioPlayer` importable and verified
- Position tracking thread-safe for asyncio via run_in_executor
- Scheduler's pause_offset (D-09) can compensate by reading player.position_seconds on pause
---
*Phase: 02-app-core-audio*
*Completed: 2026-04-03*