TTS does not lower mpv music volume while speaking

- Fixes #126
- callback was missing
This commit is contained in:
Philipp Grafe committed 2026-02-08 18:29:40 +01:00
1 parent 9b52f61a9f
commit f5b76b8921
2 files changed
+90 -68

No files matched your search

+33 -8
View File
@@ -1,4 +1,4 @@
import logging # mpv_player.py
from typing import Union, List, Callable, Optional from typing import Union, List, Callable, Optional
from .player.libmpv import LibMpvPlayer from .player.libmpv import LibMpvPlayer
@@ -6,11 +6,13 @@ from .player.libmpv import LibMpvPlayer
class MpvMediaPlayer: class MpvMediaPlayer:
""" """
LVA MediaPlayer implementation using libmpv. Linux Voice Assistant MediaPlayer implementation based on libmpv.
This class provides the MediaPlayer interface expected by LVA and
delegates all playback logic to LibMpvPlayer.
""" """
def __init__(self, device: str | None = None): def __init__(self, device: str | None = None) -> None:
self._log = logging.getLogger(self.__class__.__name__)
self._player = LibMpvPlayer(device=device) self._player = LibMpvPlayer(device=device)
self._done_callback: Optional[Callable[[], None]] = None self._done_callback: Optional[Callable[[], None]] = None
@@ -18,33 +20,56 @@ class MpvMediaPlayer:
self, self,
url: Union[str, List[str]], url: Union[str, List[str]],
done_callback: Optional[Callable[[], None]] = None, done_callback: Optional[Callable[[], None]] = None,
stop_first: bool = True, stop_first: bool = False,
) -> None: ) -> None:
self._log.info("Playing %s", url) """
Play a media URL.
Args:
url: Media URL or list of URLs (LVA currently uses a single URL).
done_callback: Optional callback invoked when playback finishes.
stop_first: Kept for API compatibility; currently unused.
"""
# LVA currently only uses single URLs
if isinstance(url, list): if isinstance(url, list):
url = url[0] url = url[0]
self._done_callback = done_callback self._done_callback = done_callback
self._player.play(url, paused=False) self._player.play(url, done_callback=done_callback, stop_first=stop_first)
def pause(self) -> None: def pause(self) -> None:
"""Pause playback."""
self._player.pause() self._player.pause()
def resume(self) -> None: def resume(self) -> None:
"""Resume playback."""
self._player.resume() self._player.resume()
def stop(self) -> None: def stop(self) -> None:
"""Stop playback and invoke the done callback if present."""
self._player.stop() self._player.stop()
if self._done_callback: if self._done_callback:
self._done_callback() self._done_callback()
self._done_callback = None self._done_callback = None
def set_volume(self, volume: float) -> None: def set_volume(self, volume: float) -> None:
"""
Set playback volume.
Args:
volume: Volume in percent (0.0–100.0).
"""
self._player.set_volume(volume) self._player.set_volume(volume)
def duck(self, factor: float = 0.5) -> None: def duck(self, factor: float = 0.5) -> None:
"""
Temporarily reduce volume.
Args:
factor: Volume multiplier (0.0–1.0).
"""
self._player.duck(factor) self._player.duck(factor)
def unduck(self) -> None: def unduck(self) -> None:
self._player.unduck() """Restore volume after ducking."""
self._player.unduck()
+57 -60
View File
@@ -1,6 +1,6 @@
import threading import threading
import logging from typing import Optional, Callable
from typing import Optional
import mpv import mpv
from .base import AudioPlayer from .base import AudioPlayer
@@ -9,28 +9,23 @@ from .state import PlayerState
class LibMpvPlayer(AudioPlayer): class LibMpvPlayer(AudioPlayer):
""" """
Audio player implementation for Linux Voice Assistant based on libmpv. AudioPlayer implementation for Linux Voice Assistant using libmpv.
Features: Responsibilities:
- Thread-safe state handling - mpv lifecycle and playback control
- Explicit pause / resume control - thread-safe state management
- Proper volume handling with ducking support - volume handling with ducking support
- Compatible with LVA AudioPlayer interface
""" """
def __init__(self, device: Optional[str] = None) -> None: def __init__(self, device: Optional[str] = None) -> None:
"""
Initialize the mpv-based audio player.
:param device: Optional mpv audio output device name
"""
self._log = logging.getLogger(self.__class__.__name__)
self._state: PlayerState = PlayerState.IDLE self._state: PlayerState = PlayerState.IDLE
self._state_lock = threading.Lock() self._state_lock = threading.Lock()
# Volume handling # Volume handling
self._user_volume: float = 100.0 # User volume (0.0 – 100.0) self._user_volume: float = 100.0 # 0.0 – 100.0
self._duck_factor: float = 1.0 # Ducking factor (0.0 – 1.0) self._duck_factor: float = 1.0 # 0.0 – 1.0
self._done_callback: Optional[Callable[[], None]] = None
# mpv setup # mpv setup
self._mpv = mpv.MPV( self._mpv = mpv.MPV(
@@ -40,59 +35,53 @@ class LibMpvPlayer(AudioPlayer):
) )
if device: if device:
self._log.info("Using audio device: %s", device)
self._mpv["audio-device"] = device self._mpv["audio-device"] = device
# -------- Core Playback Methods -------- self._mpv.event_callback("end-file")(self._on_end_file)
def play(self, url: str, paused: bool = False) -> None: # -------- Playback control --------
def play(
self,
url: str,
done_callback: Optional[Callable[[], None]] = None,
stop_first: bool = True,
) -> None:
""" """
Start playback of a media URL. Start playback of a media URL.
:param url: Media URL or file path Args:
:param paused: If True, playback starts paused url: Media URL or local file path.
done_callback: Optional callback invoked when playback finishes.
stop_first: If True, start playback in paused state.
""" """
with self._state_lock: with self._state_lock:
self._duck_factor = 1.0 # auto-unduck on new playback
self._apply_volume()
self._set_state(PlayerState.LOADING) self._set_state(PlayerState.LOADING)
self._mpv.pause = paused self._done_callback = done_callback
self._log.info("Loading media: %s (paused=%s)", url, paused) self._mpv.pause = stop_first
self._mpv.play(url) self._mpv.play(url)
def pause(self) -> None: def pause(self) -> None:
""" """Pause playback."""
Pause playback.
"""
with self._state_lock: with self._state_lock:
self._mpv.pause = True self._mpv.pause = True
self._set_state(PlayerState.PAUSED) self._set_state(PlayerState.PAUSED)
def resume(self) -> None: def resume(self) -> None:
""" """Resume playback if paused."""
Resume playback if paused.
"""
with self._state_lock: with self._state_lock:
self._duck_factor = 1.0 # ensure unduck
self._apply_volume()
self._mpv.pause = False self._mpv.pause = False
self._set_state(PlayerState.PLAYING) self._set_state(PlayerState.PLAYING)
def stop(self) -> None: def stop(self) -> None:
""" """Stop playback and reset state to IDLE."""
Stop playback and reset player state to IDLE.
"""
with self._state_lock: with self._state_lock:
self._mpv.stop() self._mpv.stop()
self._set_state(PlayerState.IDLE) self._set_state(PlayerState.IDLE)
def state(self) -> PlayerState: def state(self) -> PlayerState:
""" """Return the current player state."""
Get the current player state.
:return: Current PlayerState
"""
with self._state_lock: with self._state_lock:
return self._state return self._state
@@ -100,9 +89,10 @@ class LibMpvPlayer(AudioPlayer):
def set_volume(self, volume: float) -> None: def set_volume(self, volume: float) -> None:
""" """
Set the user volume. Set user volume.
:param volume: Volume level (0.0 – 100.0) Args:
volume: Volume level (0.0–100.0).
""" """
with self._state_lock: with self._state_lock:
self._user_volume = max(0.0, min(100.0, float(volume))) self._user_volume = max(0.0, min(100.0, float(volume)))
@@ -110,46 +100,53 @@ class LibMpvPlayer(AudioPlayer):
def duck(self, factor: float = 0.5) -> None: def duck(self, factor: float = 0.5) -> None:
""" """
Temporarily reduce volume by a ducking factor. Reduce volume temporarily by a ducking factor.
:param factor: Ducking factor (0.0 – 1.0) Args:
factor: Ducking factor (0.0–1.0).
""" """
with self._state_lock: with self._state_lock:
self._duck_factor = max(0.0, min(1.0, float(factor))) self._duck_factor = max(0.0, min(1.0, float(factor)))
self._apply_volume() self._apply_volume()
def unduck(self) -> None: def unduck(self) -> None:
""" """Restore volume to the user-defined level."""
Restore volume to the user-defined level.
"""
with self._state_lock: with self._state_lock:
self._duck_factor = 1.0 self._duck_factor = 1.0
self._apply_volume() self._apply_volume()
# -------- Internal Helpers -------- # -------- Internal helpers --------
def _apply_volume(self) -> None: def _apply_volume(self) -> None:
""" """Apply effective volume (user volume × duck factor) to mpv."""
Apply effective volume (user volume × duck factor) to mpv.
"""
effective = self._user_volume * self._duck_factor effective = self._user_volume * self._duck_factor
self._mpv.volume = max(0.0, min(100.0, effective)) self._mpv.volume = max(0.0, min(100.0, effective))
def _on_end_file(self, event) -> None:
callback: Optional[Callable[[], None]] = None
with self._state_lock:
self._set_state(PlayerState.IDLE)
callback = self._done_callback
self._done_callback = None
if callback is not None:
try:
callback()
except RuntimeError:
# Callback errors must never break the player
pass
def _on_mpv_log(self, level: str, prefix: str, text: str) -> None: def _on_mpv_log(self, level: str, prefix: str, text: str) -> None:
""" """
Handle mpv log messages. Handle mpv log messages.
Errors and fatal messages transition the player into ERROR state. Error and fatal messages transition the player into ERROR state.
""" """
if level in ("error", "fatal"): if level in ("error", "fatal"):
self._log.error("[mpv] %s", text.strip())
with self._state_lock: with self._state_lock:
self._set_state(PlayerState.ERROR) self._set_state(PlayerState.ERROR)
def _set_state(self, new_state: PlayerState) -> None: def _set_state(self, new_state: PlayerState) -> None:
""" """Update internal player state."""
Update internal player state with logging. self._state = new_state
"""
if self._state != new_state:
self._log.debug("State %s → %s", self._state.name, new_state.name)
self._state = new_state