diff --git a/gui/audio_preview.py b/gui/audio_preview.py index b7804f3..5553ff7 100644 --- a/gui/audio_preview.py +++ b/gui/audio_preview.py @@ -1,32 +1,61 @@ from pydub import AudioSegment import simpleaudio as sa from tkinter import messagebox +import threading +import time _play_obj = None +_play_thread: threading.Thread | None = None +_stop_event: threading.Event | None = None -def play_preview(path: str, start_ms: int = 30000, duration_ms: int = 15000) -> None: - """Play a short preview of the audio file at ``path``. - - Parameters - ---------- - path : str - File path of the audio clip. - start_ms : int, optional - Starting point in milliseconds, by default 30000. - duration_ms : int, optional - Duration of the preview in milliseconds, by default 15000. - """ + +def stop_preview() -> None: + """Stop any currently playing preview.""" + global _play_obj, _play_thread, _stop_event + if _stop_event is not None: + _stop_event.set() + if _play_obj and _play_obj.is_playing(): + _play_obj.stop() + _play_thread = None + _stop_event = None + _play_obj = None + + +def _loop_play(clip: AudioSegment, stop_evt: threading.Event) -> None: global _play_obj - try: - audio = AudioSegment.from_file(path) - clip = audio[start_ms : start_ms + duration_ms] - if _play_obj and _play_obj.is_playing(): - _play_obj.stop() + while not stop_evt.is_set(): _play_obj = sa.play_buffer( clip.raw_data, num_channels=clip.channels, bytes_per_sample=clip.sample_width, sample_rate=clip.frame_rate, ) + while _play_obj.is_playing() and not stop_evt.is_set(): + time.sleep(0.1) + if stop_evt.is_set(): + _play_obj.stop() + break + time.sleep(0.25) + + +def play_preview(path: str, start_ms: int = 30000, duration_ms: int = 30000) -> None: + """Play a looping preview of ``path``. + + The snippet plays for ``duration_ms`` starting at ``start_ms`` then pauses + briefly before looping until :func:`stop_preview` is called. + """ + global _play_thread, _stop_event + + stop_preview() + try: + audio = AudioSegment.from_file(path) + clip = audio[start_ms : start_ms + duration_ms] except Exception as exc: messagebox.showerror("Playback Error", str(exc)) + return + + _stop_event = threading.Event() + _play_thread = threading.Thread( + target=_loop_play, args=(clip, _stop_event), daemon=True + ) + _play_thread.start() diff --git a/main_gui.py b/main_gui.py index 80b4ba1..2bd7619 100644 --- a/main_gui.py +++ b/main_gui.py @@ -44,6 +44,7 @@ from controllers.import_controller import import_new_files from controllers.genre_list_controller import list_unique_genres from controllers.highlight_controller import play_snippet, PYDUB_AVAILABLE +from gui.audio_preview import play_preview as _play_clip, stop_preview as _stop_clip from io import BytesIO from PIL import Image, ImageTk from mutagen import File as MutagenFile @@ -2250,6 +2251,7 @@ def _replace_selected(self): # ── Quality Checker Helpers ───────────────────────────────────────── def clear_quality_view(self) -> None: + _stop_clip() for w in self.qc_inner.winfo_children(): w.destroy() @@ -2279,13 +2281,11 @@ def _play_preview(self, path: str) -> None: ) return - def task() -> None: - try: - play_snippet(path) - except Exception as e: - self.after(0, lambda: messagebox.showerror("Playback failed", str(e))) - - threading.Thread(target=task, daemon=True).start() + try: + _stop_clip() + _play_clip(path) + except Exception as e: + messagebox.showerror("Playback failed", str(e)) def _load_thumbnail(self, path: str, size: int = 100) -> ImageTk.PhotoImage: img = None