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,683 @@
|
|
|
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
|
+
"""Option panel for the GUI."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__author__ = 'Jessie Blue Cassell'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
'OptionPanel',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from PyQt6.QtCore import (
|
|
33
|
+
pyqtSignal,
|
|
34
|
+
)
|
|
35
|
+
from PyQt6.QtWidgets import (
|
|
36
|
+
QWidget,
|
|
37
|
+
QPushButton,
|
|
38
|
+
QVBoxLayout,
|
|
39
|
+
QHBoxLayout,
|
|
40
|
+
QLineEdit,
|
|
41
|
+
QSizePolicy,
|
|
42
|
+
QCheckBox,
|
|
43
|
+
QScrollArea,
|
|
44
|
+
QLabel,
|
|
45
|
+
QGridLayout,
|
|
46
|
+
QComboBox,
|
|
47
|
+
QSpinBox,
|
|
48
|
+
QDoubleSpinBox,
|
|
49
|
+
QFrame,
|
|
50
|
+
QToolButton,
|
|
51
|
+
)
|
|
52
|
+
from PyQt6.QtGui import (
|
|
53
|
+
QColor,
|
|
54
|
+
QPalette,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
import audio_tuner.tuning_systems as tuning_systems
|
|
58
|
+
import audio_tuner.common as com
|
|
59
|
+
import audio_tuner_gui.common as gcom
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _OptionLineEdit(QWidget):
|
|
63
|
+
def __init__(self, label, unit=None, none_sentinel=None):
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
self.none_sentinel = none_sentinel
|
|
67
|
+
|
|
68
|
+
self.error_condition = False
|
|
69
|
+
self.label = QLabel(label)
|
|
70
|
+
self.label.setSizePolicy(QSizePolicy())
|
|
71
|
+
self.default = None
|
|
72
|
+
|
|
73
|
+
if unit is None:
|
|
74
|
+
self.bottom = self.init_main_widget()
|
|
75
|
+
self.bottom.setSizePolicy(QSizePolicy())
|
|
76
|
+
self.widget = self.bottom
|
|
77
|
+
else:
|
|
78
|
+
self.bottom = QWidget()
|
|
79
|
+
self.hbox = QHBoxLayout(self.bottom)
|
|
80
|
+
self.widget = self.init_main_widget()
|
|
81
|
+
self.widget.setSizePolicy(QSizePolicy())
|
|
82
|
+
|
|
83
|
+
self.init_unit(unit)
|
|
84
|
+
|
|
85
|
+
self.hbox.addWidget(self.widget)
|
|
86
|
+
self.hbox.addWidget(self.unit)
|
|
87
|
+
self.hbox.addStretch()
|
|
88
|
+
self.hbox.setContentsMargins(0, 0, 0, 0)
|
|
89
|
+
|
|
90
|
+
self.vbox = QVBoxLayout(self)
|
|
91
|
+
self.vbox.addStretch()
|
|
92
|
+
self.vbox.addWidget(self.label)
|
|
93
|
+
self.vbox.addWidget(self.bottom)
|
|
94
|
+
self.vbox.addStretch()
|
|
95
|
+
|
|
96
|
+
self.orig_palette = self.palette()
|
|
97
|
+
|
|
98
|
+
def init_main_widget(self):
|
|
99
|
+
line_edit = QLineEdit()
|
|
100
|
+
line_edit.editingFinished.connect(self._edit_finished)
|
|
101
|
+
return line_edit
|
|
102
|
+
|
|
103
|
+
def init_unit(self, unit):
|
|
104
|
+
self.unit = QLabel(unit)
|
|
105
|
+
self.unit.setSizePolicy(QSizePolicy())
|
|
106
|
+
|
|
107
|
+
def _edit_finished(self):
|
|
108
|
+
selection = self.widget.text()
|
|
109
|
+
if self.default and (selection is None or selection == ''):
|
|
110
|
+
self.set(self.default)
|
|
111
|
+
|
|
112
|
+
def set(self, selection):
|
|
113
|
+
if self.none_sentinel is not None and selection is None:
|
|
114
|
+
selection = self.none_sentinel
|
|
115
|
+
self.widget.setText(selection)
|
|
116
|
+
self.unset_error()
|
|
117
|
+
|
|
118
|
+
def get(self):
|
|
119
|
+
self.unset_error()
|
|
120
|
+
ret = self.widget.text().strip()
|
|
121
|
+
if self.none_sentinel is not None and ret == self.none_sentinel:
|
|
122
|
+
ret = None
|
|
123
|
+
return ret
|
|
124
|
+
|
|
125
|
+
def set_error(self):
|
|
126
|
+
self.error_condition = True
|
|
127
|
+
palette = self.palette()
|
|
128
|
+
palette.setColor(QPalette.ColorRole.Base, QColor(255, 0, 0))
|
|
129
|
+
self.setPalette(palette)
|
|
130
|
+
self.widget.setFocus()
|
|
131
|
+
|
|
132
|
+
def unset_error(self):
|
|
133
|
+
if self.error_condition:
|
|
134
|
+
self.error_condition = False
|
|
135
|
+
self.setPalette(self.orig_palette)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _OptionStartEnd(_OptionLineEdit):
|
|
139
|
+
button_clicked = pyqtSignal()
|
|
140
|
+
|
|
141
|
+
def init_unit(self, unit):
|
|
142
|
+
self.unit = QToolButton()
|
|
143
|
+
self.unit.setText('Set to now')
|
|
144
|
+
self.unit.setStatusTip('Set to current position')
|
|
145
|
+
self.unit.setEnabled(False)
|
|
146
|
+
self.unit.clicked.connect(self.button_clicked.emit)
|
|
147
|
+
|
|
148
|
+
def _edit_finished(self):
|
|
149
|
+
selection = self.widget.text()
|
|
150
|
+
if self.none_sentinel and (selection is None or selection == ''):
|
|
151
|
+
self.set(self.none_sentinel)
|
|
152
|
+
|
|
153
|
+
def get(self) -> str:
|
|
154
|
+
self.unset_error()
|
|
155
|
+
ret = self.widget.text().replace('#', '').strip()
|
|
156
|
+
if self.none_sentinel is not None and ret == self.none_sentinel:
|
|
157
|
+
ret = None
|
|
158
|
+
return ret
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _OptionCheckBox(QWidget):
|
|
162
|
+
def __init__(self, label):
|
|
163
|
+
super().__init__()
|
|
164
|
+
|
|
165
|
+
self.vbox = QVBoxLayout(self)
|
|
166
|
+
self.widget = QCheckBox(label)
|
|
167
|
+
self.vbox.addWidget(self.widget)
|
|
168
|
+
self.vbox.setContentsMargins(15, 5, 5, 5)
|
|
169
|
+
|
|
170
|
+
def set(self, selection):
|
|
171
|
+
self.widget.setChecked(selection)
|
|
172
|
+
|
|
173
|
+
def get(self):
|
|
174
|
+
return self.widget.isChecked()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _OptionFloatLineEdit(_OptionLineEdit):
|
|
178
|
+
def set(self, selection):
|
|
179
|
+
super().set(str(selection))
|
|
180
|
+
|
|
181
|
+
def get(self):
|
|
182
|
+
try:
|
|
183
|
+
ret = float(super().get())
|
|
184
|
+
if ret <= 0:
|
|
185
|
+
raise ValueError
|
|
186
|
+
except ValueError:
|
|
187
|
+
self.set_error()
|
|
188
|
+
return gcom.ERROR_SENTINEL
|
|
189
|
+
return ret
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _OptionIntSpinBox(_OptionLineEdit):
|
|
193
|
+
def init_main_widget(self):
|
|
194
|
+
widget = QSpinBox()
|
|
195
|
+
widget.setMinimum(1)
|
|
196
|
+
|
|
197
|
+
self._revert_to_default = False
|
|
198
|
+
widget.lineEdit().textEdited.connect(self._text_edited)
|
|
199
|
+
widget.lineEdit().editingFinished.connect(self._edit_finished)
|
|
200
|
+
|
|
201
|
+
return widget
|
|
202
|
+
|
|
203
|
+
def _edit_finished(self):
|
|
204
|
+
if self.default and self._revert_to_default:
|
|
205
|
+
self.set(self.default)
|
|
206
|
+
self._revert_to_default = False
|
|
207
|
+
|
|
208
|
+
def _text_edited(self, text):
|
|
209
|
+
if text == '':
|
|
210
|
+
self._revert_to_default = True
|
|
211
|
+
else:
|
|
212
|
+
self._revert_to_default = False
|
|
213
|
+
|
|
214
|
+
def set(self, selection):
|
|
215
|
+
self.widget.setValue(selection)
|
|
216
|
+
self.unset_error()
|
|
217
|
+
|
|
218
|
+
def get(self):
|
|
219
|
+
self.unset_error()
|
|
220
|
+
try:
|
|
221
|
+
ret = self.widget.value()
|
|
222
|
+
if ret <= 0:
|
|
223
|
+
raise ValueError
|
|
224
|
+
except ValueError:
|
|
225
|
+
self.set_error()
|
|
226
|
+
return gcom.ERROR_SENTINEL
|
|
227
|
+
return ret
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class _OptionFloatSpinBox(_OptionIntSpinBox):
|
|
231
|
+
def init_main_widget(self):
|
|
232
|
+
widget = QDoubleSpinBox()
|
|
233
|
+
widget.setDecimals(3)
|
|
234
|
+
widget.setMinimum(0.25)
|
|
235
|
+
widget.setMaximum(4.0)
|
|
236
|
+
widget.setSingleStep(.001)
|
|
237
|
+
|
|
238
|
+
self._revert_to_default = False
|
|
239
|
+
widget.lineEdit().textEdited.connect(self._text_edited)
|
|
240
|
+
widget.lineEdit().editingFinished.connect(self._edit_finished)
|
|
241
|
+
|
|
242
|
+
return widget
|
|
243
|
+
|
|
244
|
+
def set(self, selection):
|
|
245
|
+
super().set(selection)
|
|
246
|
+
self._precise_value = selection
|
|
247
|
+
|
|
248
|
+
def get_precise(self):
|
|
249
|
+
imprecise = super().get()
|
|
250
|
+
if (imprecise != gcom.ERROR_SENTINEL
|
|
251
|
+
and abs(imprecise - self._precise_value) < .0006):
|
|
252
|
+
return self._precise_value
|
|
253
|
+
else:
|
|
254
|
+
return imprecise
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _prettify(text):
|
|
258
|
+
text = text.replace('b', tuning_systems.flat_symbol)
|
|
259
|
+
text = text.replace('#', tuning_systems.sharp_symbol)
|
|
260
|
+
return text
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class _OptionRefnoteLineEdit(_OptionLineEdit):
|
|
264
|
+
def get(self):
|
|
265
|
+
text = self.widget.text().strip()
|
|
266
|
+
text = text.capitalize()
|
|
267
|
+
text = _prettify(text)
|
|
268
|
+
self.set(text)
|
|
269
|
+
return text
|
|
270
|
+
|
|
271
|
+
def set(self, selection):
|
|
272
|
+
super().set(_prettify(selection))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class _OptionComboBox(_OptionLineEdit):
|
|
276
|
+
def __init__(self, label, items):
|
|
277
|
+
self.items = items
|
|
278
|
+
super().__init__(label)
|
|
279
|
+
|
|
280
|
+
def init_main_widget(self):
|
|
281
|
+
widget = QComboBox()
|
|
282
|
+
widget.addItems(self.items)
|
|
283
|
+
return widget
|
|
284
|
+
|
|
285
|
+
def set(self, selection):
|
|
286
|
+
self.widget.setCurrentText(selection)
|
|
287
|
+
|
|
288
|
+
def get(self):
|
|
289
|
+
return self.widget.currentText()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class OptionPanel(QWidget):
|
|
293
|
+
"""Panel full of option widgets. Inherits from QWidget. Includes a
|
|
294
|
+
`Hold` checkbox to hold options at their current values, a `Revert
|
|
295
|
+
to defaults` button and an `Apply to selected` button. The user can
|
|
296
|
+
request that an individual widget return to it's default value by
|
|
297
|
+
setting it's line edit box to a blank value (this only works for
|
|
298
|
+
widgets that actually have a line edit box, and only if the widget's
|
|
299
|
+
`default` attribute has been set, which can be done using the
|
|
300
|
+
`is_defaults` parameter of the `set_options` method (The
|
|
301
|
+
`set_default_options` convenience method, which is what the `Revert
|
|
302
|
+
to defaults` button is connected to, does this automatically)).
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
args : argparse.Namespace
|
|
307
|
+
Stores the default options which the `set_default_options`
|
|
308
|
+
method uses. Note that the defaults aren't actually set until
|
|
309
|
+
`set_default_options` is called.
|
|
310
|
+
|
|
311
|
+
Attributes
|
|
312
|
+
----------
|
|
313
|
+
widgets : dict
|
|
314
|
+
The widgets. The keys are the option names defined in
|
|
315
|
+
audio_tuner_gui.common.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
PushOptions = pyqtSignal(gcom.Options, bool)
|
|
319
|
+
"""Signal emitted when the user pushes a button to request that
|
|
320
|
+
options be applied to the selected analyzed audio.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
audio_tuner_gui.common.Options
|
|
325
|
+
The options to apply.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
PitchChange = pyqtSignal(float)
|
|
329
|
+
"""Signal emitted when the value in the pitch correction widget
|
|
330
|
+
changes.
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
float
|
|
335
|
+
The new pitch correction value.
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
PayAttentionToMe = pyqtSignal()
|
|
339
|
+
"""Signal emitted to request that the option panel be made visible
|
|
340
|
+
if it isn't already.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def __init__(self, args):
|
|
345
|
+
super().__init__()
|
|
346
|
+
|
|
347
|
+
self._args = args
|
|
348
|
+
self._link_ok = True
|
|
349
|
+
|
|
350
|
+
self._init_option_area()
|
|
351
|
+
self._init_buttons()
|
|
352
|
+
|
|
353
|
+
vbox = QVBoxLayout(self)
|
|
354
|
+
vbox.addWidget(self._scroll_area)
|
|
355
|
+
vbox.addWidget(self._button_panel)
|
|
356
|
+
vbox.setSpacing(5)
|
|
357
|
+
vbox.setContentsMargins(3, 3, 3, 3)
|
|
358
|
+
|
|
359
|
+
self._default_options = gcom.Options(args)
|
|
360
|
+
|
|
361
|
+
def _init_option_area(self):
|
|
362
|
+
self.widgets = {}
|
|
363
|
+
|
|
364
|
+
self._scroll_area = QScrollArea()
|
|
365
|
+
self._panel = QWidget()
|
|
366
|
+
self._grid = QGridLayout(self._panel)
|
|
367
|
+
|
|
368
|
+
box_widget = QWidget()
|
|
369
|
+
hbox = QHBoxLayout(box_widget)
|
|
370
|
+
|
|
371
|
+
# pitch correction
|
|
372
|
+
title = gcom.OPTION_PITCH
|
|
373
|
+
widget = _OptionFloatSpinBox(title)
|
|
374
|
+
hbox.addWidget(widget)
|
|
375
|
+
self.widgets[title] = widget
|
|
376
|
+
|
|
377
|
+
# Link button
|
|
378
|
+
widget = QToolButton()
|
|
379
|
+
widget.setText('&Link')
|
|
380
|
+
widget.setCheckable(True)
|
|
381
|
+
hbox.addWidget(widget)
|
|
382
|
+
self._link = widget
|
|
383
|
+
|
|
384
|
+
# tempo correction
|
|
385
|
+
title = gcom.OPTION_TEMPO
|
|
386
|
+
widget = _OptionFloatSpinBox(title)
|
|
387
|
+
hbox.addWidget(widget)
|
|
388
|
+
self.widgets[title] = widget
|
|
389
|
+
|
|
390
|
+
self._grid.addWidget(box_widget, 0, 0, 1, 2)
|
|
391
|
+
|
|
392
|
+
keybox = QWidget()
|
|
393
|
+
khbox = QHBoxLayout(keybox)
|
|
394
|
+
khbox.setContentsMargins(0, 0, 0, 0)
|
|
395
|
+
|
|
396
|
+
# down one
|
|
397
|
+
widget = QToolButton()
|
|
398
|
+
widget.setText('-100c')
|
|
399
|
+
widget.setShortcut('<')
|
|
400
|
+
widget.clicked.connect(self._pitch_down)
|
|
401
|
+
khbox.addWidget(widget)
|
|
402
|
+
|
|
403
|
+
# up one
|
|
404
|
+
widget = QToolButton()
|
|
405
|
+
widget.setText('+100c')
|
|
406
|
+
widget.setShortcut('>')
|
|
407
|
+
widget.clicked.connect(self._pitch_up)
|
|
408
|
+
khbox.addWidget(widget)
|
|
409
|
+
|
|
410
|
+
self._grid.addWidget(keybox, 1, 0)
|
|
411
|
+
|
|
412
|
+
# Reread button
|
|
413
|
+
self._reread_button = QPushButton()
|
|
414
|
+
self._reread_button.setText('&Reread file with correction')
|
|
415
|
+
self._reread_button.clicked.connect(self._reread)
|
|
416
|
+
self._grid.addWidget(self._reread_button, 1, 1)
|
|
417
|
+
|
|
418
|
+
# Separator line
|
|
419
|
+
widget = QFrame()
|
|
420
|
+
widget.setFrameShape(QFrame.Shape.HLine)
|
|
421
|
+
widget.setFrameShadow(QFrame.Shadow.Sunken)
|
|
422
|
+
self._grid.addWidget(widget, 2, 0, 1, 2)
|
|
423
|
+
|
|
424
|
+
# Analysis option widgets
|
|
425
|
+
|
|
426
|
+
# Tuning System
|
|
427
|
+
title = gcom.OPTION_TUNING_SYSTEM
|
|
428
|
+
widget = _OptionComboBox(title,
|
|
429
|
+
('Equal Temperament',
|
|
430
|
+
'Pythagorean'))
|
|
431
|
+
self._grid.addWidget(widget, 3, 0)
|
|
432
|
+
self.widgets[title] = widget
|
|
433
|
+
|
|
434
|
+
# Reference Frequency
|
|
435
|
+
title = gcom.OPTION_REF_FREQ
|
|
436
|
+
widget = _OptionFloatLineEdit(title, unit='Hz')
|
|
437
|
+
widget.setStatusTip('Frequency of the reference note')
|
|
438
|
+
self._grid.addWidget(widget, 4, 0)
|
|
439
|
+
self.widgets[title] = widget
|
|
440
|
+
|
|
441
|
+
# Reference Note
|
|
442
|
+
title = gcom.OPTION_REF_NOTE
|
|
443
|
+
widget = _OptionRefnoteLineEdit(title)
|
|
444
|
+
self._grid.addWidget(widget, 5, 0)
|
|
445
|
+
self.widgets[title] = widget
|
|
446
|
+
|
|
447
|
+
# Start Time
|
|
448
|
+
title = gcom.OPTION_START
|
|
449
|
+
widget = _OptionStartEnd(title, unit=True, none_sentinel='Beginning')
|
|
450
|
+
widget.setStatusTip('Ignore audio before this time')
|
|
451
|
+
self._grid.addWidget(widget, 3, 1)
|
|
452
|
+
self.widgets[title] = widget
|
|
453
|
+
|
|
454
|
+
# End Time
|
|
455
|
+
title = gcom.OPTION_END
|
|
456
|
+
widget = _OptionStartEnd(title, unit=True, none_sentinel='End')
|
|
457
|
+
widget.setStatusTip('Ignore the audio after this time')
|
|
458
|
+
self._grid.addWidget(widget, 4, 1)
|
|
459
|
+
self.widgets[title] = widget
|
|
460
|
+
|
|
461
|
+
# Low Cut
|
|
462
|
+
title = gcom.OPTION_LOW_CUT
|
|
463
|
+
widget = _OptionFloatLineEdit(title, unit='Hz')
|
|
464
|
+
widget.setStatusTip('Ignore frequencies below this')
|
|
465
|
+
self._grid.addWidget(widget, 6, 0)
|
|
466
|
+
self.widgets[title] = widget
|
|
467
|
+
|
|
468
|
+
# High Cut
|
|
469
|
+
title = gcom.OPTION_HIGH_CUT
|
|
470
|
+
widget = _OptionFloatLineEdit(title, unit='Hz')
|
|
471
|
+
widget.setStatusTip('Ignore frequencies above this')
|
|
472
|
+
self._grid.addWidget(widget, 7, 0)
|
|
473
|
+
self.widgets[title] = widget
|
|
474
|
+
|
|
475
|
+
# dB Range
|
|
476
|
+
title = gcom.OPTION_DB_RANGE
|
|
477
|
+
widget = _OptionFloatLineEdit(title, unit='dB')
|
|
478
|
+
widget.setStatusTip('Ignore frequencies this much fainter'
|
|
479
|
+
' than the highest peak')
|
|
480
|
+
self._grid.addWidget(widget, 5, 1)
|
|
481
|
+
self.widgets[title] = widget
|
|
482
|
+
|
|
483
|
+
# Max Peaks
|
|
484
|
+
title = gcom.OPTION_MAX_PEAKS
|
|
485
|
+
widget = _OptionIntSpinBox(title)
|
|
486
|
+
widget.setStatusTip('Maximum number of frequencies to show')
|
|
487
|
+
self._grid.addWidget(widget, 6, 1)
|
|
488
|
+
self.widgets[title] = widget
|
|
489
|
+
|
|
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
|
+
|
|
499
|
+
self.widgets[gcom.OPTION_PITCH].widget.valueChanged.connect(
|
|
500
|
+
self._pitch_changed)
|
|
501
|
+
self.widgets[gcom.OPTION_TEMPO].widget.valueChanged.connect(
|
|
502
|
+
self._tempo_changed)
|
|
503
|
+
self._link.toggled.connect(self._link_toggled)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
self._scroll_area.setWidget(self._panel)
|
|
507
|
+
|
|
508
|
+
def _init_buttons(self):
|
|
509
|
+
self._button_panel = QWidget(self)
|
|
510
|
+
hbox = QHBoxLayout(self._button_panel)
|
|
511
|
+
|
|
512
|
+
self._hold = QCheckBox('Hold')
|
|
513
|
+
self._hold.setStatusTip("Don't update settings to reflect selection")
|
|
514
|
+
hbox.addWidget(self._hold)
|
|
515
|
+
self._to_defaults = QPushButton('Revert to &defaults')
|
|
516
|
+
self._to_defaults.clicked.connect(self.set_default_options)
|
|
517
|
+
hbox.addWidget(self._to_defaults)
|
|
518
|
+
self._to_selected = QPushButton('&Apply to selected')
|
|
519
|
+
self._to_selected.clicked.connect(self._push_options)
|
|
520
|
+
hbox.addWidget(self._to_selected)
|
|
521
|
+
hbox.setContentsMargins(0, 0, 0, 0)
|
|
522
|
+
|
|
523
|
+
def _link_toggled(self, event):
|
|
524
|
+
if event:
|
|
525
|
+
self._link_ok = False
|
|
526
|
+
pitch = self.widgets[gcom.OPTION_PITCH].get()
|
|
527
|
+
self.widgets[gcom.OPTION_TEMPO].set(pitch)
|
|
528
|
+
self._link_ok = True
|
|
529
|
+
|
|
530
|
+
def _pitch_changed(self, event):
|
|
531
|
+
self.PitchChange.emit(event)
|
|
532
|
+
if self._link_ok and self._link.isChecked():
|
|
533
|
+
self._link_ok = False
|
|
534
|
+
self.widgets[gcom.OPTION_TEMPO].set(event)
|
|
535
|
+
self._link_ok = True
|
|
536
|
+
|
|
537
|
+
def _tempo_changed(self, event):
|
|
538
|
+
if self._link_ok and self._link.isChecked():
|
|
539
|
+
self._link_ok = False
|
|
540
|
+
self.widgets[gcom.OPTION_PITCH].set(event)
|
|
541
|
+
self._link_ok = True
|
|
542
|
+
|
|
543
|
+
def _pitch_up(self):
|
|
544
|
+
p = self.widgets[gcom.OPTION_PITCH].get_precise()
|
|
545
|
+
p *= com.cents_to_ratio(100)
|
|
546
|
+
self.widgets[gcom.OPTION_PITCH].set(p)
|
|
547
|
+
|
|
548
|
+
def _pitch_down(self):
|
|
549
|
+
p = self.widgets[gcom.OPTION_PITCH].get_precise()
|
|
550
|
+
p /= com.cents_to_ratio(100)
|
|
551
|
+
self.widgets[gcom.OPTION_PITCH].set(p)
|
|
552
|
+
|
|
553
|
+
def set_start(self, start):
|
|
554
|
+
"""Set the value of the start time option.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
start : str
|
|
559
|
+
The start time.
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
if start is not None:
|
|
563
|
+
self.widgets[gcom.OPTION_START].set(f'{start:.3f}')
|
|
564
|
+
|
|
565
|
+
def set_end(self, end):
|
|
566
|
+
"""Set the value of the end time option.
|
|
567
|
+
|
|
568
|
+
Parameters
|
|
569
|
+
----------
|
|
570
|
+
end : str
|
|
571
|
+
The end time.
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
if end is not None:
|
|
575
|
+
self.widgets[gcom.OPTION_END].set(f'{end:.3f}')
|
|
576
|
+
|
|
577
|
+
def start_end_enable(self, enable):
|
|
578
|
+
"""Enable or disable the buttons that set the start and end
|
|
579
|
+
values to the current player position.
|
|
580
|
+
|
|
581
|
+
Parameters
|
|
582
|
+
----------
|
|
583
|
+
enable : bool
|
|
584
|
+
Enable if True, disable if False.
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
self.widgets[gcom.OPTION_START].unit.setEnabled(enable)
|
|
588
|
+
self.widgets[gcom.OPTION_END].unit.setEnabled(enable)
|
|
589
|
+
|
|
590
|
+
def ensure_visible(self):
|
|
591
|
+
"""Trigger emission of the PayAttentionToMe signal."""
|
|
592
|
+
|
|
593
|
+
self.PayAttentionToMe.emit()
|
|
594
|
+
|
|
595
|
+
def set_options(self, options, force=False, is_defaults=False):
|
|
596
|
+
"""Set all the widgets in the panel to the specified values,
|
|
597
|
+
unless the `Hold` checkbox is checked, in which case nothing
|
|
598
|
+
changes (however, a `PitchChange` signal is still emitted in
|
|
599
|
+
that case, to make sure anything that uses that signal updates
|
|
600
|
+
properly even when `Hold` is checked).
|
|
601
|
+
|
|
602
|
+
Parameters
|
|
603
|
+
----------
|
|
604
|
+
options : audio_tuner_gui.common.Options
|
|
605
|
+
An Options object containing the values to set.
|
|
606
|
+
force : bool, optional
|
|
607
|
+
If True, ignore the `Hold` checkbox and set the options even
|
|
608
|
+
if it's checked. Default False.
|
|
609
|
+
is_defaults : bool, optional
|
|
610
|
+
If the options being set are the defaults, set this to True.
|
|
611
|
+
This causes the `default` attribute of the widgets to be set
|
|
612
|
+
in addition to the value, so that the widgets know how to
|
|
613
|
+
return themselves to the default setting if the user
|
|
614
|
+
requests it.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
if force or not self._hold.isChecked():
|
|
618
|
+
for opt in options:
|
|
619
|
+
try:
|
|
620
|
+
self.widgets[opt].set(options[opt])
|
|
621
|
+
if is_defaults:
|
|
622
|
+
self.widgets[opt].default = options[opt]
|
|
623
|
+
except KeyError:
|
|
624
|
+
pass
|
|
625
|
+
else:
|
|
626
|
+
factor = self.widgets[gcom.OPTION_PITCH].get()
|
|
627
|
+
self.PitchChange.emit(factor)
|
|
628
|
+
|
|
629
|
+
def get_options(self) -> gcom.Options:
|
|
630
|
+
"""Get the values currently set in the option widgets.
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
audio_tuner_gui.common.Options
|
|
635
|
+
The values.
|
|
636
|
+
"""
|
|
637
|
+
|
|
638
|
+
options = gcom.Options(self._args)
|
|
639
|
+
for title in self.widgets:
|
|
640
|
+
options[title] = self.widgets[title].get()
|
|
641
|
+
try:
|
|
642
|
+
self.widgets[gcom.OPTION_REF_NOTE].unset_error()
|
|
643
|
+
options.init_tuning_system()
|
|
644
|
+
except ValueError as err:
|
|
645
|
+
if err.args[0] == 'Invalid reference note':
|
|
646
|
+
self.widgets[gcom.OPTION_REF_NOTE].set_error()
|
|
647
|
+
options[gcom.OPTION_REF_NOTE] = gcom.ERROR_SENTINEL
|
|
648
|
+
for title in self.widgets:
|
|
649
|
+
if options[title] == gcom.ERROR_SENTINEL:
|
|
650
|
+
return None
|
|
651
|
+
return options
|
|
652
|
+
|
|
653
|
+
def _push_options(self):
|
|
654
|
+
options = self.get_options()
|
|
655
|
+
if options is not None:
|
|
656
|
+
self.PushOptions.emit(options, False)
|
|
657
|
+
|
|
658
|
+
def set_apply_enabled(self, enable):
|
|
659
|
+
"""Enable or disable the `Apply to selected` button.
|
|
660
|
+
|
|
661
|
+
Parameters
|
|
662
|
+
----------
|
|
663
|
+
enable : bool
|
|
664
|
+
Enable if True, disable if False.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
self._to_selected.setEnabled(enable)
|
|
668
|
+
self._reread_button.setEnabled(enable)
|
|
669
|
+
|
|
670
|
+
def _reread(self):
|
|
671
|
+
options = self.get_options()
|
|
672
|
+
if options is not None:
|
|
673
|
+
options.reread_requested = True
|
|
674
|
+
self.PushOptions.emit(options, True)
|
|
675
|
+
|
|
676
|
+
def set_default_options(self):
|
|
677
|
+
"""Set the widgets to the values passed in the constructor's
|
|
678
|
+
`args` parameter. This calls `set_options` with force=True and
|
|
679
|
+
is_defaults=True. It also sets the link button to the linked state.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
self.set_options(self._default_options, force=True, is_defaults=True)
|
|
683
|
+
self._link.setChecked(True)
|