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,863 @@
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
+ """The GUI frontend for Audio Tuner."""
22
+
23
+
24
+ __author__ = 'Jessie Blue Cassell'
25
+
26
+
27
+ import sys
28
+ import os
29
+ from collections import deque
30
+
31
+ from PyQt6.QtCore import (
32
+ Qt,
33
+ pyqtSignal,
34
+ )
35
+ from PyQt6.QtWidgets import (
36
+ QApplication,
37
+ QWidget,
38
+ QPushButton,
39
+ QMessageBox,
40
+ QMainWindow,
41
+ QVBoxLayout,
42
+ QHBoxLayout,
43
+ QSplitter,
44
+ QToolButton,
45
+ QSizePolicy,
46
+ QProgressBar,
47
+ QLabel,
48
+ QStackedWidget,
49
+ QLCDNumber,
50
+ QSlider,
51
+ )
52
+ from PyQt6.QtGui import (
53
+ QIcon,
54
+ QAction,
55
+ QKeySequence,
56
+ QFont,
57
+ QPalette,
58
+ )
59
+ from PyQt6.QtSvgWidgets import QSvgWidget
60
+
61
+ import audio_tuner.error_handling as eh
62
+ import audio_tuner.common as com
63
+ import audio_tuner.argument_parser as ap
64
+
65
+ import audio_tuner_gui.common as gcom
66
+ import audio_tuner_gui.file_selector as fs
67
+ import audio_tuner_gui.option_panel as op
68
+ import audio_tuner_gui.analysis as ga
69
+ import audio_tuner_gui.log_viewer as lv
70
+ import audio_tuner_gui.display as disp
71
+ import audio_tuner_gui.player as pl
72
+ import audio_tuner_gui.export as ex
73
+
74
+ from audio_tuner_gui import VERSION
75
+
76
+
77
+ APP_TITLE = 'Audio Tuner'
78
+ QUIT_CONFIRMATION_MESSAGE = f'Quit {APP_TITLE}?'
79
+ INIT_STATUS_MESSAGE = f'Welcome to {APP_TITLE}'
80
+
81
+ DESCRIPTION = (f'This is the GUI for {APP_TITLE}. Command line arguments'
82
+ ' can be used to run with different default settings.'
83
+ ' Arguments not listed here will be passed to the'
84
+ ' Qt GUI toolkit.')
85
+
86
+ ABOUT_TEXT = """
87
+ Copyright \N{COPYRIGHT SIGN} 2025, 2026 Jessie Blue Cassell.
88
+ License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
89
+ This is free software: you are free to change and redistribute it.
90
+ There is NO WARRANTY, to the extent permitted by law.
91
+
92
+ Folder, Options, and Process Stop icons from the
93
+ Adwaita icon theme, by the GNOME Project.
94
+ <https://download.gnome.org/sources/adwaita-icon-theme/>
95
+ Copyright \N{COPYRIGHT SIGN} 2002-2014
96
+ License: CC-BY-SA-3.0 or LGPL-3
97
+ <https://creativecommons.org/licenses/by-sa/3.0/>
98
+ <https://gnu.org/licenses/lgpl-3.0.html>
99
+ """
100
+
101
+ VERSION_STRING = f'tuner-gui ({APP_TITLE}) {VERSION}\n{ABOUT_TEXT}'
102
+
103
+ DEBUG_BUTTON = False
104
+
105
+ class _AboutWindow(QWidget):
106
+ def __init__(self, parent):
107
+ super().__init__(parent)
108
+
109
+ self.setWindowFlag(Qt.WindowType.Window)
110
+ self.setWindowTitle('About')
111
+
112
+ self.vbox = QVBoxLayout(self)
113
+
114
+ self.text = QLabel(ABOUT_TEXT, self)
115
+ self.text.setCursor(Qt.CursorShape.IBeamCursor)
116
+ self.text.setTextInteractionFlags(
117
+ Qt.TextInteractionFlag.TextSelectableByMouse)
118
+
119
+ self.config_text = QLabel('Config file path:\n' + ap.CONFIG_PATH, self)
120
+ self.config_text.setCursor(Qt.CursorShape.IBeamCursor)
121
+ self.config_text.setTextInteractionFlags(
122
+ Qt.TextInteractionFlag.TextSelectableByMouse)
123
+
124
+ th = self.text.fontMetrics().height()
125
+ pal = QApplication.palette()
126
+ text_color = pal.color(QPalette.ColorRole.WindowText)
127
+ bg_color = pal.color(QPalette.ColorRole.Window)
128
+ lightmode = text_color.lightness() < bg_color.lightness()
129
+
130
+ if lightmode:
131
+ self.logo = QSvgWidget(gcom.LOGO_DARK)
132
+ else:
133
+ self.logo = QSvgWidget(gcom.LOGO_LIGHT)
134
+ self.logo.renderer().setAspectRatioMode(
135
+ Qt.AspectRatioMode.KeepAspectRatio)
136
+ self.logo.setFixedSize(int(35 * th), int(7 * th))
137
+ self.vbox.addWidget(self.logo)
138
+
139
+ self.ver = QLabel(f'Version {VERSION}', self)
140
+ self.ver.setAlignment(Qt.AlignmentFlag.AlignCenter)
141
+ font = QFont()
142
+ font.setPointSize(16)
143
+ self.ver.setFont(font)
144
+ self.ver.setCursor(Qt.CursorShape.IBeamCursor)
145
+ self.ver.setTextInteractionFlags(
146
+ Qt.TextInteractionFlag.TextSelectableByMouse)
147
+ self.vbox.addWidget(self.ver)
148
+
149
+ self.vbox.addWidget(self.text)
150
+
151
+ self.vbox.addWidget(self.config_text)
152
+
153
+ self.hboxwidget = QWidget()
154
+ self.hbox = QHBoxLayout(self.hboxwidget)
155
+ self.hbox.setAlignment(Qt.AlignmentFlag.AlignLeft)
156
+
157
+ if lightmode:
158
+ self.gpl = QSvgWidget(gcom.GPL_RED)
159
+ else:
160
+ self.gpl = QSvgWidget(gcom.GPL_WHITE)
161
+ self.gpl.renderer().setAspectRatioMode(
162
+ Qt.AspectRatioMode.KeepAspectRatio)
163
+ self.gpl.setFixedSize(int(7 * th), int(4 * th))
164
+ self.hbox.addWidget(self.gpl)
165
+
166
+ if lightmode:
167
+ self.brainmade = QSvgWidget(gcom.BRAINMADE_BLACK)
168
+ else:
169
+ self.brainmade = QSvgWidget(gcom.BRAINMADE_WHITE)
170
+ self.brainmade.renderer().setAspectRatioMode(
171
+ Qt.AspectRatioMode.KeepAspectRatio)
172
+ self.brainmade.setFixedSize(int(5 * th), int(1.5 * th))
173
+ self.hbox.addWidget(self.brainmade)
174
+
175
+ self.vbox.addWidget(self.hboxwidget)
176
+
177
+ self.button = QPushButton('Close')
178
+ self.button.clicked.connect(self.close)
179
+ self.vbox.addWidget(self.button)
180
+
181
+ def close(self):
182
+ """Close the window."""
183
+ self.hide()
184
+
185
+
186
+ class _Slider(QSlider):
187
+ MouseReleased = pyqtSignal()
188
+ MousePressed = pyqtSignal()
189
+
190
+ def mousePressEvent(self, event):
191
+ super().mousePressEvent(event)
192
+ self.MousePressed.emit()
193
+
194
+ def mouseReleaseEvent(self, event):
195
+ super().mouseReleaseEvent(event)
196
+ self.MouseReleased.emit()
197
+
198
+
199
+ class _MainUI(QMainWindow):
200
+ def __init__(self, args, message_queue):
201
+ super().__init__()
202
+
203
+ self.args = args
204
+ self.message_queue = message_queue
205
+ self._slider_update_enabled = True
206
+ self.duration = None
207
+ self._exporting_in_progress = False
208
+ self._currently_playing_start = None
209
+ self._currently_playing_end = None
210
+
211
+ self.exit_act = QAction(QIcon.fromTheme(gcom.ICON_EXIT),
212
+ 'Exit',
213
+ self)
214
+ self.exit_act.setShortcut(QKeySequence('Ctrl+Q'))
215
+ self.exit_act.setStatusTip('Exit application')
216
+ self.exit_act.triggered.connect(QApplication.instance().quit)
217
+
218
+ self.showhidden_act = QAction('Show &hidden files', self)
219
+ self.showhidden_act.setCheckable(True)
220
+ self.showhidden_act.setShortcut(QKeySequence('Ctrl+H'))
221
+
222
+ self.play_start_end_act = QAction(
223
+ f'Play between {gcom.OPTION_START} and {gcom.OPTION_END}')
224
+ self.play_start_end_act.setCheckable(True)
225
+ self.play_start_end_act.toggled.connect(self._play_start_end_toggled)
226
+
227
+ self.toggle_options_panel_act = QAction('Show &options panel', self)
228
+ self.toggle_options_panel_act.setCheckable(True)
229
+ self.toggle_options_panel_act.setChecked(False)
230
+ self.toggle_options_panel_act.setShortcut('F2')
231
+ self.toggle_options_panel_act.setStatusTip(
232
+ 'Switch between file selector and options panel')
233
+ self.toggle_options_panel_act.triggered.connect(
234
+ self._toggle_options_panel)
235
+
236
+ self.toggle_toggle_options_panel_act = QAction(self)
237
+ self.toggle_toggle_options_panel_act.setIcon(QIcon(gcom.ICON_OPTIONS))
238
+ self.toggle_toggle_options_panel_act.setStatusTip(
239
+ 'Switch between file selector and options panel')
240
+ self.toggle_toggle_options_panel_act.triggered.connect(
241
+ self._toggle_toggle_options_panel)
242
+
243
+ self.cancel_act = QAction(self)
244
+ self.cancel_act.setIcon(QIcon.fromTheme(gcom.ICON_CANCEL))
245
+ self.cancel_act.setShortcut('Escape')
246
+ self.cancel_act.setStatusTip('Cancel')
247
+
248
+ self.export_act = QAction('&Export Audio...')
249
+ self.export_act.triggered.connect(self._export)
250
+ if com.mpv_error is not None:
251
+ self.export_act.setEnabled(False)
252
+
253
+ self.about_act = QAction('&About ' + APP_TITLE, self)
254
+ self.about_act.setIcon(QIcon.fromTheme(gcom.ICON_ABOUT))
255
+ self.about_act.triggered.connect(self._about_show)
256
+
257
+ self.log_viewer_act = gcom.SplitAction('&Message log', self)
258
+ self.log_viewer_act.setShortcut(QKeySequence('Ctrl+E'))
259
+ self.log_viewer_act.setIcon(QIcon.fromTheme(gcom.ICON_MESSAGE_LOG))
260
+ self.log_viewer_act.setStatusTip('View message log')
261
+ self.log_viewer_act.triggered_connect(self._log_show)
262
+
263
+ self.device_window_act = QAction('Select audio output &device...')
264
+ self.device_window_act.setIcon(QIcon.fromTheme(gcom.ICON_AUDIO_DEVICE))
265
+ self.device_window_act.setStatusTip('Select audio output device')
266
+ self.device_window_act.triggered.connect(self._device_window_show)
267
+
268
+ self.show_log_plot_act = QAction('Show &dB plot')
269
+ self.show_log_plot_act.setStatusTip('Show a plot of dB vs frequency')
270
+ self.show_log_plot_act.triggered.connect(self._show_log_plot)
271
+
272
+ self.show_linear_plot_act = QAction('Show &power plot')
273
+ self.show_linear_plot_act.setStatusTip(
274
+ 'Show a plot of power vs frequency')
275
+ self.show_linear_plot_act.triggered.connect(self._show_linear_plot)
276
+
277
+ self.debug_drag_act = QAction('Debug dragging')
278
+ self.debug_drag_act.setCheckable(True)
279
+ self.debug_drag_act.setChecked(False)
280
+
281
+ self.play_act = QAction(self)
282
+ self.play_act.setShortcuts([QKeySequence('Ctrl+P'),
283
+ Qt.Key.Key_MediaPlay])
284
+ self.play_act.setIcon(QIcon.fromTheme(gcom.ICON_PLAY))
285
+ self.play_act.setStatusTip('Play (Ctrl+P)')
286
+ self.play_act.triggered.connect(self._play)
287
+
288
+ self.pause_act = QAction(self)
289
+ self.pause_act.setShortcuts(['Space',
290
+ Qt.Key.Key_MediaPause])
291
+ self.pause_act.setIcon(QIcon.fromTheme(gcom.ICON_PAUSE))
292
+ self.pause_act.setStatusTip('Pause (Spacebar)')
293
+ self.pause_act.triggered.connect(self._pause)
294
+
295
+ self.stop_act = QAction(self)
296
+ self.stop_act.setShortcuts([QKeySequence('Ctrl+S'),
297
+ Qt.Key.Key_MediaStop])
298
+ self.stop_act.setIcon(QIcon.fromTheme(gcom.ICON_STOP))
299
+ self.stop_act.setStatusTip('Stop (Ctrl+S)')
300
+ self.stop_act.triggered.connect(self._stop)
301
+
302
+ if DEBUG_BUTTON:
303
+ self.debug_act = QAction(self)
304
+ #self.debug_act.setShortcut(QKeySequence('Ctrl+P'))
305
+ self.debug_act.setIcon(QIcon.fromTheme(gcom.ICON_ABOUT))
306
+ self.debug_act.setStatusTip('Print stuffs')
307
+ self.debug_act.triggered.connect(self._debug)
308
+
309
+ self.player_back_act = QAction(self)
310
+ self.player_back_act.setShortcut('Left')
311
+ self.player_back_act.setIcon(QIcon.fromTheme(gcom.ICON_PLAYER_BACK))
312
+ self.player_back_act.setStatusTip('Rewind 10 seconds (Left arrow)')
313
+
314
+ self.player_forward_act = QAction(self)
315
+ self.player_forward_act.setShortcut('Right')
316
+ self.player_forward_act.setIcon(QIcon.fromTheme(
317
+ gcom.ICON_PLAYER_FORWARD))
318
+ self.player_forward_act.setStatusTip('Jump ahead 10 seconds'
319
+ ' (Right arrow)')
320
+
321
+
322
+ self._initUI()
323
+
324
+ self._nothing_selected()
325
+
326
+ self.setAcceptDrops(True)
327
+
328
+ for arg in args.file:
329
+ self.file_selector.handle_command_line_arg(arg)
330
+
331
+
332
+ def dragEnterEvent(self, event):
333
+ if self.debug_drag_act.isChecked():
334
+ s = (f'\ndragEnterEvent\n'
335
+ f'source: {event.source()}\n'
336
+ f'hasUrls: {event.mimeData().hasUrls()}\n'
337
+ f'possibleActions: {event.possibleActions()}\n'
338
+ f'proposedAction: {event.proposedAction()}')
339
+ self._log(s, lv.LOG_LEVEL_NORMAL)
340
+ possible = event.possibleActions()
341
+ acceptable = Qt.DropAction.CopyAction | Qt.DropAction.LinkAction
342
+ if (event.source() is None
343
+ and possible & acceptable
344
+ and event.mimeData().hasUrls()):
345
+ if event.proposedAction() & acceptable:
346
+ event.acceptProposedAction()
347
+ elif Qt.DropAction.LinkAction & possible:
348
+ event.setDropAction(Qt.DropAction.LinkAction)
349
+ event.accept()
350
+ elif Qt.DropAction.CopyAction & possible:
351
+ event.setDropAction(Qt.DropAction.CopyAction)
352
+ event.accept()
353
+
354
+
355
+ def dropEvent(self, event):
356
+ possible = event.possibleActions()
357
+ acceptable = Qt.DropAction.CopyAction | Qt.DropAction.LinkAction
358
+ if event.proposedAction() & acceptable:
359
+ event.acceptProposedAction()
360
+ elif Qt.DropAction.LinkAction & possible:
361
+ event.setDropAction(Qt.DropAction.LinkAction)
362
+ event.accept()
363
+ elif Qt.DropAction.CopyAction & possible:
364
+ event.setDropAction(Qt.DropAction.CopyAction)
365
+ event.accept()
366
+ for url in event.mimeData().urls():
367
+ path = url.toLocalFile()
368
+ if not path:
369
+ path = url.toString()
370
+ self.file_selector.handle_drop(path)
371
+
372
+
373
+ def closeEvent(self, event):
374
+ reply = QMessageBox.question(
375
+ self,
376
+ 'Confirm',
377
+ QUIT_CONFIRMATION_MESSAGE,
378
+ QMessageBox.StandardButton.Yes
379
+ | QMessageBox.StandardButton.No,
380
+ QMessageBox.StandardButton.No)
381
+
382
+ if reply == QMessageBox.StandardButton.Yes:
383
+ self.analyzed_audio.close()
384
+ self.export_window.quit_thread()
385
+ event.accept()
386
+ else:
387
+ event.ignore()
388
+
389
+ def _init_menubar(self):
390
+ menubar = self.menuBar()
391
+
392
+ file_menu = menubar.addMenu('&File')
393
+ file_menu.addAction(self.export_act)
394
+ file_menu.addSeparator()
395
+ file_menu.addAction(self.exit_act)
396
+
397
+ view_menu = menubar.addMenu('&View')
398
+ view_menu.addAction(self.show_log_plot_act)
399
+ view_menu.addAction(self.show_linear_plot_act)
400
+ view_menu.addSeparator()
401
+ view_menu.addAction(self.log_viewer_act.menu())
402
+
403
+ option_menu = menubar.addMenu('&Options')
404
+ option_menu.addAction(self.showhidden_act)
405
+ option_menu.addAction(self.play_start_end_act)
406
+ option_menu.addAction(self.toggle_options_panel_act)
407
+ option_menu.addSeparator()
408
+ option_menu.addAction(self.device_window_act)
409
+ option_menu.addSeparator()
410
+ option_menu.addAction(self.debug_drag_act)
411
+
412
+ go_menu = menubar.addMenu('&Go')
413
+ go_menu.addAction(self.file_selector.up_act.menu())
414
+ go_menu.addAction(self.file_selector.back_act.menu())
415
+ go_menu.addAction(self.file_selector.forward_act.menu())
416
+ go_menu.addAction(self.file_selector.home_act.menu())
417
+
418
+ help_menu = menubar.addMenu('&Help')
419
+ help_menu.addAction(self.about_act)
420
+
421
+ self.menubar = menubar
422
+
423
+ def _init_statusbar(self):
424
+ self.prog_bar = QProgressBar()
425
+ self.prog_bar.setRange(0, 100)
426
+ sp = QSizePolicy()
427
+ self.prog_bar.setSizePolicy(sp)
428
+ self.statusBar().addPermanentWidget(self.prog_bar, 2)
429
+ self.cancel_button = QToolButton()
430
+ self.cancel_button.setDefaultAction(self.cancel_act)
431
+ self.statusBar().addPermanentWidget(self.cancel_button)
432
+ self.prog_bar.hide()
433
+ self.cancel_button.hide()
434
+ self.cancel_act.setEnabled(False)
435
+ self.statusBar().showMessage(INIT_STATUS_MESSAGE)
436
+
437
+ def _update_progbar(self, progress):
438
+ if progress >= 0:
439
+ self.prog_bar.setValue(int(progress * 100))
440
+ else:
441
+ self.prog_bar.reset()
442
+
443
+ def _update_statusbar(self, status):
444
+ self.statusBar().showMessage(status)
445
+
446
+ def _init_toolbar(self):
447
+ self.toolbar = self.addToolBar('Toolbar')
448
+
449
+ self.toolbar.addAction(self.toggle_toggle_options_panel_act)
450
+ self.toolbar.addAction(self.log_viewer_act.button())
451
+
452
+ self.toolbar.addSeparator()
453
+
454
+ self.toolbar.addAction(self.play_act)
455
+ self.pause_button = QToolButton()
456
+ self.pause_button.setDefaultAction(self.pause_act)
457
+ self.toolbar.addWidget(self.pause_button)
458
+ self.toolbar.addAction(self.stop_act)
459
+ self.toolbar.addAction(self.player_back_act)
460
+ self.toolbar.addAction(self.player_forward_act)
461
+ if DEBUG_BUTTON:
462
+ self.toolbar.addAction(self.debug_act)
463
+
464
+ lcd_style = ('QLCDNumber {'
465
+ f' background-color: {disp.DISPLAY_BG_COLOR.name()};'
466
+ f' color: {disp.DISPLAY_DATA_COLOR.name()};'
467
+ ' }')
468
+ self.lcd1 = QLCDNumber()
469
+ self.lcd1.setDigitCount(5)
470
+ self.lcd1.setStyleSheet(lcd_style)
471
+ self.toolbar.addWidget(self.lcd1)
472
+ self.lcd1.setSegmentStyle(QLCDNumber.SegmentStyle.Flat)
473
+ self.lcd1.display('--:--')
474
+
475
+ self.slider = _Slider(Qt.Orientation.Horizontal)
476
+ self.slider.setRange(0, 100)
477
+ self.slider.setMinimumWidth(100)
478
+ self.slider.setTickPosition(QSlider.TickPosition.TicksBothSides)
479
+ self.toolbar.addWidget(self.slider)
480
+
481
+ self.lcd2 = QLCDNumber()
482
+ self.lcd2.setDigitCount(6)
483
+ self.toolbar.addWidget(self.lcd2)
484
+ self.lcd2.setStyleSheet(lcd_style)
485
+ self.lcd2.setSegmentStyle(QLCDNumber.SegmentStyle.Flat)
486
+ self.lcd2.display('---:--')
487
+
488
+ self.now_playing = QLabel()
489
+ self.now_playing.setTextFormat(Qt.TextFormat.PlainText)
490
+ self.now_playing.setMinimumWidth(30)
491
+ self.now_playing.setSizePolicy(QSizePolicy.Policy.Minimum,
492
+ QSizePolicy.Policy.Preferred)
493
+ self.toolbar.addWidget(self.now_playing)
494
+
495
+ spacer = QWidget()
496
+ spacer.setSizePolicy(QSizePolicy.Policy.Expanding,
497
+ QSizePolicy.Policy.Preferred)
498
+ self.toolbar.addWidget(spacer)
499
+ self.toolbar.addAction(self.exit_act)
500
+
501
+ def _toggle_toggle_options_panel(self):
502
+ if self.toggle_options_panel_act.isChecked():
503
+ self.toggle_options_panel_act.setChecked(False)
504
+ self._toggle_options_panel(False)
505
+ else:
506
+ self.toggle_options_panel_act.setChecked(True)
507
+ self._toggle_options_panel(True)
508
+
509
+ def _show_options_panel(self):
510
+ if not self.toggle_options_panel_act.isChecked():
511
+ self.toggle_options_panel_act.setChecked(True)
512
+ self._toggle_options_panel(True)
513
+
514
+ def _toggle_options_panel(self, checked):
515
+ if checked:
516
+ self.stack.setCurrentWidget(self.option_panel)
517
+ self.toggle_toggle_options_panel_act.setIcon(
518
+ QIcon(gcom.ICON_FILES))
519
+ else:
520
+ self.stack.setCurrentWidget(self.file_selector)
521
+ self.toggle_toggle_options_panel_act.setIcon(
522
+ QIcon(gcom.ICON_OPTIONS))
523
+
524
+ def _starting_processing(self):
525
+ self.cancel_act.setEnabled(True)
526
+ self.prog_bar.show()
527
+ self.cancel_button.show()
528
+
529
+ def _finished_processing(self):
530
+ self._pitch_change(self.option_panel.widgets[gcom.OPTION_PITCH].get())
531
+ self.prog_bar.hide()
532
+ self.cancel_button.hide()
533
+ self.cancel_act.setEnabled(False)
534
+
535
+ def _export(self):
536
+ self.export_window.show(self.analyzed_audio.current_selection,
537
+ self.analyzed_audio.current_listed_title,
538
+ self.analyzed_audio.current_options,
539
+ self.analyzed_audio.current_metadata)
540
+
541
+ def _starting_exporting(self):
542
+ self._exporting_in_progress = True
543
+ self.export_act.setEnabled(False)
544
+
545
+ def _finished_exporting(self):
546
+ self._exporting_in_progress = False
547
+ if com.mpv_error is not None:
548
+ self.export_act.setEnabled(True)
549
+
550
+ def _about_show(self):
551
+ self.about_window.show()
552
+
553
+ def _log_show(self):
554
+ self.log_viewer.show()
555
+ self.log_viewer_act.setIcon(QIcon.fromTheme(gcom.ICON_MESSAGE_LOG))
556
+
557
+ def _device_window_show(self):
558
+ try:
559
+ self.player.show_device_window()
560
+ except OSError:
561
+ self.device_window_act.setEnabled(False)
562
+
563
+ def _show_log_plot(self):
564
+ self.analyzed_audio.show_plot('log')
565
+
566
+ def _show_linear_plot(self):
567
+ self.analyzed_audio.show_plot('linear')
568
+
569
+ def _log(self, message, level):
570
+ if level == lv.LOG_LEVEL_ERROR and not self.log_viewer.isVisible():
571
+ self.log_viewer_act.setIcon(QIcon.fromTheme(gcom.ICON_ALERT))
572
+ self.log_viewer.add_message(message + '\n', level)
573
+
574
+ def _handle_option_error(self, option):
575
+ self.option_panel.widgets[option].set_error()
576
+ self.option_panel.ensure_visible()
577
+
578
+ def _something_selected(self):
579
+ self._pitch_change(self.option_panel.widgets[gcom.OPTION_PITCH].get())
580
+ self.something_selected=True
581
+ self.option_panel.set_apply_enabled(True)
582
+ self.show_log_plot_act.setEnabled(True)
583
+ self.show_linear_plot_act.setEnabled(True)
584
+ if not self._exporting_in_progress and com.mpv_error is None:
585
+ self.export_act.setEnabled(True)
586
+
587
+ def _nothing_selected(self):
588
+ self.something_selected=False
589
+ self.option_panel.set_apply_enabled(False)
590
+ self.show_log_plot_act.setEnabled(False)
591
+ self.show_linear_plot_act.setEnabled(False)
592
+ self.export_act.setEnabled(False)
593
+
594
+ def _play_start_end_toggled(self, checked):
595
+ if checked:
596
+ start = self._currently_playing_start
597
+ end = self._currently_playing_end
598
+ else:
599
+ start = None
600
+ end = None
601
+ self.player.update_corrections(None, None, start, end)
602
+
603
+ def _play(self):
604
+ path = self.analyzed_audio.current_selection
605
+ if path:
606
+ if self.player.get_currently_playing() != path:
607
+ options = self.analyzed_audio.current_options
608
+ self.duration = self.analyzed_audio.current_duration
609
+ self._currently_playing_start = options[gcom.OPTION_START]
610
+ self._currently_playing_end = options[gcom.OPTION_END]
611
+ if self.play_start_end_act.isChecked():
612
+ start = options[gcom.OPTION_START]
613
+ end = options[gcom.OPTION_END]
614
+ else:
615
+ start = None
616
+ end = None
617
+ try:
618
+ self.player.play(path,
619
+ self.duration,
620
+ options[gcom.OPTION_PITCH],
621
+ options[gcom.OPTION_TEMPO],
622
+ start,
623
+ end,
624
+ )
625
+ except OSError:
626
+ self.play_act.setEnabled(False)
627
+ return
628
+ self.now_playing.setText(self.analyzed_audio.current_title)
629
+ if self.duration is not None:
630
+ self.slider.setTickInterval(
631
+ 6000//max(int(self.duration), 1))
632
+ elif self.slider.value() >= 99:
633
+ self.player.set_percent(0)
634
+ self.option_panel.start_end_enable(True)
635
+ self.player.set_pause(False)
636
+
637
+ def _pause(self):
638
+ self.player.toggle_pause()
639
+
640
+ def _update_pause(self, pause):
641
+ self.pause_button.setDown(pause)
642
+
643
+ def _stop(self):
644
+ self.player.stop()
645
+ self.now_playing.clear()
646
+ self.option_panel.start_end_enable(False)
647
+
648
+ def _back(self):
649
+ self.player.back()
650
+
651
+ def _forward(self):
652
+ self.player.forward()
653
+
654
+ def _debug(self):
655
+ pass
656
+
657
+ def _update_time_pos(self, is_valid, time_pos):
658
+ if is_valid:
659
+ t = min(time_pos, 5999)
660
+ display = f'{t//60:0>2}:{t%60:0>2}'
661
+ else:
662
+ display = '--:--'
663
+ self.lcd1.display(display)
664
+
665
+ def _enable_slider_update(self):
666
+ self._slider_update_enabled = True
667
+
668
+ def _disable_slider_update(self):
669
+ self._slider_update_enabled = False
670
+
671
+ def _update_percent(self, percent):
672
+ if self._slider_update_enabled and not self.slider.isSliderDown():
673
+ self.slider.setValue(percent)
674
+ if self.duration is None:
675
+ self.duration = self.player.get_duration()
676
+ if self.duration is not None:
677
+ self.slider.setTickInterval(6000//max(int(self.duration), 1))
678
+
679
+ def _set_percent(self):
680
+ self.player.set_percent(self.slider.value())
681
+ self._enable_slider_update()
682
+
683
+ def _update_time_rem(self, is_valid, time_rem):
684
+ if is_valid:
685
+ t = min(time_rem, 5999)
686
+ display = f'-{t//60:0>2}:{t%60:0>2}'
687
+ else:
688
+ display = '---:--'
689
+ self.lcd2.display(display)
690
+
691
+ def _update_now_playing(self, title, result_rows=None):
692
+ path = self.analyzed_audio.current_selection
693
+ if path and title != ' ':
694
+ if self.player.get_currently_playing() == path:
695
+ self.now_playing.setText(title)
696
+
697
+ def _update_corrections(self, options, reread_requested):
698
+ if (self.analyzed_audio.current_selection
699
+ == self.player.get_currently_playing()):
700
+ self._currently_playing_start = options[gcom.OPTION_START]
701
+ self._currently_playing_end = options[gcom.OPTION_END]
702
+ if self.play_start_end_act.isChecked():
703
+ start = options[gcom.OPTION_START]
704
+ end = options[gcom.OPTION_END]
705
+ else:
706
+ start = None
707
+ end = None
708
+ self.player.update_corrections(options[gcom.OPTION_PITCH],
709
+ options[gcom.OPTION_TEMPO],
710
+ start,
711
+ end,
712
+ )
713
+
714
+ def _pitch_change(self, factor):
715
+ if self.something_selected:
716
+ options = self.analyzed_audio.current_options
717
+ relative_change = factor / options[gcom.OPTION_PITCH]
718
+ cents = com.ratio_to_cents(relative_change)
719
+ self.display.update_ghost_offset(cents)
720
+
721
+ def _set_start_now(self):
722
+ now = self.player.get_current_position()
723
+ self.option_panel.set_start(now)
724
+
725
+ def _set_end_now(self):
726
+ now = self.player.get_current_position()
727
+ self.option_panel.set_end(now)
728
+
729
+ def _initUI(self):
730
+ self.resize(800, 850)
731
+ self.setWindowTitle(APP_TITLE)
732
+ self.setWindowIcon(QIcon(gcom.APP_ICON))
733
+
734
+ option_panel = op.OptionPanel(self.args)
735
+ self.option_panel = option_panel
736
+ file_selector = fs.FileSelector(option_panel)
737
+ self.file_selector = file_selector
738
+ analyzed_audio = ga.AnalyzedAudio()
739
+ self.analyzed_audio = analyzed_audio
740
+ display = disp.Display()
741
+ self.display = display
742
+
743
+ self.player = pl.Player(self)
744
+ if com.mpv_error is not None:
745
+ self.play_act.setEnabled(False)
746
+ self.stop_act.setEnabled(False)
747
+ self.pause_act.setEnabled(False)
748
+ self.player_back_act.setEnabled(False)
749
+ self.player_forward_act.setEnabled(False)
750
+ self.device_window_act.setEnabled(False)
751
+
752
+ option_panel.set_default_options()
753
+
754
+ self.cancel_act.triggered.connect(analyzed_audio.cancel)
755
+
756
+ stack = QStackedWidget()
757
+ self.stack = stack
758
+ stack.addWidget(file_selector)
759
+ stack.addWidget(option_panel)
760
+
761
+ hsplitter = QSplitter(Qt.Orientation.Horizontal)
762
+ hsplitter.addWidget(analyzed_audio)
763
+ hsplitter.insertWidget(0, stack)
764
+
765
+ vsplitter = QSplitter(Qt.Orientation.Vertical)
766
+ vsplitter.addWidget(display)
767
+ vsplitter.addWidget(hsplitter)
768
+
769
+ self.setCentralWidget(vsplitter)
770
+
771
+ self._init_menubar()
772
+ self._init_statusbar()
773
+ self._init_toolbar()
774
+
775
+ self.export_window = ex.ExportWindow(self)
776
+ self.export_window.set_dir(self.file_selector.model.rootDirectory())
777
+
778
+ file_selector.SelectForAnalysis.connect(analyzed_audio.add_audio)
779
+ self.showhidden_act.toggled.connect(file_selector._set_show_hidden)
780
+ file_selector.UpdateStatusbar.connect(self._update_statusbar)
781
+ file_selector.model.rootPathChanged.connect(self.export_window.set_dir)
782
+ self.analyzed_audio.UpdateStatusbar.connect(self._update_statusbar)
783
+ self.analyzed_audio.UpdateProgbar.connect(self._update_progbar)
784
+ self.analyzed_audio.Starting.connect(self._starting_processing)
785
+ self.analyzed_audio.Finished.connect(self._finished_processing)
786
+ self.analyzed_audio.AddToLog.connect(self._log)
787
+ self.analyzed_audio.OptionError.connect(self._handle_option_error)
788
+ self.analyzed_audio.SomethingSelected.connect(self._something_selected)
789
+ self.analyzed_audio.NothingSelected.connect(self._nothing_selected)
790
+ self.option_panel.PushOptions.connect(analyzed_audio.change_options)
791
+ self.option_panel.PushOptions.connect(self._update_corrections)
792
+ self.option_panel.PitchChange.connect(self._pitch_change)
793
+ self.option_panel.widgets[gcom.OPTION_START].button_clicked.connect(
794
+ self._set_start_now)
795
+ self.option_panel.widgets[gcom.OPTION_END].button_clicked.connect(
796
+ self._set_end_now)
797
+ self.option_panel.PayAttentionToMe.connect(self._show_options_panel)
798
+ self.player.TickPos.connect(self._update_time_pos)
799
+ self.player.TickRem.connect(self._update_time_rem)
800
+ self.player.Percent.connect(self._update_percent)
801
+ self.player.PauseStatus.connect(self._update_pause)
802
+ self.player.AddToLog.connect(self._log)
803
+ self.slider.MousePressed.connect(self._disable_slider_update)
804
+ self.slider.MouseReleased.connect(self._set_percent)
805
+ self.player_back_act.triggered.connect(self._back)
806
+ self.player_forward_act.triggered.connect(self._forward)
807
+ self.export_window.AddToLog.connect(self._log)
808
+ self.export_window.Starting.connect(self._starting_exporting)
809
+ self.export_window.Finished.connect(self._finished_exporting)
810
+
811
+ analyzed_audio.DisplayResult.connect(display.update_data,
812
+ Qt.ConnectionType.QueuedConnection)
813
+ analyzed_audio.DisplayResult.connect(self._update_now_playing)
814
+ analyzed_audio.PushOptions.connect(option_panel.set_options)
815
+
816
+ self.about_window = _AboutWindow(self)
817
+
818
+ self.log_viewer = lv.LogViewer(self)
819
+
820
+ self.show()
821
+
822
+ hsplitter.moveSplitter(500, 1)
823
+ vsplitter.moveSplitter(342, 1)
824
+
825
+ while self.message_queue:
826
+ message = self.message_queue.popleft()
827
+ self._log(message.msg, message.level)
828
+
829
+
830
+ class _Message():
831
+ def __init__(self, msg, level):
832
+ self.msg = msg
833
+ self.level = level
834
+
835
+
836
+ class _MessageQueue(deque):
837
+ def add_msg(self, msg, level=eh.NORMAL):
838
+ self.append(_Message(msg, level))
839
+
840
+
841
+ def main():
842
+ parser = ap.get_arg_parser(version=VERSION_STRING,
843
+ description=DESCRIPTION,
844
+ gui_mode=True)
845
+ cli_args, unparsed = parser.parse_known_args_gm()
846
+ message_queue = _MessageQueue()
847
+ args = ap.merge_args(cli_args, print_msg=message_queue.add_msg)
848
+ if not ap.validate(args):
849
+ return 2
850
+
851
+ app = QApplication(sys.argv[:1] + unparsed)
852
+
853
+ # Needed to make libmpv work
854
+ if os.name != 'nt':
855
+ import locale
856
+ locale.setlocale(locale.LC_NUMERIC, 'C')
857
+
858
+ ui = _MainUI(args, message_queue)
859
+
860
+ return app.exec()
861
+
862
+ if __name__ == '__main__':
863
+ sys.exit(main())