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.
@@ -0,0 +1,350 @@
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
+ """Miscellaneous constants and classes for the GUI."""
22
+
23
+
24
+ __author__ = 'Jessie Blue Cassell'
25
+
26
+
27
+ __all__ = [
28
+ 'OPTION_TUNING_SYSTEM',
29
+ 'OPTION_REF_FREQ',
30
+ 'OPTION_REF_NOTE',
31
+ 'OPTION_START',
32
+ 'OPTION_END',
33
+ 'OPTION_LOW_CUT',
34
+ 'OPTION_HIGH_CUT',
35
+ 'OPTION_DB_RANGE',
36
+ 'OPTION_MAX_PEAKS',
37
+ 'OPTION_PAD',
38
+ 'OPTION_SIZE_EXP',
39
+ 'OPTION_SAMPLERATE',
40
+ 'OPTION_PITCH',
41
+ 'OPTION_BACKENDS',
42
+ 'ERROR_SENTINEL',
43
+ 'REDO_LEVEL_ALL',
44
+ 'REDO_LEVEL_FIND_PEAKS',
45
+ 'REDO_LEVEL_TUNING_SYSTEM',
46
+ 'REDO_LEVEL_NONE',
47
+ 'APP_ICON',
48
+ 'ICON_OPTIONS',
49
+ 'ICON_FILES',
50
+ 'ICON_BACK',
51
+ 'ICON_FORWARD',
52
+ 'ICON_UP',
53
+ 'ICON_HOME',
54
+ 'ICON_REMOVE',
55
+ 'ICON_CLEAR',
56
+ 'ICON_EXIT',
57
+ 'ICON_CANCEL',
58
+ 'ICON_ABOUT',
59
+ 'ICON_MESSAGE_LOG',
60
+ 'ICON_ALERT',
61
+ 'ICON_PLAYER',
62
+ 'ICON_PLAY',
63
+ 'ICON_PAUSE',
64
+ 'ICON_STOP',
65
+ 'ICON_PLAYER_BACK',
66
+ 'ICON_PLAYER_FORWARD',
67
+ 'ICON_AUDIO_DEVICE',
68
+ 'RowData',
69
+ 'SplitAction',
70
+ 'Options',
71
+ ]
72
+
73
+
74
+ import os
75
+ from typing import TypedDict
76
+
77
+ from PyQt6.QtGui import (
78
+ QIcon,
79
+ QAction,
80
+ )
81
+
82
+ import audio_tuner.tuning_systems as tuning_systems
83
+
84
+ from audio_tuner_gui import PKGDIR
85
+
86
+
87
+ # Option names
88
+ OPTION_TUNING_SYSTEM = 'Tuning system'
89
+ OPTION_REF_FREQ = 'Reference frequency'
90
+ OPTION_REF_NOTE = 'Reference note'
91
+ OPTION_START = 'Start time'
92
+ OPTION_END = 'End time'
93
+ OPTION_LOW_CUT = 'Low cut'
94
+ OPTION_HIGH_CUT = 'High cut'
95
+ OPTION_DB_RANGE = 'dB range'
96
+ OPTION_MAX_PEAKS = 'Max peaks'
97
+ OPTION_PAD = 'Pad input'
98
+ OPTION_SIZE_EXP = 'FFT size exponent'
99
+ OPTION_SAMPLERATE = 'Sample rate'
100
+ OPTION_BACKENDS = 'backends'
101
+ OPTION_PITCH = 'Pitch correction factor'
102
+ OPTION_TEMPO = 'Tempo correction factor'
103
+
104
+ ERROR_SENTINEL = 'ERROR'
105
+
106
+ REDO_LEVEL_ALL = 3
107
+ REDO_LEVEL_FIND_PEAKS = 2
108
+ REDO_LEVEL_TUNING_SYSTEM = 1
109
+ REDO_LEVEL_NONE = 0
110
+
111
+ APP_ICON = os.path.join(PKGDIR, 'icons/audio_tuner_icon.svg')
112
+
113
+ # These are missing from ThemeIcon
114
+ ICON_OPTIONS = os.path.join(PKGDIR, 'icons/preferences-other.png')
115
+ ICON_FILES = os.path.join(PKGDIR, 'icons/folder.png')
116
+
117
+ # This one is in ThemeIcon but not on Windows for some reason
118
+ ICON_CANCEL = os.path.join(PKGDIR, 'icons/process-stop.png')
119
+
120
+ LOGO_LIGHT = os.path.join(PKGDIR, 'icons/audio_tuner_logo_light.svg')
121
+ LOGO_DARK = os.path.join(PKGDIR, 'icons/audio_tuner_logo_dark.svg')
122
+ BRAINMADE_WHITE = os.path.join(PKGDIR, 'icons/white-logo.svg')
123
+ BRAINMADE_BLACK = os.path.join(PKGDIR, 'icons/black-logo.svg')
124
+ GPL_WHITE = os.path.join(PKGDIR, 'icons/gpl-v3-logo_white.svg')
125
+ GPL_RED = os.path.join(PKGDIR, 'icons/gpl-v3-logo_red.svg')
126
+
127
+ try:
128
+ ICON_BACK = QIcon.ThemeIcon.GoPrevious
129
+ ICON_FORWARD = QIcon.ThemeIcon.GoNext
130
+ ICON_UP = QIcon.ThemeIcon.GoUp
131
+ ICON_HOME = QIcon.ThemeIcon.GoHome
132
+ ICON_REMOVE = QIcon.ThemeIcon.EditDelete
133
+ ICON_CLEAR = QIcon.ThemeIcon.EditDelete
134
+ ICON_EXIT = QIcon.ThemeIcon.ApplicationExit
135
+ # ICON_OPTIONS = 'preferences-other'
136
+ # ICON_CANCEL = QIcon.ThemeIcon.ProcessStop
137
+ ICON_ABOUT = QIcon.ThemeIcon.HelpAbout
138
+ ICON_MESSAGE_LOG = QIcon.ThemeIcon.FormatJustifyLeft
139
+ # ICON_FILES = 'folder'
140
+ ICON_ALERT = QIcon.ThemeIcon.DialogWarning
141
+ ICON_PLAYER = QIcon.ThemeIcon.MultimediaPlayer
142
+ ICON_PLAY = QIcon.ThemeIcon.MediaPlaybackStart
143
+ ICON_PAUSE = QIcon.ThemeIcon.MediaPlaybackPause
144
+ ICON_STOP = QIcon.ThemeIcon.MediaPlaybackStop
145
+ ICON_PLAYER_BACK = QIcon.ThemeIcon.MediaSeekBackward
146
+ ICON_PLAYER_FORWARD = QIcon.ThemeIcon.MediaSeekForward
147
+ ICON_AUDIO_DEVICE = QIcon.ThemeIcon.AudioCard
148
+ except AttributeError:
149
+ ICON_BACK = 'go-previous'
150
+ ICON_FORWARD = 'go-next'
151
+ ICON_UP = 'go-up'
152
+ ICON_HOME = 'go-home'
153
+ ICON_REMOVE = 'edit-delete'
154
+ ICON_CLEAR = 'edit-delete'
155
+ ICON_EXIT = 'application-exit'
156
+ # ICON_OPTIONS = 'preferences-other'
157
+ # ICON_CANCEL = 'process-stop'
158
+ ICON_ABOUT = 'help-about'
159
+ ICON_MESSAGE_LOG = 'format-justify-left'
160
+ # ICON_FILES = 'folder'
161
+ ICON_ALERT = 'dialog-warning'
162
+ ICON_PLAYER = 'multimedia-player'
163
+ ICON_PLAY = 'media-playback-start'
164
+ ICON_PAUSE = 'media-playback-pause'
165
+ ICON_STOP = 'media-playback-stop'
166
+ ICON_PLAYER_BACK = 'media-seek-backward'
167
+ ICON_PLAYER_FORWARD = 'media-seek-forward'
168
+ ICON_AUDIO_DEVICE = 'audio-card'
169
+
170
+
171
+ class RowData(TypedDict):
172
+ """This is the type of the dicts used to send data to the result
173
+ rows in the display.
174
+
175
+ Keys
176
+ ----
177
+ note : str
178
+ The name of the note.
179
+ standard : float
180
+ The frequency of the note as defined by the tuning system.
181
+ measured : float
182
+ The actual measured frequency of the note.
183
+ cents : float
184
+ The discrepancy in cents.
185
+ correction : float
186
+ The correction factor needed to make the note match the
187
+ standard.
188
+ """
189
+
190
+ note: str
191
+ standard: float
192
+ measured: float
193
+ cents: float
194
+ correction: float
195
+
196
+
197
+ class SplitAction():
198
+ """A class that's two QActions in one. One is for menu items and
199
+ includes a title, and the other is for buttons and doesn't. Useful
200
+ for avoiding unwanted tooltips on buttons while still having a title
201
+ available for menus. It's sort of a drop in replacement for
202
+ QAction, but not quite, since not all the methods are implemented
203
+ and `triggered.connect` is changed to `triggered_connect`.
204
+
205
+ Parameters
206
+ ----------
207
+ title : str
208
+ The title of the menu version of the QAction.
209
+
210
+ All other parameters are passed to the QAction constructors.
211
+ """
212
+
213
+ def __init__(self, title, *args, **kwargs):
214
+ self.menu_action = QAction(title, *args, **kwargs)
215
+ self.button_action = QAction(*args, **kwargs)
216
+
217
+ def menu(self):
218
+ """Return the menu version of the QAction."""
219
+
220
+ return self.menu_action
221
+
222
+ def button(self):
223
+ """Return the button version of the QAction."""
224
+
225
+ return self.button_action
226
+
227
+ def setIcon(self, *args, **kwargs):
228
+ """Call the `setIcon` method of both QActions."""
229
+
230
+ self.menu_action.setIcon(*args, **kwargs)
231
+ self.button_action.setIcon(*args, **kwargs)
232
+
233
+ def setShortcut(self, s):
234
+ """Call the `setShortcut` method of only the menu QAction."""
235
+
236
+ self.menu_action.setShortcut(s)
237
+
238
+ def setEnabled(self, e):
239
+ """Call the `setEnabled` method of both QActions."""
240
+
241
+ self.menu_action.setEnabled(e)
242
+ self.button_action.setEnabled(e)
243
+
244
+ def setStatusTip(self, s):
245
+ """Call the `setStatusTip` method of both QActions."""
246
+
247
+ self.menu_action.setStatusTip(s)
248
+ self.button_action.setStatusTip(s)
249
+
250
+ def triggered_connect(self, *args, **kwargs):
251
+ """Call `triggered.connect` on both QActions."""
252
+
253
+ self.menu_action.triggered.connect(*args, **kwargs)
254
+ self.button_action.triggered.connect(*args, **kwargs)
255
+
256
+
257
+ class Options(dict):
258
+ """A dictionary subclass for storing options.
259
+
260
+ Parameters
261
+ ----------
262
+ args : argparse.Namespace
263
+ The namespace object resulting from calling
264
+ audio_tuner.argument_parser.merge_args. Used to initialize the
265
+ options.
266
+
267
+ Attributes
268
+ ----------
269
+ tuning_system
270
+ An instance of one of the tuning systems defined in
271
+ audio_tuner.tuning_systems. Which one it is depends on the
272
+ value associated with the OPTION_TUNING_SYSTEM key. Only
273
+ available after `init_tuning_system` has been called.
274
+ """
275
+
276
+ def __init__(self, args):
277
+ super().__init__()
278
+ self[OPTION_TUNING_SYSTEM] = self._guify_tuning_system(args.tuning)
279
+ self[OPTION_REF_FREQ] = args.ref_freq
280
+ self[OPTION_REF_NOTE] = args.ref_note
281
+ self[OPTION_START] = args.start
282
+ self[OPTION_END] = args.end
283
+ self[OPTION_LOW_CUT] = args.low_cut
284
+ self[OPTION_HIGH_CUT] = args.high_cut
285
+ self[OPTION_DB_RANGE] = args.dB_range
286
+ self[OPTION_MAX_PEAKS] = args.max_peaks
287
+ self[OPTION_PAD] = not args.nopad
288
+ self[OPTION_SIZE_EXP] = args.size_exp
289
+ self[OPTION_SAMPLERATE] = args.samplerate
290
+ self[OPTION_BACKENDS] = args.backends
291
+ self[OPTION_PITCH] = 1.0
292
+ self[OPTION_TEMPO] = 1.0
293
+
294
+ def _guify_tuning_system(self, tuning_system):
295
+ return tuning_system.replace('_', ' ').title()
296
+
297
+ def redo_level(self, old_options):
298
+ """Compare the options stored in this instance of Options to the
299
+ options stored in an older one to find out how much analysis
300
+ needs to be redone.
301
+
302
+ Parameters
303
+ ----------
304
+ old_options : Options
305
+ The old Options instance.
306
+
307
+ Returns
308
+ -------
309
+ int
310
+ The redo level.
311
+ """
312
+
313
+ if (old_options is None
314
+ or self[OPTION_START] != old_options[OPTION_START]
315
+ or self[OPTION_END] != old_options[OPTION_END]
316
+ or self[OPTION_PAD] != old_options[OPTION_PAD]
317
+ or self[OPTION_SIZE_EXP] != old_options[OPTION_SIZE_EXP]
318
+ or self[OPTION_BACKENDS] != old_options[OPTION_BACKENDS]
319
+ or self[OPTION_SAMPLERATE] != old_options[OPTION_SAMPLERATE]):
320
+ return REDO_LEVEL_ALL
321
+ if (self[OPTION_LOW_CUT] != old_options[OPTION_LOW_CUT]
322
+ or self[OPTION_HIGH_CUT] != old_options[OPTION_HIGH_CUT]
323
+ or self[OPTION_DB_RANGE] != old_options[OPTION_DB_RANGE]
324
+ or self[OPTION_MAX_PEAKS] != old_options[OPTION_MAX_PEAKS]):
325
+ return REDO_LEVEL_FIND_PEAKS
326
+ if (self[OPTION_TUNING_SYSTEM] != old_options[OPTION_TUNING_SYSTEM]
327
+ or self[OPTION_REF_FREQ] != old_options[OPTION_REF_FREQ]
328
+ or self[OPTION_PITCH] != old_options[OPTION_PITCH]
329
+ or self[OPTION_REF_NOTE] != old_options[OPTION_REF_NOTE]):
330
+ return REDO_LEVEL_TUNING_SYSTEM
331
+
332
+ return REDO_LEVEL_NONE
333
+
334
+ def init_tuning_system(self):
335
+ """Initialize the tuning system and store it as the
336
+ `tuning_system` attribute.
337
+ """
338
+
339
+ ref_note = self[OPTION_REF_NOTE]
340
+ ref_freq = self[OPTION_REF_FREQ]
341
+ if ref_freq == ERROR_SENTINEL:
342
+ self.tuning_system = None
343
+ elif self[OPTION_TUNING_SYSTEM] == 'Equal Temperament':
344
+ self.tuning_system = tuning_systems.EqualTemperament(
345
+ ref_note=ref_note,
346
+ ref_freq=ref_freq)
347
+ elif self[OPTION_TUNING_SYSTEM] == 'Pythagorean':
348
+ self.tuning_system = tuning_systems.Pythagorean(
349
+ ref_note=ref_note,
350
+ ref_freq=ref_freq)