-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplayback.py
More file actions
143 lines (112 loc) · 4.95 KB
/
playback.py
File metadata and controls
143 lines (112 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import tkinter as tk
import numpy as np
from tkinter import messagebox
from typing import Literal, Optional
from audio_stream import _AudioStreamPlayer
try:
import customtkinter as ctk # type: ignore[import]
except ImportError:
import types
_dummy = types.SimpleNamespace()
_dummy.CTkFrame = tk.Frame
_dummy.CTkLabel = tk.Label
_dummy.CTkButton = tk.Button
ctk = _dummy
class _PlaybackTrack:
def __init__(self, parent: ctk.CTkFrame, label: str, logger) -> None:
self._logger = logger
self._player: Optional[_AudioStreamPlayer] = None
self._duration_seconds = 0.0
self._is_seeking = False
self.frame = ctk.CTkFrame(parent, fg_color="#222222")
self.frame.grid_columnconfigure(2, weight=1)
self._label = ctk.CTkLabel(self.frame, text=label, width=120, anchor="w")
self._label.grid(row=0, column=0, sticky="w", padx=(6, 8), pady=6)
self._play_button = ctk.CTkButton(self.frame, text="Play", width=70, command=self._toggle_playback)
self._play_button.grid(row=0, column=1, sticky="w", padx=(0, 8), pady=6)
self._scale_var = tk.DoubleVar(value=0.0)
self._scale = tk.Scale(self.frame, from_=0.0, to=1.0, orient="horizontal", showvalue=False,
resolution=0.01, variable=self._scale_var, length=420,)
self._scale.grid(row=0, column=2, sticky="ew", padx=(0, 8), pady=6)
self._time_label = ctk.CTkLabel(self.frame, text="00:00 / 00:00", width=110, anchor="e")
self._time_label.grid(row=0, column=3, sticky="e", padx=(0, 6), pady=6)
self._scale.bind("<ButtonPress-1>", self._on_seek_start)
self._scale.bind("<B1-Motion>", self._on_seek_move)
self._scale.bind("<ButtonRelease-1>", self._on_seek_end)
self._set_enabled(False)
def _set_enabled(self, enabled: bool) -> None:
state: Literal["normal", "disabled"] = "normal" if enabled else "disabled"
self._play_button.configure(state=state)
self._scale.configure(state=state)
def set_audio(self, audio_data: np.ndarray, sample_rate: int) -> None:
if self._player is not None:
self._player.close()
try:
self._player = _AudioStreamPlayer(audio_data, sample_rate)
except (RuntimeError, ValueError) as exc:
self._player = None
self._set_enabled(False)
self._logger(f"Playback error: {exc}")
messagebox.showwarning("Playback unavailable", f"{exc}")
return
self._duration_seconds = self._player.duration_seconds
self._scale.configure(to=self._duration_seconds)
self._scale_var.set(0.0)
self._update_time_label(0.0)
self._play_button.configure(text="Play")
self._set_enabled(True)
def update_ui(self) -> None:
if self._player is None:
return
if self._is_seeking:
return
position = self._player.get_position_seconds()
self._scale_var.set(position)
self._update_time_label(position)
if self._player.is_playing:
self._play_button.configure(text="Pause")
else:
self._play_button.configure(text="Play")
def _toggle_playback(self) -> None:
if self._player is None:
messagebox.showwarning("Playback unavailable", "No audio loaded for this track.")
return
if self._player.is_playing:
self._player.pause()
self._play_button.configure(text="Play")
else:
self._player.play()
self._play_button.configure(text="Pause")
def _on_seek_start(self, event) -> None:
self._is_seeking = True
if self._player is None:
return
width = max(1, self._scale.winfo_width())
click_ratio = max(0.0, min(1.0, event.x / width))
target_seconds = click_ratio * self._duration_seconds
self._scale_var.set(target_seconds)
self._player.set_position_seconds(target_seconds)
self._update_time_label(target_seconds)
def _on_seek_move(self, _event) -> None:
if self._player is None:
return
value = float(self._scale_var.get())
self._update_time_label(value)
def _on_seek_end(self, _event) -> None:
if self._player is None:
self._is_seeking = False
return
value = float(self._scale_var.get())
self._player.set_position_seconds(value)
self._update_time_label(value)
self._is_seeking = False
def _update_time_label(self, current_seconds: float) -> None:
total = self._format_time(self._duration_seconds)
current = self._format_time(current_seconds)
self._time_label.configure(text=f"{current} / {total}")
@staticmethod
def _format_time(seconds: float) -> str:
seconds = max(0.0, seconds)
minutes = int(seconds // 60)
remainder = int(seconds % 60)
return f"{minutes:02d}:{remainder:02d}"