From ad391aedc6302f78cb6726c747338daa9c7f860b Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Thu, 24 Apr 2025 11:49:04 +0100 Subject: [PATCH] Rewrite thread management for fade graph generation --- app/playlistrow.py | 122 +++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/app/playlistrow.py b/app/playlistrow.py index eef378b..ad44abb 100644 --- a/app/playlistrow.py +++ b/app/playlistrow.py @@ -1,6 +1,6 @@ # Standard library imports +from collections import deque import datetime as dt -from typing import Any # PyQt imports from PyQt6.QtCore import ( @@ -25,6 +25,59 @@ import ds import helpers +class FadeGraphGenerator(QObject): + finished = pyqtSignal(object, object) + task_completed = pyqtSignal() + + def generate_graph(self, plr: "PlaylistRow") -> None: + fade_graph = FadeCurve(plr.path, plr.fade_at, plr.silence_at) + if not fade_graph: + log.error(f"Failed to create FadeCurve for {plr=}") + return + + self.finished.emit(plr, fade_graph) + self.task_completed.emit() + + +class FadegraphThreadController(QObject): + def __init__(self): + super().__init__() + self._thread = None + self._generator = None + self._request_queue = deque() + + def generate_fade_graph(self, playlist_row): + self._request_queue.append(playlist_row) # Use append for enqueue with deque + if self._thread is None or not self._thread.isRunning(): + self._start_next_generation() + + def _start_next_generation(self): + if not self._request_queue: # Check if deque is empty + return + playlist_row = self._request_queue.popleft() # Use popleft for dequeue with deque + self._start_thread(playlist_row) + + def _start_thread(self, playlist_row): + self._thread = QThread() + self._generator = FadeGraphGenerator() + self._generator.moveToThread(self._thread) + self._generator.finished.connect(lambda row, graph: row.attach_fade_graph(graph)) + self._generator.task_completed.connect(self._cleanup_thread) + self._thread.started.connect(lambda: self._generator.generate_graph(playlist_row)) + self._thread.start() + + def _cleanup_thread(self): + if self._thread: + self._thread.quit() + self._thread.wait() + self._thread.deleteLater() + self._thread = None + self._generator.deleteLater() + self._generator = None + # Start the next request if any + self._start_next_generation() + + class PlaylistRow: """ Object to manage playlist row and track. @@ -41,7 +94,7 @@ class PlaylistRow: self.signals = MusicMusterSignals() self.end_of_track_signalled: bool = False self.end_time: dt.datetime | None = None - self.fade_graph: Any | None = None + self.fade_graph: FadeCurve | None = None self.fade_graph_start_updates: dt.datetime | None = None self.forecast_end_time: dt.datetime | None = None self.forecast_start_time: dt.datetime | None = None @@ -51,6 +104,7 @@ class PlaylistRow: self.row_bg: str | None = None self.row_fg: str | None = None self.start_time: dt.datetime | None = None + self.fadegraph_thread_controller = FadegraphThreadController() def __repr__(self) -> str: track_id = None @@ -223,6 +277,9 @@ class PlaylistRow: # the change to the database. self.dto.row_number = value + def attach_fade_graph(self, fade_graph): + self.fade_graph = fade_graph + def drop3db(self, enable: bool) -> None: """ If enable is true, drop output by 3db else restore to full volume @@ -337,40 +394,6 @@ class PlaylistRow: self.fade_graph.tick(self.time_playing()) -class _AddFadeCurve(QObject): - """ - Initialising a fade curve introduces a noticeable delay so carry out in - a thread. - """ - - finished = pyqtSignal() - - def __init__( - self, - plr: PlaylistRow, - track_path: str, - track_fade_at: int, - track_silence_at: int, - ) -> None: - super().__init__() - self.plr = plr - self.track_path = track_path - self.track_fade_at = track_fade_at - self.track_silence_at = track_silence_at - - def run(self) -> None: - """ - Create fade curve and add to PlaylistTrack object - """ - - fc = FadeCurve(self.track_path, self.track_fade_at, self.track_silence_at) - if not fc: - log.error(f"Failed to create FadeCurve for {self.track_path=}") - else: - self.plr.fade_graph = fc - self.finished.emit() - - class FadeCurve: GraphWidget: PlotWidget | None = None @@ -388,10 +411,10 @@ class FadeCurve: # Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE # milliseconds before fade starts to silence - self.start_ms: int = max( + self.start_ms = max( 0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1 ) - self.end_ms: int = track_silence_at + self.end_ms = track_silence_at audio_segment = audio[self.start_ms : self.end_ms] self.graph_array = np.array(audio_segment.get_array_of_samples()) @@ -470,7 +493,7 @@ class TrackSequence: self.next = None else: self.next = plr - self.create_fade_graph() + plr.fadegraph_thread_controller.generate_fade_graph(plr) def move_next_to_current(self) -> None: """ @@ -506,27 +529,6 @@ class TrackSequence: self.next = self.previous self.previous = None - def create_fade_graph(self) -> None: - """ - Initialise and add FadeCurve in a thread as it's slow - """ - - self.fadecurve_thread = QThread() - if self.next is None: - raise ApplicationError("hell in a handcart") - self.worker = _AddFadeCurve( - self.next, - track_path=self.next.path, - track_fade_at=self.next.fade_at, - track_silence_at=self.next.silence_at, - ) - self.worker.moveToThread(self.fadecurve_thread) - self.fadecurve_thread.started.connect(self.worker.run) - self.worker.finished.connect(self.fadecurve_thread.quit) - self.worker.finished.connect(self.worker.deleteLater) - self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater) - self.fadecurve_thread.start() - def update(self) -> None: """ If a PlaylistRow is edited (moved, title changed, etc), the