import os import threading import vlc from config import Config from datetime import datetime from time import sleep from log import DEBUG, ERROR lock = threading.Lock() class Music: """ Manage the playing of music tracks """ def __init__(self): self.current_track_start_time = None self.fading = 0 self.VLC = vlc.Instance() self.player = None self.track_path = None self.max_volume = Config.VOLUME_VLC_DEFAULT def fade(self): """ 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. """ with lock: DEBUG("music.fade()", True) if not self.player: return if not self.player.get_position() > 0 and self.player.is_playing(): return self.fading += 1 thread = threading.Thread(target=self._fade) thread.start() def _fade(self): """ Implementation of fading the current track in a separate thread. """ # Take a copy of current player to allow another track to be # started without interfering here p = self.player DEBUG(f"music._fade(), {self.player=}", True) fade_time = Config.FADE_TIME / 1000 steps = Config.FADE_STEPS sleep_time = fade_time / steps # We reduce volume by one mesure first, then by two measures, # then three, and so on. # The sum of the arithmetic sequence 1, 2, 3, ..n is # (n**2 + n) / 2 total_measures_count = (steps**2 + steps) / 2 measures_to_reduce_by = 0 for i in range(1, steps + 1): measures_to_reduce_by += i volume_factor = 1 - ( measures_to_reduce_by / total_measures_count) p.audio_set_volume(int(self.max_volume * volume_factor)) sleep(sleep_time) self.stop(p) self.fading -= 1 def get_playtime(self): "Return elapsed play time" with lock: if not self.player: return None return self.player.get_time() def get_position(self): "Return current position" with lock: DEBUG("music.get_position", True) return self.player.get_position() def play(self, path): """ Start playing the track at path. Log and return if path not found. """ if not os.access(path, os.R_OK): ERROR(f"play({path}): path not found") return self.track_path = path self.player = self.VLC.media_player_new(path) self.player.audio_set_volume(self.max_volume) DEBUG(f"music.play({path=}), {self.player}", True) self.player.play() self.current_track_start_time = datetime.now() def playing(self): """ Return True if currently playing a track, else False vlc.is_playing() returns True if track was faded out. get_position seems more reliable. """ with lock: if self.player: if self.player.get_position() > 0 and self.player.is_playing(): return True # We take a copy of the player when fading, so we could be # playing in a fade nowFalse return self.fading > 0 def set_position(self, ms): "Set current play time in milliseconds from start" with lock: return self.player.set_time(ms) def set_volume(self, volume): "Set maximum volume used for player" with lock: if not self.player: return self.max_volume = volume self.player.audio_set_volume(volume) def stop(self, player=None): "Immediately stop playing" with lock: DEBUG(f"music.stop(), {player=}", True) if not player: if not self.player: return player = self.player DEBUG(f"music.stop({player=})") position = player.get_position() player.stop() DEBUG(f"Releasing player {player=}", True) player.release() # Ensure we don't reference player after release player = None return position