Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions gui/audio_preview.py
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 7 additions & 7 deletions main_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down