import threading import vlc # type: ignore from config import Config from helpers import file_is_unreadable from typing import Optional from time import sleep from log import log from PyQt6.QtCore import ( QRunnable, QThreadPool, ) lock = threading.Lock() class FadeTrack(QRunnable): def __init__(self, player: vlc.MediaPlayer, fade_seconds) -> None: super().__init__() self.player = player self.fade_seconds = fade_seconds def run(self) -> None: """ Implementation of fading the player """ if not self.player: return # Reduce volume logarithmically total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND db_reduction_per_step = Config.FADEOUT_DB / total_steps reduction_factor_per_step = pow(10, (db_reduction_per_step / 20)) volume = self.player.audio_get_volume() for i in range(1, total_steps + 1): self.player.audio_set_volume( int(volume * pow(reduction_factor_per_step, i)) ) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND) self.player.stop() log.error(f"Releasing player {self.player=}") self.player.release() class Music: """ Manage the playing of music tracks """ def __init__(self) -> None: self.VLC = vlc.Instance() self.player = None self.max_volume = Config.VOLUME_VLC_DEFAULT def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: """ Fade the currently playing track. The actual management of fading runs in its own thread so as not to hold up the UI during the fade. """ log.info("Music.stop()") if not self.player: return if not self.player.get_position() > 0 and self.player.is_playing(): return # Take a copy of current player to allow another track to be # started without interfering here with lock: p = self.player self.player = None pool = QThreadPool.globalInstance() fader = FadeTrack(p, fade_seconds=fade_seconds) pool.start(fader) def get_position(self) -> Optional[float]: """Return current position""" if not self.player: return None return self.player.get_position() def play(self, path: str, position: Optional[float] = None) -> None: """ Start playing the track at path. Log and return if path not found. """ log.info(f"Music.play({path=}, {position=}") if file_is_unreadable(path): log.error(f"play({path}): path not readable") return None media = self.VLC.media_new_path(path) self.player = media.player_new_from_media() if self.player: _ = self.player.play() self.set_volume(self.max_volume) if position: self.player.set_position(position) def set_volume(self, volume=None, set_default=True) -> None: """Set maximum volume used for player""" if not self.player: return if set_default: self.max_volume = volume if volume is None: volume = Config.VOLUME_VLC_DEFAULT self.player.audio_set_volume(volume) def stop(self) -> float: """Immediately stop playing""" log.info("Music.stop()") if not self.player: return 0.0 p = self.player self.player = None with lock: position = p.get_position() p.stop() p.release() p = None return position