audio-tuner-gui 0.9.1__py3-none-any.whl → 0.11.0__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 +1 -1
- audio_tuner_gui/analysis.py +107 -81
- audio_tuner_gui/common.py +73 -18
- audio_tuner_gui/file_selector.py +71 -21
- audio_tuner_gui/option_panel.py +49 -21
- audio_tuner_gui/scripts/tuner_gui.py +126 -13
- {audio_tuner_gui-0.9.1.dist-info → audio_tuner_gui-0.11.0.dist-info}/METADATA +21 -15
- {audio_tuner_gui-0.9.1.dist-info → audio_tuner_gui-0.11.0.dist-info}/RECORD +11 -11
- {audio_tuner_gui-0.9.1.dist-info → audio_tuner_gui-0.11.0.dist-info}/WHEEL +0 -0
- {audio_tuner_gui-0.9.1.dist-info → audio_tuner_gui-0.11.0.dist-info}/entry_points.txt +0 -0
- {audio_tuner_gui-0.9.1.dist-info → audio_tuner_gui-0.11.0.dist-info}/licenses/COPYING +0 -0
audio_tuner_gui/__init__.py
CHANGED
audio_tuner_gui/analysis.py
CHANGED
|
@@ -38,6 +38,7 @@ from PyQt6.QtCore import (
|
|
|
38
38
|
pyqtSignal,
|
|
39
39
|
QThread,
|
|
40
40
|
QPersistentModelIndex,
|
|
41
|
+
QModelIndex,
|
|
41
42
|
)
|
|
42
43
|
from PyQt6.QtWidgets import (
|
|
43
44
|
QWidget,
|
|
@@ -84,6 +85,32 @@ class _AudioView(QTableView):
|
|
|
84
85
|
self.setShowGrid(False)
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
class _Result(anal.Analysis):
|
|
89
|
+
"""A class that inherits from audio_tuner.analysis.Analysis and
|
|
90
|
+
extends it with a few more attributes necessary for the GUI.
|
|
91
|
+
|
|
92
|
+
Attributes
|
|
93
|
+
----------
|
|
94
|
+
result_rows : list[audio_tuner_gui.common.RowData] | None
|
|
95
|
+
A list of RowData objects that can be passed to the
|
|
96
|
+
`update_data` method of audio_tuner_gui.display.Display, or None
|
|
97
|
+
if it hasn't been set yet.
|
|
98
|
+
options : audio_tuner_gui.common.Options | None
|
|
99
|
+
The options used to do the analysis, or None if it hasn't been
|
|
100
|
+
set yet.
|
|
101
|
+
index : PyQt6.QtCore.QPersistentModelIndex | None
|
|
102
|
+
The index of the results entry in the analyzed audio widget, or
|
|
103
|
+
None if it hasn't been set yet.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, *args, **kwargs):
|
|
107
|
+
super().__init__(*args, **kwargs)
|
|
108
|
+
|
|
109
|
+
self.result_rows = None
|
|
110
|
+
self.options = None
|
|
111
|
+
self.index = None
|
|
112
|
+
|
|
113
|
+
|
|
87
114
|
class _Worker(QObject):
|
|
88
115
|
"""A worker class to be run in it's own thread that handles
|
|
89
116
|
analysis. Inherits from QObject.
|
|
@@ -107,23 +134,23 @@ class _Worker(QObject):
|
|
|
107
134
|
The amount of progress, from 0 to 1.
|
|
108
135
|
"""
|
|
109
136
|
|
|
110
|
-
ResultReady = pyqtSignal(
|
|
137
|
+
ResultReady = pyqtSignal(_Result)
|
|
111
138
|
"""Signal emitted when analysis is finished and the result is ready.
|
|
112
139
|
|
|
113
140
|
Parameters
|
|
114
141
|
----------
|
|
115
|
-
|
|
116
|
-
The
|
|
142
|
+
_Result
|
|
143
|
+
The result of the analysis.
|
|
117
144
|
"""
|
|
118
145
|
|
|
119
|
-
ProcessingError = pyqtSignal(
|
|
120
|
-
"""Signal emitted when the
|
|
121
|
-
|
|
146
|
+
ProcessingError = pyqtSignal(_Result)
|
|
147
|
+
"""Signal emitted when the _Result object raises an exception during
|
|
148
|
+
analysis.
|
|
122
149
|
|
|
123
150
|
Parameters
|
|
124
151
|
----------
|
|
125
|
-
|
|
126
|
-
The
|
|
152
|
+
_Result
|
|
153
|
+
The _Result object.
|
|
127
154
|
"""
|
|
128
155
|
|
|
129
156
|
AddToLog = pyqtSignal(str, int)
|
|
@@ -147,9 +174,6 @@ class _Worker(QObject):
|
|
|
147
174
|
The name of the option, as defined in audio_tuner_gui.common.
|
|
148
175
|
"""
|
|
149
176
|
|
|
150
|
-
def __init__(self):
|
|
151
|
-
super().__init__()
|
|
152
|
-
|
|
153
177
|
def _progress_check(self, progress: float) -> bool:
|
|
154
178
|
self.UpdateProgbar.emit(progress)
|
|
155
179
|
return not QThread.currentThread().isInterruptionRequested()
|
|
@@ -168,24 +192,17 @@ class _Worker(QObject):
|
|
|
168
192
|
self.OptionError.emit(gcom.OPTION_END)
|
|
169
193
|
|
|
170
194
|
def analyze(self,
|
|
171
|
-
|
|
195
|
+
result: _Result,
|
|
172
196
|
options: gcom.Options,
|
|
173
197
|
reread_requested: bool) -> None:
|
|
174
|
-
"""Do the analysis. This gets passed an instance of
|
|
175
|
-
|
|
176
|
-
the
|
|
177
|
-
ProcessingError signal if it went wrong). The Analysis instance
|
|
178
|
-
will have `result_rows` and `options` in addition to it's usual
|
|
179
|
-
attributes. `result_rows` is a list of
|
|
180
|
-
audio_tuner_gui.common.RowData objects that can be passed to the
|
|
181
|
-
`update_data` method of audio_tuner_gui.display.Display, and
|
|
182
|
-
`options` is the audio_tuner_gui.common.Options object that was
|
|
183
|
-
passed to the `options` parameter of this method.
|
|
198
|
+
"""Do the analysis. This gets passed an instance of _Result,
|
|
199
|
+
and then spits it out again via the ResultReady signal when it's
|
|
200
|
+
done (or via the ProcessingError signal if it went wrong).
|
|
184
201
|
|
|
185
202
|
Parameters
|
|
186
203
|
----------
|
|
187
|
-
|
|
188
|
-
The
|
|
204
|
+
result : _Result
|
|
205
|
+
The _Result instance to put the results in.
|
|
189
206
|
options : audio_tuner_gui.common.Options
|
|
190
207
|
The analysis options.
|
|
191
208
|
reread_requested : bool
|
|
@@ -195,44 +212,41 @@ class _Worker(QObject):
|
|
|
195
212
|
|
|
196
213
|
if QThread.currentThread().isInterruptionRequested():
|
|
197
214
|
return
|
|
198
|
-
|
|
199
|
-
|
|
215
|
+
result.try_sequence = options[gcom.OPTION_BACKENDS]
|
|
216
|
+
result.print_msg = self._print_msg
|
|
200
217
|
if (reread_requested
|
|
201
|
-
and (
|
|
202
|
-
or
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
and (result.pitch != options[gcom.OPTION_PITCH]
|
|
219
|
+
or result.tempo != options[gcom.OPTION_TEMPO])):
|
|
220
|
+
result.pitch = options[gcom.OPTION_PITCH]
|
|
221
|
+
result.tempo = options[gcom.OPTION_TEMPO]
|
|
205
222
|
redo_level = gcom.REDO_LEVEL_ALL
|
|
206
223
|
else:
|
|
207
|
-
redo_level = options.redo_level(
|
|
208
|
-
basename = os.path.basename(
|
|
224
|
+
redo_level = options.redo_level(result.options)
|
|
225
|
+
basename = os.path.basename(result.inputfile)
|
|
209
226
|
self.UpdateStatusbar.emit('Analyzing ' + basename + '...')
|
|
210
227
|
if redo_level >= gcom.REDO_LEVEL_ALL:
|
|
211
228
|
try:
|
|
212
229
|
s = '\nLoading "' + basename + '"'
|
|
213
230
|
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
214
231
|
size = 2**options[gcom.OPTION_SIZE_EXP]
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
end=options[gcom.OPTION_END],
|
|
221
|
-
samplerate=options[gcom.OPTION_SAMPLERATE],
|
|
222
|
-
pad=pad_amounts)
|
|
232
|
+
pad_amounts = (size * 2, size * 2)
|
|
233
|
+
result.load_data(start=options[gcom.OPTION_START],
|
|
234
|
+
end=options[gcom.OPTION_END],
|
|
235
|
+
samplerate=options[gcom.OPTION_SAMPLERATE],
|
|
236
|
+
pad=pad_amounts)
|
|
223
237
|
except eh.LoadError:
|
|
224
238
|
s = 'Error processing ' + basename
|
|
225
239
|
self.UpdateStatusbar.emit(s)
|
|
226
|
-
self.ProcessingError.emit(
|
|
240
|
+
self.ProcessingError.emit(result)
|
|
227
241
|
return
|
|
228
242
|
try:
|
|
229
243
|
self.AddToLog.emit('Analyzing "' + basename + '"',
|
|
230
244
|
lv.LOG_LEVEL_NORMAL)
|
|
231
|
-
|
|
245
|
+
result.fft(size=size, progress_hook=self._progress_check)
|
|
232
246
|
except eh.ShortError:
|
|
233
247
|
s = 'Error processing ' + basename
|
|
234
248
|
self.UpdateStatusbar.emit(s)
|
|
235
|
-
self.ProcessingError.emit(
|
|
249
|
+
self.ProcessingError.emit(result)
|
|
236
250
|
self.AddToLog.emit(eh.ERRMSG_SHORT,
|
|
237
251
|
lv.LOG_LEVEL_ERROR)
|
|
238
252
|
return
|
|
@@ -243,16 +257,16 @@ class _Worker(QObject):
|
|
|
243
257
|
lv.LOG_LEVEL_WARNING)
|
|
244
258
|
return
|
|
245
259
|
if redo_level >= gcom.REDO_LEVEL_FIND_PEAKS:
|
|
246
|
-
|
|
247
|
-
low_cut=options[gcom.OPTION_LOW_CUT] *
|
|
248
|
-
high_cut=options[gcom.OPTION_HIGH_CUT] *
|
|
260
|
+
result.find_peaks(
|
|
261
|
+
low_cut=options[gcom.OPTION_LOW_CUT] * result.pitch,
|
|
262
|
+
high_cut=options[gcom.OPTION_HIGH_CUT] * result.pitch,
|
|
249
263
|
max_peaks=options[gcom.OPTION_MAX_PEAKS],
|
|
250
264
|
dB_range=options[gcom.OPTION_DB_RANGE])
|
|
251
265
|
if redo_level >= gcom.REDO_LEVEL_TUNING_SYSTEM:
|
|
252
|
-
peaks =
|
|
266
|
+
peaks = result.peaks
|
|
253
267
|
tuning_system = options.tuning_system
|
|
254
268
|
result_rows = []
|
|
255
|
-
pitch_offset = options[gcom.OPTION_PITCH] /
|
|
269
|
+
pitch_offset = options[gcom.OPTION_PITCH] / result.pitch
|
|
256
270
|
for note in tuning_system(
|
|
257
271
|
options[gcom.OPTION_LOW_CUT] * options[gcom.OPTION_PITCH],
|
|
258
272
|
options[gcom.OPTION_HIGH_CUT] * options[gcom.OPTION_PITCH]):
|
|
@@ -268,11 +282,11 @@ class _Worker(QObject):
|
|
|
268
282
|
result_row['cents'] = -com.ratio_to_cents(freq_ratio)
|
|
269
283
|
result_row['correction'] = freq_ratio * pitch_offset
|
|
270
284
|
result_rows.append(result_row)
|
|
271
|
-
|
|
285
|
+
result.result_rows = result_rows
|
|
272
286
|
|
|
273
|
-
|
|
287
|
+
result.options = options
|
|
274
288
|
|
|
275
|
-
self.ResultReady.emit(
|
|
289
|
+
self.ResultReady.emit(result)
|
|
276
290
|
self.UpdateProgbar.emit(-1)
|
|
277
291
|
self.AddToLog.emit('Finished analyzing "' + basename + '"',
|
|
278
292
|
lv.LOG_LEVEL_NORMAL)
|
|
@@ -326,13 +340,13 @@ class AnalyzedAudio(QWidget):
|
|
|
326
340
|
The amount of progress, from 0 to 1.
|
|
327
341
|
"""
|
|
328
342
|
|
|
329
|
-
AnalyzeAudio = pyqtSignal(
|
|
343
|
+
AnalyzeAudio = pyqtSignal(_Result, gcom.Options, bool)
|
|
330
344
|
"""Signal emitted to tell the worker thread to run an analysis.
|
|
331
345
|
|
|
332
346
|
Parameters
|
|
333
347
|
----------
|
|
334
|
-
|
|
335
|
-
The
|
|
348
|
+
_Result
|
|
349
|
+
The _Result object to use.
|
|
336
350
|
audio_tuner_gui.common.Options
|
|
337
351
|
The options.
|
|
338
352
|
bool
|
|
@@ -405,7 +419,7 @@ class AnalyzedAudio(QWidget):
|
|
|
405
419
|
self.current_duration = None
|
|
406
420
|
self.current_metadata = None
|
|
407
421
|
|
|
408
|
-
self.
|
|
422
|
+
self._results = {}
|
|
409
423
|
self._in_progress = {}
|
|
410
424
|
self._cancelled = False
|
|
411
425
|
|
|
@@ -460,6 +474,7 @@ class AnalyzedAudio(QWidget):
|
|
|
460
474
|
view.setColumnWidth(0, 20)
|
|
461
475
|
|
|
462
476
|
view.selectionModel().currentRowChanged.connect(self._audio_selected)
|
|
477
|
+
view.clicked.connect(self._clicked)
|
|
463
478
|
model.itemChanged.connect(self._item_changed)
|
|
464
479
|
|
|
465
480
|
self._model = model
|
|
@@ -495,6 +510,14 @@ class AnalyzedAudio(QWidget):
|
|
|
495
510
|
self._remove_button = remove_button
|
|
496
511
|
self._set_removal_enabled(False)
|
|
497
512
|
|
|
513
|
+
def _clicked(self, index: QModelIndex):
|
|
514
|
+
item2 = self._model.item(index.row(), 2)
|
|
515
|
+
if item2:
|
|
516
|
+
options = self._results[item2.data()].options
|
|
517
|
+
# FIXME: PushOptions gets emitted twice when the user
|
|
518
|
+
# changes the selection by clicking.
|
|
519
|
+
self.PushOptions.emit(options)
|
|
520
|
+
|
|
498
521
|
def _audio_selected(self, new, prev):
|
|
499
522
|
title = None
|
|
500
523
|
item1 = self._model.item(new.row(), 1)
|
|
@@ -510,11 +533,11 @@ class AnalyzedAudio(QWidget):
|
|
|
510
533
|
self.current_title = title
|
|
511
534
|
self.current_listed_title = listed_title
|
|
512
535
|
self.current_selection = item2.data()
|
|
513
|
-
self.current_options = self.
|
|
514
|
-
self.current_duration = self.
|
|
515
|
-
self.current_metadata = self.
|
|
536
|
+
self.current_options = self._results[item2.data()].options
|
|
537
|
+
self.current_duration = self._results[item2.data()].file_duration
|
|
538
|
+
self.current_metadata = self._results[item2.data()].file_metadata
|
|
516
539
|
self.DisplayResult.emit(title,
|
|
517
|
-
self.
|
|
540
|
+
self._results[item2.data()].result_rows)
|
|
518
541
|
self.PushOptions.emit(self.current_options)
|
|
519
542
|
self.SomethingSelected.emit()
|
|
520
543
|
|
|
@@ -523,7 +546,7 @@ class AnalyzedAudio(QWidget):
|
|
|
523
546
|
if data is not None:
|
|
524
547
|
if item.text() == '':
|
|
525
548
|
self._model.itemChanged.disconnect(self._item_changed)
|
|
526
|
-
file_title = self.
|
|
549
|
+
file_title = self._results[data].file_title
|
|
527
550
|
item.setText(file_title if file_title else ' ')
|
|
528
551
|
self._model.itemChanged.connect(self._item_changed)
|
|
529
552
|
if data == self.current_selection:
|
|
@@ -540,7 +563,7 @@ class AnalyzedAudio(QWidget):
|
|
|
540
563
|
if title:
|
|
541
564
|
self.current_title = title
|
|
542
565
|
self.DisplayResult.emit(title,
|
|
543
|
-
self.
|
|
566
|
+
self._results[item2.data()].result_rows)
|
|
544
567
|
|
|
545
568
|
def add_audio(self, path, options):
|
|
546
569
|
"""Analyze an audio file asynchronously and add it to the list
|
|
@@ -560,8 +583,8 @@ class AnalyzedAudio(QWidget):
|
|
|
560
583
|
canonical_path = os.path.realpath(path)
|
|
561
584
|
if canonical_path in self._in_progress:
|
|
562
585
|
return
|
|
563
|
-
if canonical_path in self.
|
|
564
|
-
index = self.
|
|
586
|
+
if canonical_path in self._results:
|
|
587
|
+
index = self._results[canonical_path].index
|
|
565
588
|
self._view.selectRow(index.row())
|
|
566
589
|
return
|
|
567
590
|
|
|
@@ -574,8 +597,11 @@ class AnalyzedAudio(QWidget):
|
|
|
574
597
|
self.Starting.emit()
|
|
575
598
|
|
|
576
599
|
self._in_progress[canonical_path] = True
|
|
577
|
-
|
|
578
|
-
|
|
600
|
+
|
|
601
|
+
# This is a misnomer at this point because there's no results in
|
|
602
|
+
# "result" yet, but that will soon change.
|
|
603
|
+
result = _Result(canonical_path)
|
|
604
|
+
self.AnalyzeAudio.emit(result, options, False)
|
|
579
605
|
|
|
580
606
|
def change_options(self, new_options, reread_requested):
|
|
581
607
|
"""Change the options of the currently selected analyzed audio.
|
|
@@ -602,19 +628,19 @@ class AnalyzedAudio(QWidget):
|
|
|
602
628
|
self.Starting.emit()
|
|
603
629
|
|
|
604
630
|
self._in_progress[canonical_path] = True
|
|
605
|
-
|
|
606
|
-
self.AnalyzeAudio.emit(
|
|
631
|
+
result = self._results[canonical_path]
|
|
632
|
+
self.AnalyzeAudio.emit(result, new_options, reread_requested)
|
|
607
633
|
self.current_options = new_options
|
|
608
634
|
|
|
609
|
-
def _handle_result(self,
|
|
610
|
-
canonical_path =
|
|
611
|
-
file_title =
|
|
612
|
-
if canonical_path in self.
|
|
635
|
+
def _handle_result(self, result):
|
|
636
|
+
canonical_path = result.inputfile
|
|
637
|
+
file_title = result.file_title
|
|
638
|
+
if canonical_path in self._results:
|
|
613
639
|
if canonical_path == self.current_selection:
|
|
614
640
|
self.DisplayResult.emit(self.current_title,
|
|
615
|
-
self.
|
|
641
|
+
self._results[canonical_path].result_rows)
|
|
616
642
|
else:
|
|
617
|
-
file_track =
|
|
643
|
+
file_track = result.file_track
|
|
618
644
|
|
|
619
645
|
track_num = QStandardItem(file_track.partition('/')[0]
|
|
620
646
|
if file_track
|
|
@@ -630,8 +656,8 @@ class AnalyzedAudio(QWidget):
|
|
|
630
656
|
|
|
631
657
|
index = QPersistentModelIndex(self._model.indexFromItem(filename))
|
|
632
658
|
self._model.setVerticalHeaderItem(index.row(), QStandardItem(' '))
|
|
633
|
-
|
|
634
|
-
self.
|
|
659
|
+
result.index = index
|
|
660
|
+
self._results[canonical_path] = result
|
|
635
661
|
|
|
636
662
|
del self._in_progress[canonical_path]
|
|
637
663
|
if len(self._in_progress) == 0:
|
|
@@ -655,9 +681,9 @@ class AnalyzedAudio(QWidget):
|
|
|
655
681
|
|
|
656
682
|
canonical_path = self.current_selection
|
|
657
683
|
if canonical_path is not None:
|
|
658
|
-
pitch = self.
|
|
684
|
+
pitch = self._results[canonical_path].options[gcom.OPTION_PITCH]
|
|
659
685
|
try:
|
|
660
|
-
self.
|
|
686
|
+
self._results[canonical_path].show_plot(plot_type=plot_type,
|
|
661
687
|
asynchronous=True,
|
|
662
688
|
title=self.current_title,
|
|
663
689
|
pitch=pitch,
|
|
@@ -669,8 +695,8 @@ class AnalyzedAudio(QWidget):
|
|
|
669
695
|
def _handle_log_message(self, message, level):
|
|
670
696
|
self.AddToLog.emit(message, level)
|
|
671
697
|
|
|
672
|
-
def _handle_error(self,
|
|
673
|
-
del self._in_progress[
|
|
698
|
+
def _handle_error(self, result):
|
|
699
|
+
del self._in_progress[result.inputfile]
|
|
674
700
|
if len(self._in_progress) == 0:
|
|
675
701
|
self.Finished.emit()
|
|
676
702
|
|
|
@@ -692,9 +718,9 @@ class AnalyzedAudio(QWidget):
|
|
|
692
718
|
except IndexError:
|
|
693
719
|
return
|
|
694
720
|
path = self._model.item(row, 2).data()
|
|
695
|
-
del self.
|
|
721
|
+
del self._results[path]
|
|
696
722
|
self._model.removeRows(row, 1)
|
|
697
|
-
if len(self.
|
|
723
|
+
if len(self._results) == 0:
|
|
698
724
|
self.DisplayResult.emit(' ', [])
|
|
699
725
|
self.NothingSelected.emit()
|
|
700
726
|
self.current_title = None
|
audio_tuner_gui/common.py
CHANGED
|
@@ -34,7 +34,6 @@ __all__ = [
|
|
|
34
34
|
'OPTION_HIGH_CUT',
|
|
35
35
|
'OPTION_DB_RANGE',
|
|
36
36
|
'OPTION_MAX_PEAKS',
|
|
37
|
-
'OPTION_PAD',
|
|
38
37
|
'OPTION_SIZE_EXP',
|
|
39
38
|
'OPTION_SAMPLERATE',
|
|
40
39
|
'OPTION_PITCH',
|
|
@@ -65,6 +64,8 @@ __all__ = [
|
|
|
65
64
|
'ICON_PLAYER_BACK',
|
|
66
65
|
'ICON_PLAYER_FORWARD',
|
|
67
66
|
'ICON_AUDIO_DEVICE',
|
|
67
|
+
'ICON_SAVE',
|
|
68
|
+
'ICON_DEL',
|
|
68
69
|
'RowData',
|
|
69
70
|
'SplitAction',
|
|
70
71
|
'Options',
|
|
@@ -73,6 +74,7 @@ __all__ = [
|
|
|
73
74
|
|
|
74
75
|
import os
|
|
75
76
|
from typing import TypedDict
|
|
77
|
+
from argparse import Namespace
|
|
76
78
|
|
|
77
79
|
from PyQt6.QtGui import (
|
|
78
80
|
QIcon,
|
|
@@ -94,7 +96,6 @@ OPTION_LOW_CUT = 'Low cut'
|
|
|
94
96
|
OPTION_HIGH_CUT = 'High cut'
|
|
95
97
|
OPTION_DB_RANGE = 'dB range'
|
|
96
98
|
OPTION_MAX_PEAKS = 'Max peaks'
|
|
97
|
-
OPTION_PAD = 'Pad input'
|
|
98
99
|
OPTION_SIZE_EXP = 'FFT size exponent'
|
|
99
100
|
OPTION_SAMPLERATE = 'Sample rate'
|
|
100
101
|
OPTION_BACKENDS = 'backends'
|
|
@@ -145,6 +146,8 @@ try:
|
|
|
145
146
|
ICON_PLAYER_BACK = QIcon.ThemeIcon.MediaSeekBackward
|
|
146
147
|
ICON_PLAYER_FORWARD = QIcon.ThemeIcon.MediaSeekForward
|
|
147
148
|
ICON_AUDIO_DEVICE = QIcon.ThemeIcon.AudioCard
|
|
149
|
+
ICON_SAVE = QIcon.ThemeIcon.DocumentSave
|
|
150
|
+
ICON_DEL = QIcon.ThemeIcon.EditDelete
|
|
148
151
|
except AttributeError:
|
|
149
152
|
ICON_BACK = 'go-previous'
|
|
150
153
|
ICON_FORWARD = 'go-next'
|
|
@@ -166,6 +169,8 @@ except AttributeError:
|
|
|
166
169
|
ICON_PLAYER_BACK = 'media-seek-backward'
|
|
167
170
|
ICON_PLAYER_FORWARD = 'media-seek-forward'
|
|
168
171
|
ICON_AUDIO_DEVICE = 'audio-card'
|
|
172
|
+
ICON_SAVE = 'document-save'
|
|
173
|
+
ICON_DEL = 'edit-delete'
|
|
169
174
|
|
|
170
175
|
|
|
171
176
|
class RowData(TypedDict):
|
|
@@ -275,25 +280,76 @@ class Options(dict):
|
|
|
275
280
|
|
|
276
281
|
def __init__(self, args):
|
|
277
282
|
super().__init__()
|
|
278
|
-
|
|
279
|
-
self
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
283
|
+
|
|
284
|
+
self._options_and_args = (
|
|
285
|
+
(OPTION_TUNING_SYSTEM, 'tuning'),
|
|
286
|
+
(OPTION_REF_FREQ, 'ref_freq'),
|
|
287
|
+
(OPTION_REF_NOTE, 'ref_note'),
|
|
288
|
+
(OPTION_START, 'start'),
|
|
289
|
+
(OPTION_END, 'end'),
|
|
290
|
+
(OPTION_LOW_CUT, 'low_cut'),
|
|
291
|
+
(OPTION_HIGH_CUT, 'high_cut'),
|
|
292
|
+
(OPTION_DB_RANGE, 'dB_range'),
|
|
293
|
+
(OPTION_MAX_PEAKS, 'max_peaks'),
|
|
294
|
+
(OPTION_SIZE_EXP, 'size_exp'),
|
|
295
|
+
(OPTION_SAMPLERATE, 'samplerate'),
|
|
296
|
+
(OPTION_BACKENDS, 'backends'),
|
|
297
|
+
(OPTION_PITCH, 'pitch'),
|
|
298
|
+
(OPTION_TEMPO, 'tempo'),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for opt, arg in self._options_and_args:
|
|
302
|
+
if arg == 'tuning':
|
|
303
|
+
val = getattr(args, arg)
|
|
304
|
+
self[opt] = self._guify_tuning_system(val)
|
|
305
|
+
else:
|
|
306
|
+
self[opt] = getattr(args, arg)
|
|
293
307
|
|
|
294
308
|
def _guify_tuning_system(self, tuning_system):
|
|
295
309
|
return tuning_system.replace('_', ' ').title()
|
|
296
310
|
|
|
311
|
+
def _unguify_tuning_system(self, tuning_system):
|
|
312
|
+
return tuning_system.replace(' ', '_').lower()
|
|
313
|
+
|
|
314
|
+
def argparse_namespace(self) -> Namespace:
|
|
315
|
+
"""Return an argparse.Namespace object containing the options.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
argparse.Namespace
|
|
320
|
+
The options.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
args = Namespace()
|
|
324
|
+
|
|
325
|
+
for opt, arg in self._options_and_args:
|
|
326
|
+
if arg == 'tuning':
|
|
327
|
+
val = self[opt]
|
|
328
|
+
setattr(args, arg, self._unguify_tuning_system(val))
|
|
329
|
+
else:
|
|
330
|
+
setattr(args, arg, self[opt])
|
|
331
|
+
|
|
332
|
+
return args
|
|
333
|
+
|
|
334
|
+
def merge_args(self, args: Namespace):
|
|
335
|
+
"""Modify the options according to the arguments in an
|
|
336
|
+
argparse.Namespace object.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
args : argparse.Namespace
|
|
341
|
+
The arguments.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
for opt, arg in self._options_and_args:
|
|
345
|
+
if arg == 'tuning':
|
|
346
|
+
if (val := getattr(args, arg, None)) is not None:
|
|
347
|
+
self[opt] = self._guify_tuning_system(val)
|
|
348
|
+
else:
|
|
349
|
+
if (val := getattr(args, arg, None)) is not None:
|
|
350
|
+
self[opt] = val
|
|
351
|
+
|
|
352
|
+
|
|
297
353
|
def redo_level(self, old_options):
|
|
298
354
|
"""Compare the options stored in this instance of Options to the
|
|
299
355
|
options stored in an older one to find out how much analysis
|
|
@@ -313,7 +369,6 @@ class Options(dict):
|
|
|
313
369
|
if (old_options is None
|
|
314
370
|
or self[OPTION_START] != old_options[OPTION_START]
|
|
315
371
|
or self[OPTION_END] != old_options[OPTION_END]
|
|
316
|
-
or self[OPTION_PAD] != old_options[OPTION_PAD]
|
|
317
372
|
or self[OPTION_SIZE_EXP] != old_options[OPTION_SIZE_EXP]
|
|
318
373
|
or self[OPTION_BACKENDS] != old_options[OPTION_BACKENDS]
|
|
319
374
|
or self[OPTION_SAMPLERATE] != old_options[OPTION_SAMPLERATE]):
|
audio_tuner_gui/file_selector.py
CHANGED
|
@@ -38,6 +38,8 @@ from PyQt6.QtCore import (
|
|
|
38
38
|
QDir,
|
|
39
39
|
pyqtSignal,
|
|
40
40
|
QModelIndex,
|
|
41
|
+
QMutex,
|
|
42
|
+
QMutexLocker,
|
|
41
43
|
)
|
|
42
44
|
from PyQt6.QtWidgets import (
|
|
43
45
|
QWidget,
|
|
@@ -147,13 +149,13 @@ class FileSelector(QWidget):
|
|
|
147
149
|
The option panel widget to get analysis options from.
|
|
148
150
|
"""
|
|
149
151
|
|
|
150
|
-
SelectForAnalysis = pyqtSignal(str
|
|
152
|
+
SelectForAnalysis = pyqtSignal(str)
|
|
151
153
|
"""Signal emitted when a file is selected.
|
|
152
154
|
|
|
153
155
|
Parameters
|
|
154
156
|
----------
|
|
155
|
-
|
|
156
|
-
The
|
|
157
|
+
str
|
|
158
|
+
The path of the audio file.
|
|
157
159
|
"""
|
|
158
160
|
|
|
159
161
|
UpdateStatusbar = pyqtSignal(str)
|
|
@@ -169,6 +171,8 @@ class FileSelector(QWidget):
|
|
|
169
171
|
def __init__(self, option_panel):
|
|
170
172
|
super().__init__()
|
|
171
173
|
|
|
174
|
+
self._init_complete = False
|
|
175
|
+
|
|
172
176
|
self.option_panel = option_panel
|
|
173
177
|
|
|
174
178
|
vbox = QVBoxLayout(self)
|
|
@@ -184,6 +188,12 @@ class FileSelector(QWidget):
|
|
|
184
188
|
vbox.addWidget(self.panel)
|
|
185
189
|
vbox.addWidget(self.view)
|
|
186
190
|
|
|
191
|
+
self._scroll_positions = {}
|
|
192
|
+
self._scroll_mutex = QMutex()
|
|
193
|
+
self._scroll_update_needed = False
|
|
194
|
+
|
|
195
|
+
self._init_complete = True
|
|
196
|
+
|
|
187
197
|
def _init_file_view(self):
|
|
188
198
|
model = QFileSystemModel(self)
|
|
189
199
|
|
|
@@ -203,7 +213,15 @@ class FileSelector(QWidget):
|
|
|
203
213
|
self.model = model
|
|
204
214
|
self.view = view
|
|
205
215
|
|
|
206
|
-
def
|
|
216
|
+
def set_show_hidden(self, checked):
|
|
217
|
+
"""Select whether to show hidden files.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
checked : bool
|
|
222
|
+
Whether or not to show hidden files.
|
|
223
|
+
"""
|
|
224
|
+
|
|
207
225
|
if checked:
|
|
208
226
|
self.model.setFilter(self.showhidden_filter)
|
|
209
227
|
else:
|
|
@@ -274,11 +292,7 @@ class FileSelector(QWidget):
|
|
|
274
292
|
self._cd(QDir.fromNativeSeparators(self.pathedit.text()))
|
|
275
293
|
except NotADirectoryError:
|
|
276
294
|
path = self.pathedit.text()
|
|
277
|
-
|
|
278
|
-
if options is None:
|
|
279
|
-
self.option_panel.ensure_visible()
|
|
280
|
-
else:
|
|
281
|
-
self.SelectForAnalysis.emit(path, options)
|
|
295
|
+
self.SelectForAnalysis.emit(path)
|
|
282
296
|
self._select_filename()
|
|
283
297
|
|
|
284
298
|
def handle_command_line_arg(self, arg):
|
|
@@ -300,11 +314,7 @@ class FileSelector(QWidget):
|
|
|
300
314
|
try:
|
|
301
315
|
self._cd(QDir.fromNativeSeparators(arg))
|
|
302
316
|
except NotADirectoryError:
|
|
303
|
-
|
|
304
|
-
if options is None:
|
|
305
|
-
self.option_panel.ensure_visible()
|
|
306
|
-
else:
|
|
307
|
-
self.SelectForAnalysis.emit(arg, options)
|
|
317
|
+
self.SelectForAnalysis.emit(arg)
|
|
308
318
|
|
|
309
319
|
def handle_drop(self, path):
|
|
310
320
|
"""If `path` is a file, act like it's been selected and send it
|
|
@@ -346,9 +356,35 @@ class FileSelector(QWidget):
|
|
|
346
356
|
dest)
|
|
347
357
|
path = dest
|
|
348
358
|
index = self.model.index(dest)
|
|
359
|
+
|
|
360
|
+
if self._init_complete:
|
|
361
|
+
self._save_scroll_position()
|
|
362
|
+
self.model.directoryLoaded.connect(self._load_scroll_position,
|
|
363
|
+
Qt.ConnectionType.SingleShotConnection)
|
|
364
|
+
|
|
365
|
+
self._scroll_update_needed = True
|
|
349
366
|
self.model.setRootPath(path)
|
|
350
367
|
self.view.setRootIndex(index)
|
|
368
|
+
|
|
369
|
+
# The directoryLoaded signal is NOT always emitted when the
|
|
370
|
+
# directory is finished loading, so this is necessary to make
|
|
371
|
+
# sure the scroll position is updated anyway.
|
|
372
|
+
testindex = self.model.index(path)
|
|
373
|
+
if testindex.isValid():
|
|
374
|
+
try:
|
|
375
|
+
# testindex being valid means the new directory is
|
|
376
|
+
# already loaded, so we'll go ahead and update the
|
|
377
|
+
# scroll position and not bother to wait for the signal
|
|
378
|
+
self.model.directoryLoaded.disconnect(
|
|
379
|
+
self._load_scroll_position)
|
|
380
|
+
self._load_scroll_position(path)
|
|
381
|
+
except TypeError:
|
|
382
|
+
# The signal's already been emitted, so there's nothing
|
|
383
|
+
# to do
|
|
384
|
+
pass
|
|
385
|
+
|
|
351
386
|
self.pathedit.setText(QDir(path).absolutePath())
|
|
387
|
+
|
|
352
388
|
if hist == 'previous':
|
|
353
389
|
self.path_history.back()
|
|
354
390
|
elif hist == 'next':
|
|
@@ -400,6 +436,25 @@ class FileSelector(QWidget):
|
|
|
400
436
|
if self.up_button.underMouse():
|
|
401
437
|
self.UpdateStatusbar.emit(status)
|
|
402
438
|
|
|
439
|
+
def _save_scroll_position(self):
|
|
440
|
+
current_path = QDir(self.model.rootPath()).absolutePath()
|
|
441
|
+
point = self.view.rect().topLeft()
|
|
442
|
+
position = self.view.indexAt(point).data()
|
|
443
|
+
self._scroll_positions[current_path] = position
|
|
444
|
+
|
|
445
|
+
def _load_scroll_position(self, path):
|
|
446
|
+
with QMutexLocker(self._scroll_mutex):
|
|
447
|
+
if not self._scroll_update_needed:
|
|
448
|
+
return
|
|
449
|
+
abs_path = QDir(path).absolutePath()
|
|
450
|
+
position = self._scroll_positions.get(abs_path)
|
|
451
|
+
if position:
|
|
452
|
+
rowindex = self.model.index(os.path.join(path, position))
|
|
453
|
+
if rowindex.isValid():
|
|
454
|
+
self.view.scrollTo(rowindex,
|
|
455
|
+
QAbstractItemView.ScrollHint.PositionAtTop)
|
|
456
|
+
self._scroll_update_needed = False
|
|
457
|
+
|
|
403
458
|
def _file_selector_doubleclick(self, index):
|
|
404
459
|
if self.model.isDir(index):
|
|
405
460
|
self._cd(index)
|
|
@@ -411,13 +466,8 @@ class FileSelector(QWidget):
|
|
|
411
466
|
if len(selected) == 1 and self.model.isDir(selected[0]):
|
|
412
467
|
self._cd(selected[0])
|
|
413
468
|
else:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
self.option_panel.ensure_visible()
|
|
417
|
-
else:
|
|
418
|
-
for index in selected:
|
|
419
|
-
self.SelectForAnalysis.emit(self.model.filePath(index),
|
|
420
|
-
options)
|
|
469
|
+
for index in selected:
|
|
470
|
+
self.SelectForAnalysis.emit(self.model.filePath(index))
|
|
421
471
|
|
|
422
472
|
def _updir(self):
|
|
423
473
|
directory = self.model.rootDirectory()
|
audio_tuner_gui/option_panel.py
CHANGED
|
@@ -29,8 +29,11 @@ __all__ = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
import copy
|
|
33
|
+
|
|
32
34
|
from PyQt6.QtCore import (
|
|
33
35
|
pyqtSignal,
|
|
36
|
+
Qt,
|
|
34
37
|
)
|
|
35
38
|
from PyQt6.QtWidgets import (
|
|
36
39
|
QWidget,
|
|
@@ -231,8 +234,8 @@ class _OptionFloatSpinBox(_OptionIntSpinBox):
|
|
|
231
234
|
def init_main_widget(self):
|
|
232
235
|
widget = QDoubleSpinBox()
|
|
233
236
|
widget.setDecimals(3)
|
|
234
|
-
widget.setMinimum(
|
|
235
|
-
widget.setMaximum(
|
|
237
|
+
widget.setMinimum(com.PITCH_TEMPO_MIN)
|
|
238
|
+
widget.setMaximum(com.PITCH_TEMPO_MAX)
|
|
236
239
|
widget.setSingleStep(.001)
|
|
237
240
|
|
|
238
241
|
self._revert_to_default = False
|
|
@@ -291,15 +294,16 @@ class _OptionComboBox(_OptionLineEdit):
|
|
|
291
294
|
|
|
292
295
|
class OptionPanel(QWidget):
|
|
293
296
|
"""Panel full of option widgets. Inherits from QWidget. Includes a
|
|
294
|
-
`Hold` checkbox to hold options at their current values, a `
|
|
295
|
-
|
|
296
|
-
request that an individual widget
|
|
297
|
-
setting it's line edit box to a
|
|
298
|
-
widgets that actually have a line
|
|
299
|
-
`default` attribute has been set,
|
|
300
|
-
`is_defaults` parameter of the
|
|
301
|
-
`set_default_options` convenience method,
|
|
302
|
-
to defaults` button is connected to, does
|
|
297
|
+
`Hold` checkbox to hold options at their current values, a `Use in
|
|
298
|
+
next analysis` checkbox, a `Revert to defaults` button and an `Apply
|
|
299
|
+
to selected` button. The user can request that an individual widget
|
|
300
|
+
return to it's default value by setting it's line edit box to a
|
|
301
|
+
blank value (this only works for widgets that actually have a line
|
|
302
|
+
edit box, and only if the widget's `default` attribute has been set,
|
|
303
|
+
which can be done using the `is_defaults` parameter of the
|
|
304
|
+
`set_options` method (The `set_default_options` convenience method,
|
|
305
|
+
which is what the `Revert to defaults` button is connected to, does
|
|
306
|
+
this automatically)).
|
|
303
307
|
|
|
304
308
|
Parameters
|
|
305
309
|
----------
|
|
@@ -376,7 +380,9 @@ class OptionPanel(QWidget):
|
|
|
376
380
|
|
|
377
381
|
# Link button
|
|
378
382
|
widget = QToolButton()
|
|
379
|
-
widget.setText('
|
|
383
|
+
widget.setText('Link')
|
|
384
|
+
widget.setShortcut('=')
|
|
385
|
+
widget.setStatusTip('Link pitch and tempo (=)')
|
|
380
386
|
widget.setCheckable(True)
|
|
381
387
|
hbox.addWidget(widget)
|
|
382
388
|
self._link = widget
|
|
@@ -487,14 +493,6 @@ class OptionPanel(QWidget):
|
|
|
487
493
|
self._grid.addWidget(widget, 6, 1)
|
|
488
494
|
self.widgets[title] = widget
|
|
489
495
|
|
|
490
|
-
# Pad input
|
|
491
|
-
title = gcom.OPTION_PAD
|
|
492
|
-
widget = _OptionCheckBox(title)
|
|
493
|
-
widget.setStatusTip("Pad the audio with zeros to ensure the FFT"
|
|
494
|
-
" window doesn't miss the very beginning and end")
|
|
495
|
-
self._grid.addWidget(widget, 7, 1)
|
|
496
|
-
self.widgets[title] = widget
|
|
497
|
-
|
|
498
496
|
|
|
499
497
|
self.widgets[gcom.OPTION_PITCH].widget.valueChanged.connect(
|
|
500
498
|
self._pitch_changed)
|
|
@@ -509,9 +507,14 @@ class OptionPanel(QWidget):
|
|
|
509
507
|
self._button_panel = QWidget(self)
|
|
510
508
|
hbox = QHBoxLayout(self._button_panel)
|
|
511
509
|
|
|
512
|
-
self._hold = QCheckBox('
|
|
510
|
+
self._hold = QCheckBox('Ho&ld')
|
|
513
511
|
self._hold.setStatusTip("Don't update settings to reflect selection")
|
|
514
512
|
hbox.addWidget(self._hold)
|
|
513
|
+
self._use = QCheckBox('&Use in next analysis')
|
|
514
|
+
self._use.setStatusTip('Use these settings to analyze the next'
|
|
515
|
+
' file selected for analysis')
|
|
516
|
+
self._use.checkStateChanged.connect(self._use_changed)
|
|
517
|
+
hbox.addWidget(self._use)
|
|
515
518
|
self._to_defaults = QPushButton('Revert to &defaults')
|
|
516
519
|
self._to_defaults.clicked.connect(self.set_default_options)
|
|
517
520
|
hbox.addWidget(self._to_defaults)
|
|
@@ -520,6 +523,10 @@ class OptionPanel(QWidget):
|
|
|
520
523
|
hbox.addWidget(self._to_selected)
|
|
521
524
|
hbox.setContentsMargins(0, 0, 0, 0)
|
|
522
525
|
|
|
526
|
+
def _use_changed(self, event):
|
|
527
|
+
if event == Qt.CheckState.Checked:
|
|
528
|
+
self._hold.setChecked(True)
|
|
529
|
+
|
|
523
530
|
def _link_toggled(self, event):
|
|
524
531
|
if event:
|
|
525
532
|
self._link_ok = False
|
|
@@ -626,6 +633,17 @@ class OptionPanel(QWidget):
|
|
|
626
633
|
factor = self.widgets[gcom.OPTION_PITCH].get()
|
|
627
634
|
self.PitchChange.emit(factor)
|
|
628
635
|
|
|
636
|
+
def use_options(self) -> bool:
|
|
637
|
+
"""Determine whether the 'Use in next analysis' checkbox is
|
|
638
|
+
checked.
|
|
639
|
+
|
|
640
|
+
Returns
|
|
641
|
+
-------
|
|
642
|
+
bool
|
|
643
|
+
True if checked, False if not.
|
|
644
|
+
"""
|
|
645
|
+
return self._use.isChecked()
|
|
646
|
+
|
|
629
647
|
def get_options(self) -> gcom.Options:
|
|
630
648
|
"""Get the values currently set in the option widgets.
|
|
631
649
|
|
|
@@ -673,6 +691,16 @@ class OptionPanel(QWidget):
|
|
|
673
691
|
options.reread_requested = True
|
|
674
692
|
self.PushOptions.emit(options, True)
|
|
675
693
|
|
|
694
|
+
def get_default_options(self) -> gcom.Options:
|
|
695
|
+
"""Return a copy of the default options.
|
|
696
|
+
|
|
697
|
+
Returns
|
|
698
|
+
-------
|
|
699
|
+
audio_tuner_gui.common.Options
|
|
700
|
+
The default options.
|
|
701
|
+
"""
|
|
702
|
+
return copy.copy(self._default_options)
|
|
703
|
+
|
|
676
704
|
def set_default_options(self):
|
|
677
705
|
"""Set the widgets to the values passed in the constructor's
|
|
678
706
|
`args` parameter. This calls `set_options` with force=True and
|
|
@@ -61,6 +61,7 @@ from PyQt6.QtSvgWidgets import QSvgWidget
|
|
|
61
61
|
import audio_tuner.error_handling as eh
|
|
62
62
|
import audio_tuner.common as com
|
|
63
63
|
import audio_tuner.argument_parser as ap
|
|
64
|
+
import audio_tuner.saved_options as so
|
|
64
65
|
|
|
65
66
|
import audio_tuner_gui.common as gcom
|
|
66
67
|
import audio_tuner_gui.file_selector as fs
|
|
@@ -103,7 +104,10 @@ VERSION_STRING = f'tuner-gui ({APP_TITLE}) {VERSION}\n{ABOUT_TEXT}'
|
|
|
103
104
|
DEBUG_BUTTON = False
|
|
104
105
|
|
|
105
106
|
class _AboutWindow(QWidget):
|
|
106
|
-
def __init__(self,
|
|
107
|
+
def __init__(self,
|
|
108
|
+
parent,
|
|
109
|
+
config_path: str = ap.CONFIG_PATH,
|
|
110
|
+
savefile_path: str = ap.SAVEFILE_PATH):
|
|
107
111
|
super().__init__(parent)
|
|
108
112
|
|
|
109
113
|
self.setWindowFlag(Qt.WindowType.Window)
|
|
@@ -116,7 +120,8 @@ class _AboutWindow(QWidget):
|
|
|
116
120
|
self.text.setTextInteractionFlags(
|
|
117
121
|
Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
118
122
|
|
|
119
|
-
self.config_text = QLabel('Config file path:\n' +
|
|
123
|
+
self.config_text = QLabel('Config file path:\n' + config_path
|
|
124
|
+
+ '\n\nSave file path:\n' + savefile_path, self)
|
|
120
125
|
self.config_text.setCursor(Qt.CursorShape.IBeamCursor)
|
|
121
126
|
self.config_text.setTextInteractionFlags(
|
|
122
127
|
Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
@@ -215,6 +220,28 @@ class _MainUI(QMainWindow):
|
|
|
215
220
|
self.exit_act.setStatusTip('Exit application')
|
|
216
221
|
self.exit_act.triggered.connect(QApplication.instance().quit)
|
|
217
222
|
|
|
223
|
+
self.save_act = QAction(QIcon.fromTheme(gcom.ICON_SAVE),
|
|
224
|
+
'&Save options',
|
|
225
|
+
self)
|
|
226
|
+
self.save_act.setShortcut(QKeySequence('Ctrl+S'))
|
|
227
|
+
self.save_act.setStatusTip(
|
|
228
|
+
'Save the options of the currently selected analyzed audio')
|
|
229
|
+
self.save_act.triggered.connect(self._save)
|
|
230
|
+
|
|
231
|
+
self.reload_saved_act = QAction('&Reload saved options')
|
|
232
|
+
self.reload_saved_act.setStatusTip('Load the saved options for the'
|
|
233
|
+
' currently selected analyzed'
|
|
234
|
+
' audio into the option panel')
|
|
235
|
+
self.reload_saved_act.triggered.connect(self._reload_saved)
|
|
236
|
+
|
|
237
|
+
self.delete_saved_act = QAction(QIcon.fromTheme(gcom.ICON_DEL),
|
|
238
|
+
'&Delete saved options',
|
|
239
|
+
self)
|
|
240
|
+
self.delete_saved_act.setStatusTip(
|
|
241
|
+
'Delete the saved options of the'
|
|
242
|
+
' currently selected analyzed audio')
|
|
243
|
+
self.delete_saved_act.triggered.connect(self._delete_saved)
|
|
244
|
+
|
|
218
245
|
self.showhidden_act = QAction('Show &hidden files', self)
|
|
219
246
|
self.showhidden_act.setCheckable(True)
|
|
220
247
|
self.showhidden_act.setShortcut(QKeySequence('Ctrl+H'))
|
|
@@ -224,19 +251,19 @@ class _MainUI(QMainWindow):
|
|
|
224
251
|
self.play_start_end_act.setCheckable(True)
|
|
225
252
|
self.play_start_end_act.toggled.connect(self._play_start_end_toggled)
|
|
226
253
|
|
|
227
|
-
self.toggle_options_panel_act = QAction('Show &
|
|
254
|
+
self.toggle_options_panel_act = QAction('Show &option panel', self)
|
|
228
255
|
self.toggle_options_panel_act.setCheckable(True)
|
|
229
256
|
self.toggle_options_panel_act.setChecked(False)
|
|
230
257
|
self.toggle_options_panel_act.setShortcut('F2')
|
|
231
258
|
self.toggle_options_panel_act.setStatusTip(
|
|
232
|
-
'Switch between file selector and
|
|
259
|
+
'Switch between file selector and option panel')
|
|
233
260
|
self.toggle_options_panel_act.triggered.connect(
|
|
234
261
|
self._toggle_options_panel)
|
|
235
262
|
|
|
236
263
|
self.toggle_toggle_options_panel_act = QAction(self)
|
|
237
264
|
self.toggle_toggle_options_panel_act.setIcon(QIcon(gcom.ICON_OPTIONS))
|
|
238
265
|
self.toggle_toggle_options_panel_act.setStatusTip(
|
|
239
|
-
'Switch between file selector and
|
|
266
|
+
'Switch between file selector and option panel')
|
|
240
267
|
self.toggle_toggle_options_panel_act.triggered.connect(
|
|
241
268
|
self._toggle_toggle_options_panel)
|
|
242
269
|
|
|
@@ -245,7 +272,7 @@ class _MainUI(QMainWindow):
|
|
|
245
272
|
self.cancel_act.setShortcut('Escape')
|
|
246
273
|
self.cancel_act.setStatusTip('Cancel')
|
|
247
274
|
|
|
248
|
-
self.export_act = QAction('&Export
|
|
275
|
+
self.export_act = QAction('&Export audio...')
|
|
249
276
|
self.export_act.triggered.connect(self._export)
|
|
250
277
|
if com.mpv_error is not None:
|
|
251
278
|
self.export_act.setEnabled(False)
|
|
@@ -293,10 +320,9 @@ class _MainUI(QMainWindow):
|
|
|
293
320
|
self.pause_act.triggered.connect(self._pause)
|
|
294
321
|
|
|
295
322
|
self.stop_act = QAction(self)
|
|
296
|
-
self.stop_act.
|
|
297
|
-
Qt.Key.Key_MediaStop])
|
|
323
|
+
self.stop_act.setShortcut(Qt.Key.Key_MediaStop)
|
|
298
324
|
self.stop_act.setIcon(QIcon.fromTheme(gcom.ICON_STOP))
|
|
299
|
-
self.stop_act.setStatusTip('Stop
|
|
325
|
+
self.stop_act.setStatusTip('Stop')
|
|
300
326
|
self.stop_act.triggered.connect(self._stop)
|
|
301
327
|
|
|
302
328
|
if DEBUG_BUTTON:
|
|
@@ -390,6 +416,10 @@ class _MainUI(QMainWindow):
|
|
|
390
416
|
menubar = self.menuBar()
|
|
391
417
|
|
|
392
418
|
file_menu = menubar.addMenu('&File')
|
|
419
|
+
file_menu.addAction(self.reload_saved_act)
|
|
420
|
+
file_menu.addAction(self.save_act)
|
|
421
|
+
file_menu.addAction(self.delete_saved_act)
|
|
422
|
+
file_menu.addSeparator()
|
|
393
423
|
file_menu.addAction(self.export_act)
|
|
394
424
|
file_menu.addSeparator()
|
|
395
425
|
file_menu.addAction(self.exit_act)
|
|
@@ -532,6 +562,53 @@ class _MainUI(QMainWindow):
|
|
|
532
562
|
self.cancel_button.hide()
|
|
533
563
|
self.cancel_act.setEnabled(False)
|
|
534
564
|
|
|
565
|
+
def _save(self):
|
|
566
|
+
audio_path = self.analyzed_audio.current_selection
|
|
567
|
+
args = self.analyzed_audio.current_options.argparse_namespace()
|
|
568
|
+
if self.saved_options.save(args, audio_path):
|
|
569
|
+
self._update_statusbar('Saved')
|
|
570
|
+
self.delete_saved_act.setEnabled(True)
|
|
571
|
+
self.reload_saved_act.setEnabled(True)
|
|
572
|
+
else:
|
|
573
|
+
self._update_statusbar('Error while saving')
|
|
574
|
+
|
|
575
|
+
def _reload_saved(self):
|
|
576
|
+
path = self.analyzed_audio.current_selection
|
|
577
|
+
options = self.option_panel.get_default_options()
|
|
578
|
+
saved_args = self.saved_options.get_args(path)
|
|
579
|
+
if saved_args:
|
|
580
|
+
options.merge_args(saved_args)
|
|
581
|
+
try:
|
|
582
|
+
options.init_tuning_system()
|
|
583
|
+
except ValueError:
|
|
584
|
+
s = (f'invalid ref_note:'
|
|
585
|
+
f' {options[gcom.OPTION_REF_NOTE]}')
|
|
586
|
+
self._log(s, lv.LOG_LEVEL_ERROR)
|
|
587
|
+
return
|
|
588
|
+
self.option_panel.set_options(options, force=True)
|
|
589
|
+
|
|
590
|
+
def _delete_saved(self):
|
|
591
|
+
audio_path = self.analyzed_audio.current_selection
|
|
592
|
+
message = (f'Delete saved options for'
|
|
593
|
+
f'\n{audio_path}'
|
|
594
|
+
f'\nfrom'
|
|
595
|
+
f'\n{self.saved_options.path}?')
|
|
596
|
+
reply = QMessageBox.question(
|
|
597
|
+
self,
|
|
598
|
+
'Confirm',
|
|
599
|
+
message,
|
|
600
|
+
QMessageBox.StandardButton.Yes
|
|
601
|
+
| QMessageBox.StandardButton.No,
|
|
602
|
+
QMessageBox.StandardButton.No)
|
|
603
|
+
|
|
604
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
605
|
+
if self.saved_options.delete(audio_path):
|
|
606
|
+
self._update_statusbar('Deleted')
|
|
607
|
+
self.delete_saved_act.setEnabled(False)
|
|
608
|
+
self.reload_saved_act.setEnabled(False)
|
|
609
|
+
else:
|
|
610
|
+
self._update_statusbar('Error while deleting')
|
|
611
|
+
|
|
535
612
|
def _export(self):
|
|
536
613
|
self.export_window.show(self.analyzed_audio.current_selection,
|
|
537
614
|
self.analyzed_audio.current_listed_title,
|
|
@@ -581,6 +658,14 @@ class _MainUI(QMainWindow):
|
|
|
581
658
|
self.option_panel.set_apply_enabled(True)
|
|
582
659
|
self.show_log_plot_act.setEnabled(True)
|
|
583
660
|
self.show_linear_plot_act.setEnabled(True)
|
|
661
|
+
self.save_act.setEnabled(True)
|
|
662
|
+
selected = self.analyzed_audio.current_selection
|
|
663
|
+
if self.saved_options.has_options(selected):
|
|
664
|
+
self.delete_saved_act.setEnabled(True)
|
|
665
|
+
self.reload_saved_act.setEnabled(True)
|
|
666
|
+
else:
|
|
667
|
+
self.delete_saved_act.setEnabled(False)
|
|
668
|
+
self.reload_saved_act.setEnabled(False)
|
|
584
669
|
if not self._exporting_in_progress and com.mpv_error is None:
|
|
585
670
|
self.export_act.setEnabled(True)
|
|
586
671
|
|
|
@@ -590,6 +675,9 @@ class _MainUI(QMainWindow):
|
|
|
590
675
|
self.show_log_plot_act.setEnabled(False)
|
|
591
676
|
self.show_linear_plot_act.setEnabled(False)
|
|
592
677
|
self.export_act.setEnabled(False)
|
|
678
|
+
self.save_act.setEnabled(False)
|
|
679
|
+
self.delete_saved_act.setEnabled(False)
|
|
680
|
+
self.reload_saved_act.setEnabled(False)
|
|
593
681
|
|
|
594
682
|
def _play_start_end_toggled(self, checked):
|
|
595
683
|
if checked:
|
|
@@ -726,6 +814,26 @@ class _MainUI(QMainWindow):
|
|
|
726
814
|
now = self.player.get_current_position()
|
|
727
815
|
self.option_panel.set_end(now)
|
|
728
816
|
|
|
817
|
+
def _add_audio(self, path: str):
|
|
818
|
+
if self.option_panel.use_options():
|
|
819
|
+
options = self.option_panel.get_options()
|
|
820
|
+
if options is None:
|
|
821
|
+
self.option_panel.ensure_visible()
|
|
822
|
+
return
|
|
823
|
+
else:
|
|
824
|
+
options = self.option_panel.get_default_options()
|
|
825
|
+
saved_args = self.saved_options.get_args(path)
|
|
826
|
+
if saved_args:
|
|
827
|
+
options.merge_args(saved_args)
|
|
828
|
+
try:
|
|
829
|
+
options.init_tuning_system()
|
|
830
|
+
except ValueError:
|
|
831
|
+
s = (f'invalid ref_note:'
|
|
832
|
+
f' {options[gcom.OPTION_REF_NOTE]}')
|
|
833
|
+
self._log(s, lv.LOG_LEVEL_ERROR)
|
|
834
|
+
return
|
|
835
|
+
self.analyzed_audio.add_audio(path, options)
|
|
836
|
+
|
|
729
837
|
def _initUI(self):
|
|
730
838
|
self.resize(800, 850)
|
|
731
839
|
self.setWindowTitle(APP_TITLE)
|
|
@@ -775,8 +883,8 @@ class _MainUI(QMainWindow):
|
|
|
775
883
|
self.export_window = ex.ExportWindow(self)
|
|
776
884
|
self.export_window.set_dir(self.file_selector.model.rootDirectory())
|
|
777
885
|
|
|
778
|
-
file_selector.SelectForAnalysis.connect(
|
|
779
|
-
self.showhidden_act.toggled.connect(file_selector.
|
|
886
|
+
file_selector.SelectForAnalysis.connect(self._add_audio)
|
|
887
|
+
self.showhidden_act.toggled.connect(file_selector.set_show_hidden)
|
|
780
888
|
file_selector.UpdateStatusbar.connect(self._update_statusbar)
|
|
781
889
|
file_selector.model.rootPathChanged.connect(self.export_window.set_dir)
|
|
782
890
|
self.analyzed_audio.UpdateStatusbar.connect(self._update_statusbar)
|
|
@@ -813,10 +921,15 @@ class _MainUI(QMainWindow):
|
|
|
813
921
|
analyzed_audio.DisplayResult.connect(self._update_now_playing)
|
|
814
922
|
analyzed_audio.PushOptions.connect(option_panel.set_options)
|
|
815
923
|
|
|
816
|
-
self.about_window = _AboutWindow(self)
|
|
817
|
-
|
|
818
924
|
self.log_viewer = lv.LogViewer(self)
|
|
819
925
|
|
|
926
|
+
self.saved_options = so.SavedOptions(self.args.savefile,
|
|
927
|
+
print_msg=self._log)
|
|
928
|
+
|
|
929
|
+
self.about_window = _AboutWindow(self,
|
|
930
|
+
config_path=self.args.config,
|
|
931
|
+
savefile_path=self.saved_options.path)
|
|
932
|
+
|
|
820
933
|
self.show()
|
|
821
934
|
|
|
822
935
|
hsplitter.moveSplitter(500, 1)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: audio-tuner-gui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Graphical interface for Audio Tuner
|
|
5
5
|
Project-URL: Homepage, https://codeberg.org/bluesloth/audio_tuner
|
|
6
6
|
Project-URL: Repository, https://codeberg.org/bluesloth/audio_tuner.git
|
|
7
7
|
Project-URL: Issues, https://codeberg.org/bluesloth/audio_tuner/issues
|
|
8
|
-
Project-URL: Documentation, https://audio-tuner.readthedocs.io/
|
|
8
|
+
Project-URL: Documentation, https://audio-tuner.readthedocs.io/
|
|
9
|
+
Project-URL: News, https://codeberg.org/bluesloth/audio_tuner/src/branch/master/NEWS
|
|
9
10
|
Author-email: Jessie Blue Cassell <bluesloth600@gmail.com>
|
|
10
11
|
License-File: COPYING
|
|
11
12
|
Keywords: audio,music,pitch shift,player,tempo shift,tuning
|
|
@@ -48,6 +49,23 @@ What it's not
|
|
|
48
49
|
Audio Tuner is not autotune. It pitch corrects entire songs, not individual
|
|
49
50
|
notes.
|
|
50
51
|
|
|
52
|
+
Dependencies
|
|
53
|
+
~~~~~~~~~~~~
|
|
54
|
+
|
|
55
|
+
**audio-tuner-gui** requires **Python version 3.11 or higher**, **libmpv2**,
|
|
56
|
+
and the `audio-tuner`_ and `PyQt6`_ python packages.
|
|
57
|
+
|
|
58
|
+
Note that pip is not able to install **libmpv2**. If your operating system
|
|
59
|
+
has a package manager, use that to install it. See the `documentation`_
|
|
60
|
+
for details.
|
|
61
|
+
|
|
62
|
+
Audio Tuner can be configured to use **ffmpeg** and **ffprobe** instead of
|
|
63
|
+
libmpv2, at the cost of reduced functionality in the GUI.
|
|
64
|
+
|
|
65
|
+
.. _documentation: https://audio-tuner.readthedocs.io/
|
|
66
|
+
.. _audio-tuner: https://pypi.org/project/audio-tuner/
|
|
67
|
+
.. _PyQt6: https://www.riverbankcomputing.com/software/pyqt/
|
|
68
|
+
|
|
51
69
|
Free as in Freedom
|
|
52
70
|
~~~~~~~~~~~~~~~~~~
|
|
53
71
|
|
|
@@ -75,19 +93,7 @@ stable, but of course nothing's guaranteed until it reaches version 1.0.0.
|
|
|
75
93
|
.. _Semantic Versioning: https://semver.org
|
|
76
94
|
.. _PEP 440: https://peps.python.org/pep-0440/
|
|
77
95
|
|
|
78
|
-
Dependencies
|
|
79
|
-
~~~~~~~~~~~~
|
|
80
|
-
|
|
81
|
-
**audio-tuner-gui** requires **Python version 3.11 or higher**, **libmpv2**,
|
|
82
|
-
and the **audio-tuner** and **PyQt6** python packages.
|
|
83
|
-
|
|
84
|
-
Audio Tuner can be configured to use **ffmpeg** and **ffprobe** instead of
|
|
85
|
-
libmpv2, at the cost of reduced functionality in the GUI.
|
|
86
|
-
|
|
87
96
|
See Also
|
|
88
97
|
~~~~~~~~
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
instructions.
|
|
92
|
-
|
|
93
|
-
.. _html documentation: https://audio-tuner.readthedocs.io/en/latest/
|
|
99
|
+
audio-tuner: https://pypi.org/project/audio-tuner/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
audio_tuner_gui/__init__.py,sha256=
|
|
2
|
-
audio_tuner_gui/analysis.py,sha256=
|
|
3
|
-
audio_tuner_gui/common.py,sha256=
|
|
1
|
+
audio_tuner_gui/__init__.py,sha256=17XOOeuLEPrnb3mwJM4noZKHB7AzHe5Nula67Zy9ETE,1181
|
|
2
|
+
audio_tuner_gui/analysis.py,sha256=cgtTnLf1Uha1dz49WeJ0dYNyCh_0-vBE-kPyLtM_Qlk,26307
|
|
3
|
+
audio_tuner_gui/common.py,sha256=SxKbsdZo6JYtKiRl9FZJVh-L3YTgQtvPTX_OdMA6avQ,13223
|
|
4
4
|
audio_tuner_gui/display.py,sha256=-h1G57UVsL2_Z1m14Z9rSvzcrOEevhLEMHosnrxX6aI,19825
|
|
5
5
|
audio_tuner_gui/export.py,sha256=g6FNYm7EaHDCXSIR7fZjTU71Zs2Pku0JjxBKB6DWNfI,19878
|
|
6
|
-
audio_tuner_gui/file_selector.py,sha256=
|
|
6
|
+
audio_tuner_gui/file_selector.py,sha256=19IXnVBHwct0fJwUZr2awbutJTEa-tGr82g-jzOWaBk,15679
|
|
7
7
|
audio_tuner_gui/log_viewer.py,sha256=PNf0knuQkRvktazkg4mQQhJ5O2HGs8v3w9IJZLwAPbI,4547
|
|
8
|
-
audio_tuner_gui/option_panel.py,sha256=
|
|
8
|
+
audio_tuner_gui/option_panel.py,sha256=fg9NK8eHmXrry6nVxP1weiHOpK5F9w9lpy8kD6p8F8s,22412
|
|
9
9
|
audio_tuner_gui/player.py,sha256=USYmJXYiYR9iIAUyct6t3rpcz_zvlkf1r_ng_ZXAu0g,23467
|
|
10
10
|
audio_tuner_gui/icons/audio_tuner_icon.ico,sha256=vUipvB6K3b8LnzPV5-V6axqW12KDcloGvt9BKGry9uQ,28838
|
|
11
11
|
audio_tuner_gui/icons/audio_tuner_icon.svg,sha256=V_mLVlyF4X3uu_t77QlxTRPmkVXuIjldC5aKCCMYXnU,6180
|
|
@@ -20,9 +20,9 @@ audio_tuner_gui/icons/preferences-other.png,sha256=atKxIcgcOspwAOLbS09PzmeZ72is-
|
|
|
20
20
|
audio_tuner_gui/icons/process-stop.png,sha256=1--WGyi9wpODEiIwhhxvjAS3wOOFfFqb90xnNWncUt0,1963
|
|
21
21
|
audio_tuner_gui/icons/white-logo.svg,sha256=IQG-9kQLgtvuerVCvOLqiNt3_D6WPSEi37FE_ZiOS68,7488
|
|
22
22
|
audio_tuner_gui/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
audio_tuner_gui/scripts/tuner_gui.py,sha256=
|
|
24
|
-
audio_tuner_gui-0.
|
|
25
|
-
audio_tuner_gui-0.
|
|
26
|
-
audio_tuner_gui-0.
|
|
27
|
-
audio_tuner_gui-0.
|
|
28
|
-
audio_tuner_gui-0.
|
|
23
|
+
audio_tuner_gui/scripts/tuner_gui.py,sha256=iuIG4rekUFLGa7eOz_qlI_X97ILkhIhavPAj6WoLo7c,38268
|
|
24
|
+
audio_tuner_gui-0.11.0.dist-info/METADATA,sha256=6-81gUplZTu7mQN4vvT6rRcInMa8mBIzUvT4PkOoc-0,3513
|
|
25
|
+
audio_tuner_gui-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
audio_tuner_gui-0.11.0.dist-info/entry_points.txt,sha256=z_NHwJeBb9B9jnk_F6qZmOtWsobvOIrIFWuIdeBMa_w,65
|
|
27
|
+
audio_tuner_gui-0.11.0.dist-info/licenses/COPYING,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
28
|
+
audio_tuner_gui-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|