chore(02-03): delete lightsync/audio/engine.py (MPV engine removed)
- Browser plays audio now; server-side MPV engine is no longer used - Beat detection via librosa replaces position-polling engine
This commit is contained in:
@@ -1,113 +0,0 @@
|
|||||||
"""MPV-based audio engine — headless playback with 10Hz position polling."""
|
|
||||||
import ctypes.util as _ctypes_util
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
# Alpine/musl: ctypes.util.find_library can't discover libmpv without ldconfig or objdump.
|
|
||||||
# Patch it to return the known path so python-mpv can load the library.
|
|
||||||
_orig_find_library = _ctypes_util.find_library
|
|
||||||
def _patched_find_library(name: str) -> str | None:
|
|
||||||
if name == "mpv":
|
|
||||||
return "/usr/lib/libmpv.so.2"
|
|
||||||
return _orig_find_library(name)
|
|
||||||
_ctypes_util.find_library = _patched_find_library
|
|
||||||
|
|
||||||
import mpv # noqa: E402
|
|
||||||
|
|
||||||
_ctypes_util.find_library = _orig_find_library # restore after mpv loaded
|
|
||||||
|
|
||||||
|
|
||||||
def apply_timer_fix() -> None:
|
|
||||||
"""Apply 1ms timer resolution on Windows. No-op on Linux/macOS (INF-03)."""
|
|
||||||
if sys.platform == "win32":
|
|
||||||
import ctypes
|
|
||||||
winmm = ctypes.WinDLL("winmm")
|
|
||||||
winmm.timeBeginPeriod(1)
|
|
||||||
|
|
||||||
|
|
||||||
class MPVEngine:
|
|
||||||
"""Wraps libmpv for headless audio playback with position polling.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
engine = MPVEngine()
|
|
||||||
engine.start() # starts 10Hz polling thread
|
|
||||||
engine.load("/path/to/song.mp3")
|
|
||||||
engine.play()
|
|
||||||
pos = engine.get_state() # {"position": 42.1, "paused": False, ...}
|
|
||||||
engine.stop()
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ao: str = "null"):
|
|
||||||
self._player: mpv.MPV = mpv.MPV(
|
|
||||||
vo="null",
|
|
||||||
ao=ao,
|
|
||||||
input_default_bindings=False,
|
|
||||||
input_vo_keyboard=False,
|
|
||||||
)
|
|
||||||
self._position: float = 0.0
|
|
||||||
self._duration: float | None = None
|
|
||||||
self._loaded_path: str | None = None
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self._running = False
|
|
||||||
self._thread: threading.Thread | None = None
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
"""Start the 10Hz position polling thread."""
|
|
||||||
apply_timer_fix()
|
|
||||||
self._running = True
|
|
||||||
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def _poll_loop(self) -> None:
|
|
||||||
"""Poll time-pos at 10Hz. Do NOT use observe_property — fires per audio frame."""
|
|
||||||
while self._running:
|
|
||||||
pos = self._player.time_pos
|
|
||||||
dur = self._player.duration
|
|
||||||
with self._lock:
|
|
||||||
self._position = pos if pos is not None else 0.0
|
|
||||||
if dur is not None:
|
|
||||||
self._duration = dur
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
def get_state(self) -> dict[str, Any]:
|
|
||||||
"""Thread-safe snapshot of current playback state."""
|
|
||||||
with self._lock:
|
|
||||||
return {
|
|
||||||
"position": self._position,
|
|
||||||
"paused": bool(self._player.pause),
|
|
||||||
"duration": self._duration,
|
|
||||||
"loaded": self._loaded_path is not None,
|
|
||||||
"file": self._loaded_path,
|
|
||||||
}
|
|
||||||
|
|
||||||
def load(self, path: str) -> None:
|
|
||||||
"""Load an audio file. Starts paused. Supports MP3, WAV, FLAC, OGG (AUD-01)."""
|
|
||||||
self._loaded_path = path
|
|
||||||
self._player.loadfile(path, mode="replace")
|
|
||||||
# loadfile is async in mpv — file loads in background
|
|
||||||
# pause=True will take effect once mpv has loaded the file
|
|
||||||
self._player.pause = True
|
|
||||||
|
|
||||||
def play(self) -> None:
|
|
||||||
"""Resume playback (AUD-03)."""
|
|
||||||
self._player.pause = False
|
|
||||||
|
|
||||||
def pause(self) -> None:
|
|
||||||
"""Pause playback (AUD-03)."""
|
|
||||||
self._player.pause = True
|
|
||||||
|
|
||||||
def seek(self, seconds: float) -> None:
|
|
||||||
"""Seek to absolute position in seconds (AUD-03)."""
|
|
||||||
self._player.seek(seconds, reference="absolute")
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""Stop polling and terminate mpv."""
|
|
||||||
self._running = False
|
|
||||||
if self._thread:
|
|
||||||
self._thread.join(timeout=2.0)
|
|
||||||
try:
|
|
||||||
self._player.terminate()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
Reference in New Issue
Block a user