audio-tuner-gui 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- audio_tuner_gui/__init__.py +39 -0
- audio_tuner_gui/analysis.py +717 -0
- audio_tuner_gui/common.py +350 -0
- audio_tuner_gui/display.py +605 -0
- audio_tuner_gui/export.py +563 -0
- audio_tuner_gui/file_selector.py +435 -0
- audio_tuner_gui/icons/audio_tuner_icon.ico +0 -0
- audio_tuner_gui/icons/audio_tuner_icon.svg +215 -0
- audio_tuner_gui/icons/audio_tuner_icon_hires.svg +215 -0
- audio_tuner_gui/icons/audio_tuner_logo_dark.svg +563 -0
- audio_tuner_gui/icons/audio_tuner_logo_light.svg +563 -0
- audio_tuner_gui/icons/black-logo.svg +24 -0
- audio_tuner_gui/icons/folder.png +0 -0
- audio_tuner_gui/icons/gpl-v3-logo_red.svg +223 -0
- audio_tuner_gui/icons/gpl-v3-logo_white.svg +224 -0
- audio_tuner_gui/icons/preferences-other.png +0 -0
- audio_tuner_gui/icons/process-stop.png +0 -0
- audio_tuner_gui/icons/white-logo.svg +24 -0
- audio_tuner_gui/log_viewer.py +154 -0
- audio_tuner_gui/option_panel.py +683 -0
- audio_tuner_gui/player.py +757 -0
- audio_tuner_gui/scripts/__init__.py +0 -0
- audio_tuner_gui/scripts/tuner_gui.py +863 -0
- audio_tuner_gui-0.9.1.dist-info/METADATA +93 -0
- audio_tuner_gui-0.9.1.dist-info/RECORD +28 -0
- audio_tuner_gui-0.9.1.dist-info/WHEEL +4 -0
- audio_tuner_gui-0.9.1.dist-info/entry_points.txt +2 -0
- audio_tuner_gui-0.9.1.dist-info/licenses/COPYING +674 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Audio Tuner.
|
|
4
|
+
#
|
|
5
|
+
# Copyright 2025, 2026 Jessie Blue Cassell <bluesloth600@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU General Public License
|
|
18
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
"""Audio player for the GUI."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__author__ = 'Jessie Blue Cassell'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
'Player',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
import atexit
|
|
33
|
+
|
|
34
|
+
from PyQt6.QtCore import (
|
|
35
|
+
Qt,
|
|
36
|
+
QObject,
|
|
37
|
+
pyqtSignal,
|
|
38
|
+
)
|
|
39
|
+
from PyQt6.QtWidgets import (
|
|
40
|
+
QWidget,
|
|
41
|
+
QVBoxLayout,
|
|
42
|
+
QHBoxLayout,
|
|
43
|
+
QPushButton,
|
|
44
|
+
QTableView,
|
|
45
|
+
)
|
|
46
|
+
from PyQt6.QtGui import (
|
|
47
|
+
QStandardItemModel,
|
|
48
|
+
QStandardItem,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
import audio_tuner_gui.log_viewer as lv
|
|
52
|
+
import audio_tuner.common as com
|
|
53
|
+
|
|
54
|
+
mpv_error = com.mpv_error
|
|
55
|
+
if mpv_error is None:
|
|
56
|
+
import mpv
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _DeviceView(QTableView):
|
|
60
|
+
def __init__(self):
|
|
61
|
+
super().__init__()
|
|
62
|
+
|
|
63
|
+
self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
64
|
+
self.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
|
65
|
+
self.setShowGrid(False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _DeviceWindow(QWidget):
|
|
69
|
+
Select = pyqtSignal(str)
|
|
70
|
+
|
|
71
|
+
def __init__(self, parent):
|
|
72
|
+
super().__init__(parent)
|
|
73
|
+
|
|
74
|
+
self.setWindowFlag(Qt.WindowType.Window)
|
|
75
|
+
self.setWindowTitle('Audio Devices')
|
|
76
|
+
|
|
77
|
+
self.vbox = QVBoxLayout(self)
|
|
78
|
+
|
|
79
|
+
self._init_device_view()
|
|
80
|
+
|
|
81
|
+
self.vbox.addWidget(self.view)
|
|
82
|
+
|
|
83
|
+
self._init_button_panel()
|
|
84
|
+
self.vbox.addWidget(self.button_panel)
|
|
85
|
+
|
|
86
|
+
self.resize(600, 400)
|
|
87
|
+
|
|
88
|
+
def _init_device_view(self):
|
|
89
|
+
model = QStandardItemModel(0, 2, self)
|
|
90
|
+
headers = ('Name', 'Description')
|
|
91
|
+
model.setHorizontalHeaderLabels(headers)
|
|
92
|
+
|
|
93
|
+
view = _DeviceView()
|
|
94
|
+
view.setModel(model)
|
|
95
|
+
|
|
96
|
+
view.setColumnWidth(0, 200)
|
|
97
|
+
view.setColumnWidth(1, 400)
|
|
98
|
+
|
|
99
|
+
self.model = model
|
|
100
|
+
self.view = view
|
|
101
|
+
|
|
102
|
+
def _init_button_panel(self):
|
|
103
|
+
self.button_panel = QWidget()
|
|
104
|
+
self.hbox = QHBoxLayout(self.button_panel)
|
|
105
|
+
|
|
106
|
+
self.cancel_button = QPushButton('Cancel')
|
|
107
|
+
self.cancel_button.setShortcut('Esc')
|
|
108
|
+
self.hbox.addWidget(self.cancel_button)
|
|
109
|
+
self.cancel_button.clicked.connect(self.close)
|
|
110
|
+
|
|
111
|
+
self.select_button = QPushButton('OK')
|
|
112
|
+
self.select_button.setShortcut('Return')
|
|
113
|
+
self.hbox.addWidget(self.select_button)
|
|
114
|
+
self.select_button.clicked.connect(self.select)
|
|
115
|
+
|
|
116
|
+
def set_device_list(self, devices, current_device):
|
|
117
|
+
self.model.setRowCount(0)
|
|
118
|
+
selection = None
|
|
119
|
+
for i, device in enumerate(devices):
|
|
120
|
+
name = device['name']
|
|
121
|
+
description = device['description']
|
|
122
|
+
if name == current_device:
|
|
123
|
+
selection = i
|
|
124
|
+
name_item = QStandardItem(name)
|
|
125
|
+
description_item = QStandardItem(description)
|
|
126
|
+
self.model.appendRow((name_item, description_item))
|
|
127
|
+
if selection is not None:
|
|
128
|
+
self.view.selectRow(selection)
|
|
129
|
+
|
|
130
|
+
def select(self):
|
|
131
|
+
row = self.view.selectedIndexes()[0].row()
|
|
132
|
+
name = self.model.item(row, 0).text()
|
|
133
|
+
self.Select.emit(name)
|
|
134
|
+
self.close()
|
|
135
|
+
|
|
136
|
+
def close(self):
|
|
137
|
+
self.hide()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _hms_to_seconds(time):
|
|
142
|
+
hrmin, c, sec = time.rpartition(':')
|
|
143
|
+
hr, c, minute = hrmin.rpartition(':')
|
|
144
|
+
|
|
145
|
+
if hr:
|
|
146
|
+
hrf = float(hr)
|
|
147
|
+
else:
|
|
148
|
+
hrf = 0
|
|
149
|
+
if minute:
|
|
150
|
+
minutef = float(minute)
|
|
151
|
+
else:
|
|
152
|
+
minutef = 0
|
|
153
|
+
if sec:
|
|
154
|
+
secf = float(sec)
|
|
155
|
+
else:
|
|
156
|
+
secf = 0
|
|
157
|
+
|
|
158
|
+
minutef += hrf * 60
|
|
159
|
+
secf += minutef * 60
|
|
160
|
+
|
|
161
|
+
return secf
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Player(QObject):
|
|
165
|
+
"""Audio player that uses libmpv as a backend. Inherits from
|
|
166
|
+
QObject. It plays audio from the start position to the end position
|
|
167
|
+
with pitch and tempo correction, with the `start`, `end`, `pitch`
|
|
168
|
+
and `tempo` parameters set by the `play` and `update_corrections`
|
|
169
|
+
methods.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
parent
|
|
174
|
+
The parent widget.
|
|
175
|
+
|
|
176
|
+
Attributes
|
|
177
|
+
----------
|
|
178
|
+
time_number : int
|
|
179
|
+
The total number of times a TickPos or TickRem signal has been
|
|
180
|
+
emitted. This can be used for diagnostic purposes. If it
|
|
181
|
+
increases too much faster than 60 times per second, libmpv may
|
|
182
|
+
be misbehaving.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
TickPos = pyqtSignal(bool, int)
|
|
186
|
+
"""Signal emitted when the time from the beginning of the audio
|
|
187
|
+
changes. Although it has one second resolution, it may be emitted
|
|
188
|
+
more than once per second. The exact rate is controlled by libmpv.
|
|
189
|
+
Note that this is the time from the beginning of the audio, not the
|
|
190
|
+
`start` position. If tempo correction is applied, it won't change
|
|
191
|
+
the time, but it will tick at a rate other than 1 second per second.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
bool
|
|
196
|
+
True if audio is loaded and the time is valid, False otherwise.
|
|
197
|
+
int
|
|
198
|
+
The time in seconds.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
TickRem = pyqtSignal(bool, int)
|
|
202
|
+
"""Signal emitted when the time remaining to the end of the audio
|
|
203
|
+
changes. Although it has one second resolution, it may be emitted
|
|
204
|
+
more than once per second. The exact rate is controlled by libmpv.
|
|
205
|
+
Note that this is the time to the end of the audio, not the `end`
|
|
206
|
+
position. If tempo correction is applied, it won't change the time,
|
|
207
|
+
but it will tick at a rate other than 1 second per second.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
bool
|
|
212
|
+
True if audio is loaded and the time is valid, False otherwise.
|
|
213
|
+
int
|
|
214
|
+
The time in seconds.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
Percent = pyqtSignal(int)
|
|
218
|
+
"""Signal emitted to indicate the position in the audio as a
|
|
219
|
+
percentage. Unlike TickPos and TickRem, this does take into account
|
|
220
|
+
the `start` and `end` positions, so it will progresses from 0% to
|
|
221
|
+
100% even if `start` to `end` only covers a small portion of the
|
|
222
|
+
audio.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
int
|
|
227
|
+
The percentage
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
PauseStatus = pyqtSignal(bool)
|
|
231
|
+
"""Signal emitted when the pause status changes.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
bool
|
|
236
|
+
True if paused, False otherwise.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
AddToLog = pyqtSignal(str, int)
|
|
240
|
+
"""Signal emitted to request a message be added to the log.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
str
|
|
245
|
+
The message.
|
|
246
|
+
int
|
|
247
|
+
The severity level as defined in audio_tuner_gui.log_viewer
|
|
248
|
+
(LOG_LEVEL_ERROR, LOG_LEVEL_WARNING or LOG_LEVEL_NORMAL).
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(self, parent):
|
|
252
|
+
super().__init__()
|
|
253
|
+
|
|
254
|
+
self._player = None
|
|
255
|
+
|
|
256
|
+
self._time_pos = None
|
|
257
|
+
self._time_rem = None
|
|
258
|
+
self.time_number = 0
|
|
259
|
+
|
|
260
|
+
self._pitch_t = 1.0
|
|
261
|
+
self._pitch = 1.0
|
|
262
|
+
self._tempo = 1.0
|
|
263
|
+
self._start = None
|
|
264
|
+
self._end = None
|
|
265
|
+
self._duration = None
|
|
266
|
+
|
|
267
|
+
self._start_percent = 0
|
|
268
|
+
|
|
269
|
+
self._device = 'auto'
|
|
270
|
+
|
|
271
|
+
self._device_window = _DeviceWindow(parent)
|
|
272
|
+
self._device_window.Select.connect(self.set_device)
|
|
273
|
+
|
|
274
|
+
atexit.register(self._stop_player)
|
|
275
|
+
|
|
276
|
+
def _player_time_to_percent(self, time: str, endmode: bool=False):
|
|
277
|
+
p = None
|
|
278
|
+
|
|
279
|
+
if time is None:
|
|
280
|
+
time = 'none'
|
|
281
|
+
|
|
282
|
+
time = time.strip(' ')
|
|
283
|
+
if time.startswith('-'):
|
|
284
|
+
time = time.lstrip('-')
|
|
285
|
+
negative = True
|
|
286
|
+
else:
|
|
287
|
+
negative = False
|
|
288
|
+
|
|
289
|
+
if time == 'none':
|
|
290
|
+
return 100.0 if endmode else 0.0
|
|
291
|
+
|
|
292
|
+
if time.endswith('%'):
|
|
293
|
+
try:
|
|
294
|
+
p = float(time.rstrip('%'))
|
|
295
|
+
except ValueError:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
elif d := self.get_duration():
|
|
299
|
+
try:
|
|
300
|
+
secf = _hms_to_seconds(time)
|
|
301
|
+
except ValueError:
|
|
302
|
+
return None
|
|
303
|
+
p = 100.0 * (secf / d)
|
|
304
|
+
|
|
305
|
+
if p:
|
|
306
|
+
if negative:
|
|
307
|
+
return 100 - p
|
|
308
|
+
else:
|
|
309
|
+
return p
|
|
310
|
+
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def _player_time_to_seconds(self, time: str, endmode: bool=False):
|
|
314
|
+
p = None
|
|
315
|
+
d = self.get_duration()
|
|
316
|
+
|
|
317
|
+
if time is None:
|
|
318
|
+
time = 'none'
|
|
319
|
+
|
|
320
|
+
time = time.strip(' ')
|
|
321
|
+
if time.startswith('-'):
|
|
322
|
+
time = time.lstrip('-')
|
|
323
|
+
negative = True
|
|
324
|
+
else:
|
|
325
|
+
negative = False
|
|
326
|
+
|
|
327
|
+
if time == 'none':
|
|
328
|
+
return d if endmode else 0.0
|
|
329
|
+
|
|
330
|
+
if time.endswith('%'):
|
|
331
|
+
try:
|
|
332
|
+
p = float(time.rstrip('%'))
|
|
333
|
+
except ValueError:
|
|
334
|
+
return None
|
|
335
|
+
secf = (p / 100.0) * d
|
|
336
|
+
|
|
337
|
+
else:
|
|
338
|
+
try:
|
|
339
|
+
secf = _hms_to_seconds(time)
|
|
340
|
+
except ValueError:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
if secf:
|
|
344
|
+
if negative:
|
|
345
|
+
return d - secf
|
|
346
|
+
else:
|
|
347
|
+
return secf
|
|
348
|
+
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
def _percent_player_to_slider(self, percent):
|
|
352
|
+
if self._player is not None:
|
|
353
|
+
start = self._start
|
|
354
|
+
end = self._end
|
|
355
|
+
|
|
356
|
+
startf = self._player_time_to_percent(start)
|
|
357
|
+
endf = self._player_time_to_percent(end, endmode=True)
|
|
358
|
+
if startf is None or endf is None:
|
|
359
|
+
return percent
|
|
360
|
+
|
|
361
|
+
ret = (percent - startf) * (100.0 / (endf - startf))
|
|
362
|
+
return max(0.0, min(100.0, ret))
|
|
363
|
+
|
|
364
|
+
def _percent_slider_to_player(self, percent):
|
|
365
|
+
if self._player is not None:
|
|
366
|
+
start = self._start
|
|
367
|
+
end = self._end
|
|
368
|
+
|
|
369
|
+
startf = self._player_time_to_percent(start)
|
|
370
|
+
endf = self._player_time_to_percent(end, endmode=True)
|
|
371
|
+
if startf is None or endf is None:
|
|
372
|
+
return percent
|
|
373
|
+
|
|
374
|
+
ret = (percent * (endf - startf)) / 100 + startf
|
|
375
|
+
return max(0.0, min(100.0, ret))
|
|
376
|
+
|
|
377
|
+
def _ensure_player_is_running(self,
|
|
378
|
+
pitch=1.0,
|
|
379
|
+
tempo=1.0,
|
|
380
|
+
start=None,
|
|
381
|
+
end=None):
|
|
382
|
+
if mpv_error is not None:
|
|
383
|
+
self.AddToLog.emit(mpv_error, lv.LOG_LEVEL_ERROR)
|
|
384
|
+
raise OSError
|
|
385
|
+
|
|
386
|
+
if self._player is not None and self._player.core_shutdown:
|
|
387
|
+
self._unobserve()
|
|
388
|
+
self._player.terminate()
|
|
389
|
+
self._player = None
|
|
390
|
+
if self._player is None:
|
|
391
|
+
pitch_t = pitch / tempo
|
|
392
|
+
self._pitch_t = pitch_t
|
|
393
|
+
self._pitch = pitch
|
|
394
|
+
self._tempo = tempo
|
|
395
|
+
mpv_opts = {
|
|
396
|
+
'audio_pitch_correction': 'no',
|
|
397
|
+
'keep_open': 'yes',
|
|
398
|
+
'audio_device': self._device,
|
|
399
|
+
'audio_fallback_to_null': 'yes',
|
|
400
|
+
'af': f'@rb:rubberband=pitch-scale={pitch_t:.3f}',
|
|
401
|
+
'speed': f'{tempo:.3f}'
|
|
402
|
+
}
|
|
403
|
+
if start:
|
|
404
|
+
mpv_opts['start'] = start
|
|
405
|
+
self._start = start
|
|
406
|
+
if end:
|
|
407
|
+
mpv_opts['end'] = end
|
|
408
|
+
self._end = end
|
|
409
|
+
self._player = mpv.MPV(**mpv_opts)
|
|
410
|
+
|
|
411
|
+
if self._player.audio_device == 'null':
|
|
412
|
+
s = f'Unable to open output device {self._device}'
|
|
413
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_ERROR)
|
|
414
|
+
|
|
415
|
+
self._player.observe_property('time-pos',
|
|
416
|
+
self._time_pos_observer)
|
|
417
|
+
self._player.observe_property('time-remaining',
|
|
418
|
+
self._time_rem_observer)
|
|
419
|
+
self._player.observe_property('percent-pos',
|
|
420
|
+
self._percent_observer)
|
|
421
|
+
self._player.observe_property('pause',
|
|
422
|
+
self._pause_observer)
|
|
423
|
+
|
|
424
|
+
def _unobserve(self):
|
|
425
|
+
if self._player is not None:
|
|
426
|
+
try:
|
|
427
|
+
self._player.unobserve_property('time-pos',
|
|
428
|
+
self._time_pos_observer)
|
|
429
|
+
except ValueError as err:
|
|
430
|
+
self.AddToLog.emit(str(err), lv.LOG_LEVEL_ERROR)
|
|
431
|
+
try:
|
|
432
|
+
self._player.unobserve_property('time-remaining',
|
|
433
|
+
self._time_rem_observer)
|
|
434
|
+
except ValueError as err:
|
|
435
|
+
self.AddToLog.emit(str(err), lv.LOG_LEVEL_ERROR)
|
|
436
|
+
try:
|
|
437
|
+
self._player.unobserve_property('percent-pos',
|
|
438
|
+
self._percent_observer)
|
|
439
|
+
except ValueError as err:
|
|
440
|
+
self.AddToLog.emit(str(err), lv.LOG_LEVEL_ERROR)
|
|
441
|
+
try:
|
|
442
|
+
self._player.unobserve_property('pause',
|
|
443
|
+
self._pause_observer)
|
|
444
|
+
except ValueError as err:
|
|
445
|
+
self.AddToLog.emit(str(err), lv.LOG_LEVEL_ERROR)
|
|
446
|
+
|
|
447
|
+
def _restart_mpv(self, pitch=1.0, tempo=1.0, start=None, end=None):
|
|
448
|
+
if self._player is None:
|
|
449
|
+
self._ensure_player_is_running(pitch, tempo, start, end)
|
|
450
|
+
else:
|
|
451
|
+
path = com.raw_to_string(self._player.raw.path)
|
|
452
|
+
pause = self._player.pause
|
|
453
|
+
time_pos = self._player.time_pos
|
|
454
|
+
|
|
455
|
+
self._stop_player()
|
|
456
|
+
|
|
457
|
+
if path is not None:
|
|
458
|
+
self.play(path, pitch, tempo, start, end)
|
|
459
|
+
self._player.pause = pause
|
|
460
|
+
self._player.time_pos = time_pos
|
|
461
|
+
else:
|
|
462
|
+
self._ensure_player_is_running(pitch, tempo, start, end)
|
|
463
|
+
self._player.pause = pause
|
|
464
|
+
|
|
465
|
+
def update_corrections(self, pitch=None, tempo=None, start=None, end=None):
|
|
466
|
+
"""Update the pitch correction, tempo correction, start time and
|
|
467
|
+
end time. See the `--start` option in the `mpv` manpage to find
|
|
468
|
+
out how to format the `start` and `end` parameters.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
pitch : float, optional
|
|
473
|
+
Pitch correction factor. Default None.
|
|
474
|
+
tempo : float, optional
|
|
475
|
+
Tempo correction factor. Default None.
|
|
476
|
+
start : str, optional
|
|
477
|
+
The position to start playing at. If None, start at the
|
|
478
|
+
beginning. This causes a seek iff the starting position set
|
|
479
|
+
is after the current position. Default None.
|
|
480
|
+
end : str, optional
|
|
481
|
+
The position to stop playing at. If None, end at the end.
|
|
482
|
+
Default None.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
if self._player is not None:
|
|
486
|
+
if pitch is None and tempo is None:
|
|
487
|
+
self._update_start_end(start, end)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
if pitch is None:
|
|
491
|
+
pitch = self._pitch
|
|
492
|
+
else:
|
|
493
|
+
self._pitch = pitch
|
|
494
|
+
if tempo is None:
|
|
495
|
+
tempo = self._tempo
|
|
496
|
+
pitch_t = pitch / tempo
|
|
497
|
+
try:
|
|
498
|
+
if abs(pitch_t - self._pitch_t) > .0011:
|
|
499
|
+
self._player.af_command('rb',
|
|
500
|
+
'set-pitch',
|
|
501
|
+
f'{pitch_t:.3f}')
|
|
502
|
+
self._pitch_t = pitch_t
|
|
503
|
+
if abs(tempo - self._tempo) > .0011:
|
|
504
|
+
self._player.speed = f'{tempo:.3f}'
|
|
505
|
+
self._tempo = tempo
|
|
506
|
+
self._update_start_end(start, end)
|
|
507
|
+
except SystemError:
|
|
508
|
+
self._restart_mpv(pitch, tempo, start, end)
|
|
509
|
+
|
|
510
|
+
def _update_start_end(self, start, end):
|
|
511
|
+
if self._player is not None:
|
|
512
|
+
try:
|
|
513
|
+
if start:
|
|
514
|
+
self._player.start = start
|
|
515
|
+
self._start = start
|
|
516
|
+
else:
|
|
517
|
+
self._player.start = 'none'
|
|
518
|
+
self._start = None
|
|
519
|
+
if end:
|
|
520
|
+
self._player.end = end
|
|
521
|
+
self._end = end
|
|
522
|
+
else:
|
|
523
|
+
self._player.end = 'none'
|
|
524
|
+
self._end = None
|
|
525
|
+
except SystemError:
|
|
526
|
+
self._restart_mpv(self._pitch, self._tempo, start, end)
|
|
527
|
+
|
|
528
|
+
minimum = self._player_time_to_seconds(self._start)
|
|
529
|
+
try:
|
|
530
|
+
if self._player.time_pos < minimum:
|
|
531
|
+
self._player.time_pos = minimum
|
|
532
|
+
except TypeError:
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
def play(self,
|
|
536
|
+
path: str,
|
|
537
|
+
duration=None,
|
|
538
|
+
pitch=1.0,
|
|
539
|
+
tempo=1.0,
|
|
540
|
+
start=None,
|
|
541
|
+
end=None):
|
|
542
|
+
"""Load an audio file and play it.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
path : str
|
|
547
|
+
The path to the audio file.
|
|
548
|
+
duration : int, optional
|
|
549
|
+
The duration of the audio in the file. If this is not set,
|
|
550
|
+
the duration will be estimated by libmpv, which may be
|
|
551
|
+
inaccurate.
|
|
552
|
+
pitch : float, optional
|
|
553
|
+
Pitch correction factor. Default 1.0.
|
|
554
|
+
tempo : float, optional
|
|
555
|
+
Tempo correction factor. Default 1.0.
|
|
556
|
+
start : str, optional
|
|
557
|
+
The position to start playing at. If None, start at the
|
|
558
|
+
beginning. Default None.
|
|
559
|
+
end : str, optional
|
|
560
|
+
The position to stop playing at. If None, end at the end.
|
|
561
|
+
Default None.
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
self._ensure_player_is_running(pitch, tempo, start, end)
|
|
565
|
+
|
|
566
|
+
self.update_corrections(pitch, tempo, start, end)
|
|
567
|
+
|
|
568
|
+
if self.get_currently_playing() is not None:
|
|
569
|
+
self.stop()
|
|
570
|
+
|
|
571
|
+
self._duration = duration
|
|
572
|
+
|
|
573
|
+
if self._start_percent == 0:
|
|
574
|
+
self._player.command('loadfile',
|
|
575
|
+
com.string_to_raw(path),
|
|
576
|
+
'replace')
|
|
577
|
+
else:
|
|
578
|
+
p = self._percent_slider_to_player(self._start_percent)
|
|
579
|
+
self._player.command('loadfile',
|
|
580
|
+
com.string_to_raw(path),
|
|
581
|
+
'replace',
|
|
582
|
+
f'start={p}%')
|
|
583
|
+
self._start_percent = 0
|
|
584
|
+
|
|
585
|
+
def stop(self):
|
|
586
|
+
"""Stop playing and unload the file."""
|
|
587
|
+
|
|
588
|
+
if self._player is not None:
|
|
589
|
+
self._player.stop()
|
|
590
|
+
|
|
591
|
+
def toggle_pause(self):
|
|
592
|
+
"""Pause if unpaused, unpause if paused."""
|
|
593
|
+
|
|
594
|
+
if self._player is not None:
|
|
595
|
+
self._player.cycle('pause')
|
|
596
|
+
|
|
597
|
+
def set_pause(self, pause):
|
|
598
|
+
"""Set the pause state.
|
|
599
|
+
|
|
600
|
+
Parameters
|
|
601
|
+
----------
|
|
602
|
+
pause : bool
|
|
603
|
+
Pause if True, unpause if False.
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
if self._player is not None:
|
|
607
|
+
self._player.pause = pause
|
|
608
|
+
|
|
609
|
+
def _pause_observer(self, name, value):
|
|
610
|
+
self.PauseStatus.emit(value)
|
|
611
|
+
|
|
612
|
+
def get_currently_playing(self):
|
|
613
|
+
"""Get the path of the currently loaded file.
|
|
614
|
+
|
|
615
|
+
Returns
|
|
616
|
+
-------
|
|
617
|
+
str
|
|
618
|
+
The path of the file.
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
if self._player is None:
|
|
622
|
+
return None
|
|
623
|
+
else:
|
|
624
|
+
return com.raw_to_string(self._player.raw.path)
|
|
625
|
+
|
|
626
|
+
def get_duration(self):
|
|
627
|
+
"""Get the duration of the audio in the currently loaded file.
|
|
628
|
+
|
|
629
|
+
Returns
|
|
630
|
+
-------
|
|
631
|
+
float
|
|
632
|
+
The duration in seconds.
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
if self._player is None:
|
|
636
|
+
return None
|
|
637
|
+
if not self._duration:
|
|
638
|
+
self._duration = self._player.duration
|
|
639
|
+
return self._duration
|
|
640
|
+
|
|
641
|
+
def _time_pos_observer(self, name, value):
|
|
642
|
+
self.time_number += 1
|
|
643
|
+
try:
|
|
644
|
+
cur_time = int(value)
|
|
645
|
+
if cur_time != self._time_pos:
|
|
646
|
+
self._time_pos = cur_time
|
|
647
|
+
self.TickPos.emit(True, cur_time)
|
|
648
|
+
except TypeError:
|
|
649
|
+
self._time_pos = None
|
|
650
|
+
self.TickPos.emit(False, 0)
|
|
651
|
+
|
|
652
|
+
def _time_rem_observer(self, name, value):
|
|
653
|
+
self.time_number += 1
|
|
654
|
+
try:
|
|
655
|
+
cur_time = int(value + .9)
|
|
656
|
+
if cur_time != self._time_rem:
|
|
657
|
+
self._time_rem = cur_time
|
|
658
|
+
self.TickRem.emit(True, cur_time)
|
|
659
|
+
except TypeError:
|
|
660
|
+
self._time_rem = None
|
|
661
|
+
self.TickRem.emit(False, 0)
|
|
662
|
+
|
|
663
|
+
def _percent_observer(self, name, value):
|
|
664
|
+
if value is None:
|
|
665
|
+
self.Percent.emit(0)
|
|
666
|
+
else:
|
|
667
|
+
self.Percent.emit(int(self._percent_player_to_slider(value)))
|
|
668
|
+
|
|
669
|
+
def set_percent(self, percent):
|
|
670
|
+
"""Set the playback position as a percentage. The percentage
|
|
671
|
+
works the same as in the `Percent` signal, so 0% sets it to
|
|
672
|
+
whatever position `start` is set to and 100% sets it to whatever
|
|
673
|
+
position `end` is set to.
|
|
674
|
+
|
|
675
|
+
Parameters
|
|
676
|
+
----------
|
|
677
|
+
percent : numeric type
|
|
678
|
+
The percentage.
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
if self._player is not None and self._player.raw.path is not None:
|
|
682
|
+
self._player.percent_pos = self._percent_slider_to_player(percent)
|
|
683
|
+
else:
|
|
684
|
+
self._start_percent = percent
|
|
685
|
+
|
|
686
|
+
def back(self, amount=10.0):
|
|
687
|
+
"""Seek backwards. If that would put the play position before
|
|
688
|
+
`start`, it goes to `start` instead.
|
|
689
|
+
|
|
690
|
+
Parameters
|
|
691
|
+
----------
|
|
692
|
+
amount : float, optional
|
|
693
|
+
The amount of time to seek in seconds. Default 10.
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
if self._player is not None:
|
|
697
|
+
minimum = self._player_time_to_seconds(self._start)
|
|
698
|
+
if minimum is not None:
|
|
699
|
+
new = max(self._time_pos - amount, minimum)
|
|
700
|
+
self._player.time_pos = new
|
|
701
|
+
|
|
702
|
+
def forward(self, amount=10.0):
|
|
703
|
+
"""Seek forwards. If that would put the play position after
|
|
704
|
+
`end`, it goes to `end` instead.
|
|
705
|
+
|
|
706
|
+
Parameters
|
|
707
|
+
----------
|
|
708
|
+
amount : float, optional
|
|
709
|
+
The amount of time to seek in seconds. Default 10.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
if self._player is not None:
|
|
713
|
+
self._player.seek(amount)
|
|
714
|
+
|
|
715
|
+
def get_current_position(self):
|
|
716
|
+
"""Get the current play position, or None if the player is not
|
|
717
|
+
running.
|
|
718
|
+
|
|
719
|
+
Returns
|
|
720
|
+
-------
|
|
721
|
+
float
|
|
722
|
+
The play position.
|
|
723
|
+
"""
|
|
724
|
+
|
|
725
|
+
if self._player is not None:
|
|
726
|
+
return self._player.time_pos
|
|
727
|
+
|
|
728
|
+
def show_device_window(self):
|
|
729
|
+
"""Open a window where the user can select an audio output
|
|
730
|
+
device.
|
|
731
|
+
"""
|
|
732
|
+
|
|
733
|
+
self._ensure_player_is_running()
|
|
734
|
+
self._device_window.set_device_list(self._player.audio_device_list,
|
|
735
|
+
self._player.audio_device)
|
|
736
|
+
self._device_window.show()
|
|
737
|
+
|
|
738
|
+
def set_device(self, device):
|
|
739
|
+
"""Set the audio output device.
|
|
740
|
+
|
|
741
|
+
Parameters
|
|
742
|
+
----------
|
|
743
|
+
device : str
|
|
744
|
+
The device. See `--audio-device` in the mpv manpage for a
|
|
745
|
+
description of what to put here.
|
|
746
|
+
"""
|
|
747
|
+
|
|
748
|
+
self._device = device
|
|
749
|
+
if self._player is not None:
|
|
750
|
+
self._player.audio_device = device
|
|
751
|
+
|
|
752
|
+
def _stop_player(self):
|
|
753
|
+
if self._player is not None:
|
|
754
|
+
self._unobserve()
|
|
755
|
+
self._player.command('quit')
|
|
756
|
+
self._player.terminate()
|
|
757
|
+
self._player = None
|