-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathactive_track.py
More file actions
192 lines (159 loc) · 6.93 KB
/
active_track.py
File metadata and controls
192 lines (159 loc) · 6.93 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
from just_playback import Playback
from tinytag import TinyTag
class ActiveTrack():
def __init__(self, path:str = "", volume:float=1.0, gui_manager=None, tk_after=None):
self._path = ""
self._track = Playback()
self._duration, self._progress = 0.0, 0.0
self._volume = volume
self._loop = False
# Reference to be able to send events to the GUI
self._gui = gui_manager
# Enable volume fading support through TK. (no extra multithreading necessary)
self._scheduler = tk_after # Any TK reference from the UI that supports .after()
self._fade_job = None
self._fading = False
self._meta = None
self.kbps, self.khz, self.file_size, self.album_track, self.channels = 0.0, 0, 0, 0, 0
self.title, self.artist, self.album = "", "", ""
self._status = "Unloaded"
self.load(path, False)
def status(self) -> str: return self._status
def isLoaded(self) -> bool: return bool(self._path)
def isPlaying(self) -> bool: return self._track.playing
def isPaused(self) -> bool: return self._track.paused
def loops(self) -> bool: return self._loop
@property
def duration(self): return self._duration
@property
def volume(self): return self._volume
@property
def meta(self): return self._meta
@property
def info(self):
out = self.title
if self.artist: out += f" - {self.artist}"
if self.album: out += f" - {self.album}"
if self.album_track: out += f" [Track #{self.album_track}]"
return out.strip()
def load(self, path:str, autoplay=True, display_error=True):
if path:
self._reset()
try:
self._track.load_file(path)
self._duration = self._track.duration
self._populateMeta(path)
self._path = path
self._gui.registerTrack(path)
if autoplay: self.play()
else: self.stop()
except Exception as e:
self._reset()
if display_error:
try: # Play error sound
self._track.set_volume(1.0)
self._track.load_file("GUI/551543__phiiraco__8-bit-denyerror-sound.wav")
self._track.play()
except Exception as e2: print("Failed to play error sound:\n", e2)
e = str(e)
self.title = f"CANNOT PLAY FILE: '{path.replace("\\", "/").split('/')[-1]}'" if e == "MA_ERROR" else e
self._gui.unloadTrack()
self._gui.setStatics()
return self.isLoaded()
def unload(self): self._reset()
def play(self):
if self.isLoaded():
if not self._track.paused:
if self.isPlaying(): self._progress = 0.0
self._track.play()
self._track.seek(self._progress * self._duration)
else: self._track.resume()
self._status = "Playing"
self._track.set_volume(self._volume)
self._track.loop_at_end(self._loop)
self._gui.updateProgress()
self._gui.setState()
def pause(self):
if self.isLoaded() and self._status != "Stopped":
if not self._track.paused:
self._track.pause()
self._status = "Paused"
self._gui.setState()
else: self.play()
def stop(self):
if self.isLoaded():
self._status = "Stopped"
if self._track.active: self._track.stop()
self._progress = 0.0
self._gui.updateProgress(True)
self._gui.setState()
def setVolume(self, volume:float):
self._volume = volume
if self.isLoaded(): self._track.set_volume(volume)
def fadeVolume(self, end:float, duration:float, start:float = None, steps_per_sec:int = 32):
if self._scheduler is None: # Must have a scheduler (Tk root or widget)
raise RuntimeError("ActiveTrack.fadeVolume() requires a Tk scheduler with .after()")
# Cancel any existing fade
if self._fade_job is not None:
try: self._scheduler.after_cancel(self._fade_job)
except Exception: pass
self._fade_job = None
self._fading = True
# Collect/define the needed values for interpolation.
start = max(0.0, start if start is not None else float(self._volume))
end = max(0.0, float(end))
total_steps = max(1, int(duration * steps_per_sec))
step_time = int(1000 / steps_per_sec)
# Quadratic easing method.
def _ease_out(t: float) -> float: return 1 - (1 - t)**2
# A packable, recallable function that can be passed into Tk's .after() method.
def _step(i=0, cur=start):
if i >= total_steps:
self.setVolume(end)
self._gui.setVolume(cur, False)
self._fading = False
self._fade_job = None
return
t = (i + 1) / total_steps # Normalized progress
cur = start + (end - start) * _ease_out(t)
self.setVolume(cur)
self._gui.setVolume(cur, True)
self._fade_job = self._scheduler.after(step_time, lambda: _step(i+1, cur))
_step()
def setProgress(self, percent:float) -> float:
self._progress = percent
secs = self.getSeconds()
if self.isLoaded():
self._track.seek(self._progress * self._duration)
return secs
return 0.0
def getSeconds(self) -> float:
if self.isLoaded(): return self._track.curr_pos
return 0.0
def getPercent(self):
if self.isLoaded(): return self._track.curr_pos / self._duration
return 0.0
def setLoop(self, will_loop:bool):
self._loop = will_loop # loop_at_end() will restart playback if at end and stopped so this is needed.
if not will_loop or self.isPlaying(): self._track.loop_at_end(will_loop)
def _reset(self):
try: self._track.load_file("")
except: pass
self._path = ""
self._duration, self._progress = 0.0, 0.0
self._meta = None
self.kbps, self.khz, self.file_size, self.album_track, self.channels = 0.0, 0, 0, 0, 0
self.title, self.artist, self.album = "No track loaded.", "", ""
self._status = "Unloaded"
def _populateMeta(self, path:str):
self._meta = TinyTag.get(path)
# Audio attributes
self.kbps = self._meta.bitrate
self.khz = self._meta.samplerate
self.file_size = self._meta.filesize
self.channels = self._meta.channels
# Tag metadata
self.title = self._meta.title or self._meta.filename.split("/")[-1].split("\\")[-1] # Split for Win & Linux
self.artist = self._meta.artist or ""
self.album = self._meta.album or ""
self.album_track = self._meta.track or ""