From 71e76e02d10929614788a327a4a1f6d10fbbe25b Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 22 May 2024 15:45:21 +0100 Subject: [PATCH] Merge changes from master --- app/classes.py | 8 ++++-- app/dialogs.py | 38 +++++++++++++++----------- app/music.py | 66 +++++++++++++++++++++++++++++++++++++++++----- app/musicmuster.py | 57 +++++---------------------------------- app/playlists.py | 2 +- 5 files changed, 94 insertions(+), 77 deletions(-) diff --git a/app/classes.py b/app/classes.py index fda81b0..c6ebd8e 100644 --- a/app/classes.py +++ b/app/classes.py @@ -199,8 +199,12 @@ class PlaylistTrack: ) # Calculate time fade_graph should start updating - update_graph_at_ms = max(0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1) - self.fade_graph_start_updates = now + dt.timedelta(milliseconds=update_graph_at_ms) + update_graph_at_ms = max( + 0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1 + ) + self.fade_graph_start_updates = now + dt.timedelta( + milliseconds=update_graph_at_ms + ) @dataclass diff --git a/app/dialogs.py b/app/dialogs.py index 36ec626..519802b 100644 --- a/app/dialogs.py +++ b/app/dialogs.py @@ -84,7 +84,7 @@ class ReplaceFilesDialog(QDialog): continue rf = TrackFileData(new_file_path=new_file_path) rf.tags = get_tags(new_file_path) - if not rf.tags['title'] or not rf.tags['artist']: + if not rf.tags["title"] or not rf.tags["artist"]: show_warning( parent=self.main_window, title="Error", @@ -98,11 +98,11 @@ class ReplaceFilesDialog(QDialog): # Check for same filename match_track = self.check_by_basename( - session, new_file_path, rf.tags['artist'], rf.tags['title'] + session, new_file_path, rf.tags["artist"], rf.tags["title"] ) if not match_track: match_track = self.check_by_title( - session, new_file_path, rf.tags['artist'], rf.tags['title'] + session, new_file_path, rf.tags["artist"], rf.tags["title"] ) if not match_track: @@ -113,8 +113,7 @@ class ReplaceFilesDialog(QDialog): # We will store new file in the same directory as the # existing file but with the new file name rf.track_path = os.path.join( - os.path.dirname(match_track.path), - new_file_basename + os.path.dirname(match_track.path), new_file_basename ) # We will remove existing track file @@ -125,27 +124,34 @@ class ReplaceFilesDialog(QDialog): if match_basename == new_file_basename: path_text = " " + new_file_basename + " (no change)" else: - path_text = f" {match_basename} →\n {new_file_basename} (replace)" + path_text = ( + f" {match_basename} →\n {new_file_basename} (replace)" + ) filename_item = QTableWidgetItem(path_text) - if match_track.title == rf.tags['title']: - title_text = " " + rf.tags['title'] + " (no change)" + if match_track.title == rf.tags["title"]: + title_text = " " + rf.tags["title"] + " (no change)" else: - title_text = f" {match_track.title} →\n {rf.tags['title']} (update)" + title_text = ( + f" {match_track.title} →\n {rf.tags['title']} (update)" + ) title_item = QTableWidgetItem(title_text) - if match_track.artist == rf.tags['artist']: - artist_text = " " + rf.tags['artist'] + " (no change)" + if match_track.artist == rf.tags["artist"]: + artist_text = " " + rf.tags["artist"] + " (no change)" else: - artist_text = f" {match_track.artist} →\n {rf.tags['artist']} (update)" + artist_text = ( + f" {match_track.artist} →\n {rf.tags['artist']} (update)" + ) artist_item = QTableWidgetItem(artist_text) else: - rf.track_path = os.path.join(Config.REPLACE_FILES_DEFAULT_DESTINATION, - new_file_basename) + rf.track_path = os.path.join( + Config.REPLACE_FILES_DEFAULT_DESTINATION, new_file_basename + ) filename_item = QTableWidgetItem(" " + new_file_basename + " (new)") - title_item = QTableWidgetItem(" " + rf.tags['title']) - artist_item = QTableWidgetItem(" " + rf.tags['artist']) + title_item = QTableWidgetItem(" " + rf.tags["title"]) + artist_item = QTableWidgetItem(" " + rf.tags["artist"]) self.replacement_files.append(rf) row = self.ui.tableWidget.rowCount() diff --git a/app/music.py b/app/music.py index d5a295f..453eb24 100644 --- a/app/music.py +++ b/app/music.py @@ -1,18 +1,23 @@ +# Standard library imports +import datetime as dt import threading +from time import sleep +from typing import Optional + +# Third party imports 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 - +# PyQt imports from PyQt6.QtCore import ( QRunnable, QThreadPool, ) +# App imports +from config import Config +from helpers import file_is_unreadable +from log import log + lock = threading.Lock() @@ -57,6 +62,7 @@ class Music: self.VLC = vlc.Instance() self.player = None self.max_volume = Config.VOLUME_VLC_DEFAULT + self.start_dt: Optional[dt.datetime] = None def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: """ @@ -83,6 +89,21 @@ class Music: pool = QThreadPool.globalInstance() fader = FadeTrack(p, fade_seconds=fade_seconds) pool.start(fader) + self.start_dt = None + + def get_playtime(self) -> int: + """ + Return number of milliseconds current track has been playing or + zero if not playing. The vlc function get_time() only updates 3-4 + times a second; this function has much better resolution. + """ + + if self.start_dt is None: + return 0 + + now = dt.datetime.now() + elapsed_seconds = (now - self.start_dt).total_seconds() + return int(elapsed_seconds * 1000) def get_position(self) -> Optional[float]: """Return current position""" @@ -91,6 +112,23 @@ class Music: return None return self.player.get_position() + def is_playing(self) -> bool: + """Return True if playing""" + + # There is a discrete time between starting playing a track and + # player.is_playing() returning True, so assume playing if less + # than Config.PLAY_SETTLE microseconds have passed since + # starting play. + return ( + self.player is not None + and self.start_dt is not None + and ( + self.player.is_playing() + or (dt.datetime.now() - self.start_dt) + < dt.timedelta(microseconds=Config.PLAY_SETTLE) + ) + ) + def play(self, path: str, position: Optional[float] = None) -> None: """ Start playing the track at path. @@ -109,8 +147,10 @@ class Music: if self.player: _ = self.player.play() self.set_volume(self.max_volume) + if position: self.player.set_position(position) + self.start_dt = dt.datetime.now() def set_volume(self, volume=None, set_default=True) -> None: """Set maximum volume used for player""" @@ -125,6 +165,17 @@ class Music: volume = Config.VOLUME_VLC_DEFAULT self.player.audio_set_volume(volume) + # Ensure volume correct + # For as-yet unknown reasons. sometimes the volume gets + # reset to zero within 200mS or so of starting play. This + # only happened since moving to Debian 12, which uses + # Pipewire for sound (which may be irrelevant). + for _ in range(3): + current_volume = self.player.audio_get_volume() + if current_volume < volume: + self.player.audio_set_volume(volume) + log.debug(f"Reset from {volume=}") + sleep(0.1) def stop(self) -> float: """Immediately stop playing""" @@ -136,6 +187,7 @@ class Music: p = self.player self.player = None + self.start_dt = None with lock: position = p.get_position() diff --git a/app/musicmuster.py b/app/musicmuster.py index 310378f..09eb9e0 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -781,21 +781,6 @@ class Window(QMainWindow, Ui_MainWindow): self.stop_playing(fade=True) - def get_playtime(self) -> int: - """ - Return number of milliseconds current track has been playing or - zero if not playing. The vlc function get_time() only updates 3-4 - times a second; this function has much better resolution. - """ - - if track_sequence.now.track_id is None or track_sequence.now.start_time is None: - return 0 - - now = dt.datetime.now() - track_start = track_sequence.now.start_time - elapsed_seconds = (now - track_start).total_seconds() - return int(elapsed_seconds * 1000) - def hide_played(self): """Toggle hide played tracks""" @@ -1146,7 +1131,6 @@ class Window(QMainWindow, Ui_MainWindow): - Clear next track - Restore volume if -3dB active - Play (new) current track. - - Ensure 100% volume - Show closing volume graph - Notify model - Note that track is now playing @@ -1167,13 +1151,10 @@ class Window(QMainWindow, Ui_MainWindow): # If return is pressed during first PLAY_NEXT_GUARD_MS then # default to NOT playing the next track, else default to # playing it. - default_yes: bool = ( - track_sequence.now.start_time is not None - and ( - (dt.datetime.now() - track_sequence.now.start_time).total_seconds() - * 1000 - > Config.PLAY_NEXT_GUARD_MS - ) + default_yes: bool = track_sequence.now.start_time is not None and ( + (dt.datetime.now() - track_sequence.now.start_time).total_seconds() + * 1000 + > Config.PLAY_NEXT_GUARD_MS ) if not helpers.ask_yes_no( "Track playing", @@ -1215,20 +1196,6 @@ class Window(QMainWindow, Ui_MainWindow): return self.music.play(track_sequence.now.path, position) - # Ensure 100% volume - # For as-yet unknown reasons. sometimes the volume gets - # reset to zero within 200mS or so of starting play. This - # only happened since moving to Debian 12, which uses - # Pipewire for sound (which may be irrelevant). - for _ in range(3): - if self.music.player: - volume = self.music.player.audio_get_volume() - if volume < Config.VOLUME_VLC_DEFAULT: - self.music.set_volume() - log.debug(f"Reset from {volume=}") - break - sleep(0.1) - # Show closing volume graph if track_sequence.now.fade_graph: track_sequence.now.fade_graph.plot() @@ -1715,20 +1682,8 @@ class Window(QMainWindow, Ui_MainWindow): return # If track is playing, update track clocks time and colours - # There is a discrete time between starting playing a track and - # player.is_playing() returning True, so assume playing if less - # than Config.PLAY_SETTLE microseconds have passed since - # starting play. - if ( - self.music.player - and track_sequence.now.start_time - and ( - self.music.player.is_playing() - or (dt.datetime.now() - track_sequence.now.start_time) - < dt.timedelta(microseconds=Config.PLAY_SETTLE) - ) - ): - playtime = self.get_playtime() + if self.music.player and self.music.player.is_playing(): + playtime = self.music.player.get_playtime() time_to_fade = track_sequence.now.fade_at - playtime time_to_silence = track_sequence.now.silence_at - playtime diff --git a/app/playlists.py b/app/playlists.py index 388018d..d436fec 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -614,7 +614,7 @@ class PlaylistTab(QTableView): else: result = self.source_model.get_row_track_path(model_row_number) - log.info(f"get_selected_row_track_path() returned: {result=}") + log.debug(f"get_selected_row_track_path() returned: {result=}") return result def get_selected_rows(self) -> List[int]: