--- phase: 02-audio-engine plan: "01" subsystem: audio tags: [mpv, python-mpv, libmpv, soundfile, numpy, ffmpeg, audio-engine, playback] # Dependency graph requires: - phase: 01-foundation provides: FastAPI app skeleton, lifespan pattern, DeviceRegistry, ShowStore, WebSocket ConnectionManager provides: - MPVEngine class with load/play/pause/seek/stop and 10Hz position polling - apply_timer_fix() with sys.platform == win32 guard (INF-03) - Alpine Dockerfile with mpv-libs, ffmpeg, ffmpeg-libs, libsndfile - pyproject.toml with python-mpv, soundfile, numpy - MPVEngine integrated into FastAPI lifespan (app.state.engine) affects: - 02-02 (waveform extraction uses soundfile/numpy added here) - 02-03 (WebSocket broadcast loop reads MPVEngine.get_state()) - all future phases that use engine.get_state() for position # Tech tracking tech-stack: added: - python-mpv>=1.0.5 (libmpv ctypes binding for audio playback) - soundfile>=0.12.0 (audio file reading via libsndfile) - numpy>=1.26.0 (waveform downsampling) - mpv-libs 0.37.0-r0 (Alpine apk, libmpv.so.2) - ffmpeg + ffmpeg-libs 6.1.1-r0 (Alpine apk, MP3 decode support) - libsndfile 1.2.2-r1 (Alpine apk, WAV/FLAC/OGG reading) patterns: - 10Hz polling via time.sleep(0.1) in daemon thread (not observe_property which fires per audio frame) - Thread-safe state snapshot via threading.Lock in get_state() - MPVEngine injected at lifespan startup, exposed via app.state.engine - ao=null default for server-mode (no audio output), overridable via MPV_AO env var key-files: created: - lightsync/audio/__init__.py - lightsync/audio/engine.py modified: - Dockerfile - pyproject.toml - lightsync/main.py key-decisions: - "MPV_AO env var controls audio output device — defaults to null for Docker server mode, overridable for local dev" - "Polling at 100ms (10Hz) over observe_property — observe_property fires per decoded audio frame (~43x/s at 44.1kHz), overwhelming asyncio" - "apply_timer_fix() is a Linux no-op — timeBeginPeriod is Windows-only Win32 API; guard with sys.platform == win32" - "MPVEngine uses ao param in constructor defaulting to null — caller controls output device" patterns-established: - "Pattern: Daemon thread polling loop — blocking operations in thread, results accessed thread-safely via Lock" - "Pattern: app.state.engine — MPVEngine available to all API routes via request.app.state.engine" - "Pattern: Lifespan start/stop symmetry — engine.start() on startup, engine.stop() on shutdown" requirements-completed: [AUD-01, AUD-03, INF-03] # Metrics duration: 10min completed: 2026-04-06 --- # Phase 02 Plan 01: Audio Engine — MPVEngine Backend **MPVEngine wraps libmpv via python-mpv for headless audio playback with 10Hz position polling, integrated into FastAPI lifespan with Alpine Docker audio stack** ## Performance - **Duration:** ~10 min - **Started:** 2026-04-06T13:01:00Z - **Completed:** 2026-04-06T13:03:49Z - **Tasks:** 2 of 2 - **Files modified:** 5 ## Accomplishments - Created MPVEngine class with load/play/pause/seek/stop methods and thread-safe 10Hz position polling - Updated Dockerfile with Alpine audio stack (mpv-libs, ffmpeg, ffmpeg-libs, libsndfile) - Added python-mpv, soundfile, numpy to pyproject.toml dependencies - Integrated MPVEngine into FastAPI lifespan with MPV_AO env var for configurable audio output - Implemented INF-03 as platform-guarded no-op (apply_timer_fix is Linux-safe) ## Task Commits Each task was committed atomically: 1. **Task 1: Add audio dependencies to Dockerfile and pyproject.toml** - `474b4a7` (chore) 2. **Task 2: Create MPVEngine class with position polling and platform timer fix** - `5f75a19` (feat) **Plan metadata:** (committed with SUMMARY + state updates) ## Files Created/Modified - `lightsync/audio/__init__.py` - Package marker for audio module - `lightsync/audio/engine.py` - MPVEngine class with headless mpv wrapper, 10Hz polling daemon thread, thread-safe get_state(), apply_timer_fix() INF-03 platform guard - `Dockerfile` - Added apk packages: mpv-libs, ffmpeg, ffmpeg-libs, libsndfile - `pyproject.toml` - Added python-mpv>=1.0.5, soundfile>=0.12.0, numpy>=1.26.0 - `lightsync/main.py` - Import MPVEngine, create/start/stop in lifespan, expose via app.state.engine ## Decisions Made - **MPV_AO env var** for audio output device: defaults to `null` (server mode, no speakers needed), overridable for local dev with actual audio output. This avoids hardcoding and keeps Docker images working without audio devices. - **Polling over observe_property**: observe_property fires on every decoded audio frame (~43x/s at 44.1kHz), which would flood the asyncio event loop. Fixed 100ms polling gives exactly 10Hz regardless of audio format. - **apply_timer_fix() as Linux no-op**: `timeBeginPeriod` is a Windows-only Win32 API (`winmm.dll`). On Linux, the kernel provides sub-millisecond timer resolution natively. INF-03 is satisfied as a platform-guarded stub. ## Deviations from Plan None — plan executed exactly as written. The load() method follows the research correction noted in the plan (no wait_for_playback() call, using async mpv loadfile). ## Issues Encountered None — all dependencies verified in research phase, implementation matched expected patterns. ## User Setup Required None - no external service configuration required. MPV_AO env var is optional (defaults to "null" which works in Docker without audio devices). ## Next Phase Readiness - MPVEngine is ready for Plan 02-02 (waveform extraction using soundfile/numpy added in this plan) - MPVEngine is ready for Plan 02-03 (WebSocket broadcast loop using engine.get_state()) - engine accessible in all API route handlers via `request.app.state.engine` - Docker image will build with all required audio libraries once deployed ## Self-Check: PASSED - lightsync/audio/__init__.py: FOUND - lightsync/audio/engine.py: FOUND - lightsync/main.py: FOUND - .planning/phases/02-audio-engine/02-01-SUMMARY.md: FOUND - Commit 474b4a7 (chore: audio deps): FOUND - Commit 5f75a19 (feat: MPVEngine): FOUND --- *Phase: 02-audio-engine* *Completed: 2026-04-06*