audio-tuner-gui 0.10.0__tar.gz → 0.11.1__tar.gz

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.
Files changed (48) hide show
  1. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/PKG-INFO +4 -3
  2. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/README.rst +1 -1
  3. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/__init__.py +1 -1
  4. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/analysis.py +107 -81
  5. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/common.py +77 -25
  6. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/file_selector.py +71 -21
  7. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/README +4 -0
  8. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/application-exit.svg +457 -0
  9. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/audio-card.svg +434 -0
  10. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/audio-x-generic.svg +180 -0
  11. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/dialog-warning.svg +373 -0
  12. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/document-save.svg +619 -0
  13. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/edit-delete.svg +896 -0
  14. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/folder.svg +424 -0
  15. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/format-justify-left.svg +271 -0
  16. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/go-home.svg +445 -0
  17. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/go-next.svg +192 -0
  18. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/go-previous.svg +854 -0
  19. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/go-up.svg +196 -0
  20. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/media-playback-pause.svg +641 -0
  21. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/media-playback-start.svg +319 -0
  22. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/media-playback-stop.svg +651 -0
  23. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/media-seek-backward.svg +374 -0
  24. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/media-seek-forward.svg +383 -0
  25. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/preferences-system.svg +398 -0
  26. audio_tuner_gui-0.11.1/audio_tuner_gui/icons/fallback/process-stop.svg +336 -0
  27. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/option_panel.py +49 -21
  28. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/scripts/tuner_gui.py +137 -21
  29. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/pyproject.toml +2 -1
  30. audio_tuner_gui-0.10.0/audio_tuner_gui/icons/folder.png +0 -0
  31. audio_tuner_gui-0.10.0/audio_tuner_gui/icons/preferences-other.png +0 -0
  32. audio_tuner_gui-0.10.0/audio_tuner_gui/icons/process-stop.png +0 -0
  33. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/.gitignore +0 -0
  34. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/COPYING +0 -0
  35. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/display.py +0 -0
  36. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/export.py +0 -0
  37. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/audio_tuner_icon.ico +0 -0
  38. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/audio_tuner_icon.svg +0 -0
  39. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/audio_tuner_icon_hires.svg +0 -0
  40. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/audio_tuner_logo_dark.svg +0 -0
  41. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/audio_tuner_logo_light.svg +0 -0
  42. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/black-logo.svg +0 -0
  43. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/gpl-v3-logo_red.svg +0 -0
  44. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/gpl-v3-logo_white.svg +0 -0
  45. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/icons/white-logo.svg +0 -0
  46. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/log_viewer.py +0 -0
  47. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/player.py +0 -0
  48. {audio_tuner_gui-0.10.0 → audio_tuner_gui-0.11.1}/audio_tuner_gui/scripts/__init__.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audio-tuner-gui
3
- Version: 0.10.0
3
+ Version: 0.11.1
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/en/latest/
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
@@ -61,7 +62,7 @@ for details.
61
62
  Audio Tuner can be configured to use **ffmpeg** and **ffprobe** instead of
62
63
  libmpv2, at the cost of reduced functionality in the GUI.
63
64
 
64
- .. _documentation: https://audio-tuner.readthedocs.io/en/latest/
65
+ .. _documentation: https://audio-tuner.readthedocs.io/
65
66
  .. _audio-tuner: https://pypi.org/project/audio-tuner/
66
67
  .. _PyQt6: https://www.riverbankcomputing.com/software/pyqt/
67
68
 
@@ -41,7 +41,7 @@ for details.
41
41
  Audio Tuner can be configured to use **ffmpeg** and **ffprobe** instead of
42
42
  libmpv2, at the cost of reduced functionality in the GUI.
43
43
 
44
- .. _documentation: https://audio-tuner.readthedocs.io/en/latest/
44
+ .. _documentation: https://audio-tuner.readthedocs.io/
45
45
  .. _audio-tuner: https://pypi.org/project/audio-tuner/
46
46
  .. _PyQt6: https://www.riverbankcomputing.com/software/pyqt/
47
47
 
@@ -32,7 +32,7 @@ __all__ = [
32
32
  # Release candidates: X.Y.ZrcN
33
33
  # Releases: X.Y.Z
34
34
 
35
- VERSION = '0.10.0'
35
+ VERSION = '0.11.1'
36
36
 
37
37
  from os.path import dirname
38
38
 
@@ -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(anal.Analysis)
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
- audio_tuner.analysis.Analysis
116
- The analysis object that has finished with it's analysis.
142
+ _Result
143
+ The result of the analysis.
117
144
  """
118
145
 
119
- ProcessingError = pyqtSignal(anal.Analysis)
120
- """Signal emitted when the analysis object raises an exception
121
- during analysis.
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
- audio_tuner.analysis.Analysis
126
- The analysis object.
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
- analysis: anal.Analysis,
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
- audio_tuner.analysis.Analysis, and then spits it out again via
176
- the ResultReady signal when it's done (or via 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
- analysis : audio_tuner.analysis.Analysis
188
- The Analysis instance.
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
- analysis.try_sequence = options[gcom.OPTION_BACKENDS]
199
- analysis.print_msg = self._print_msg
215
+ result.try_sequence = options[gcom.OPTION_BACKENDS]
216
+ result.print_msg = self._print_msg
200
217
  if (reread_requested
201
- and (analysis.pitch != options[gcom.OPTION_PITCH]
202
- or analysis.tempo != options[gcom.OPTION_TEMPO])):
203
- analysis.pitch = options[gcom.OPTION_PITCH]
204
- analysis.tempo = options[gcom.OPTION_TEMPO]
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(getattr(analysis, 'options', None))
208
- basename = os.path.basename(analysis.inputfile)
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
- if options[gcom.OPTION_PAD]:
216
- pad_amounts = (size * 2, size * 2)
217
- else:
218
- pad_amounts = None
219
- analysis.load_data(start=options[gcom.OPTION_START],
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(analysis)
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
- analysis.fft(size=size, progress_hook=self._progress_check)
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(analysis)
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
- analysis.find_peaks(
247
- low_cut=options[gcom.OPTION_LOW_CUT] * analysis.pitch,
248
- high_cut=options[gcom.OPTION_HIGH_CUT] * analysis.pitch,
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 = analysis.peaks
266
+ peaks = result.peaks
253
267
  tuning_system = options.tuning_system
254
268
  result_rows = []
255
- pitch_offset = options[gcom.OPTION_PITCH] / analysis.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
- analysis.result_rows = result_rows
285
+ result.result_rows = result_rows
272
286
 
273
- analysis.options = options
287
+ result.options = options
274
288
 
275
- self.ResultReady.emit(analysis)
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(anal.Analysis, gcom.Options, bool)
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
- audio_tuner.analysis.Analysis
335
- The analysis object to use.
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._analysis = {}
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._analysis[item2.data()].options
514
- self.current_duration = self._analysis[item2.data()].file_duration
515
- self.current_metadata = self._analysis[item2.data()].file_metadata
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._analysis[item2.data()].result_rows)
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._analysis[data].file_title
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._analysis[item2.data()].result_rows)
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._analysis:
564
- index = self._analysis[canonical_path].index
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
- analysis = anal.Analysis(canonical_path)
578
- self.AnalyzeAudio.emit(analysis, options, False)
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
- analysis = self._analysis[canonical_path]
606
- self.AnalyzeAudio.emit(analysis, new_options, reread_requested)
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, analysis):
610
- canonical_path = analysis.inputfile
611
- file_title = analysis.file_title
612
- if canonical_path in self._analysis:
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._analysis[canonical_path].result_rows)
641
+ self._results[canonical_path].result_rows)
616
642
  else:
617
- file_track = analysis.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
- analysis.index = index
634
- self._analysis[canonical_path] = analysis
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._analysis[canonical_path].options[gcom.OPTION_PITCH]
684
+ pitch = self._results[canonical_path].options[gcom.OPTION_PITCH]
659
685
  try:
660
- self._analysis[canonical_path].show_plot(plot_type=plot_type,
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, analysis):
673
- del self._in_progress[analysis.inputfile]
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._analysis[path]
721
+ del self._results[path]
696
722
  self._model.removeRows(row, 1)
697
- if len(self._analysis) == 0:
723
+ if len(self._results) == 0:
698
724
  self.DisplayResult.emit(' ', [])
699
725
  self.NothingSelected.emit()
700
726
  self.current_title = None
@@ -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'
@@ -111,11 +112,8 @@ REDO_LEVEL_NONE = 0
111
112
  APP_ICON = os.path.join(PKGDIR, 'icons/audio_tuner_icon.svg')
112
113
 
113
114
  # 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')
115
+ ICON_OPTIONS = os.path.join(PKGDIR, 'icons/fallback/preferences-system.svg')
116
+ ICON_FILES = os.path.join(PKGDIR, 'icons/fallback/folder.svg')
119
117
 
120
118
  LOGO_LIGHT = os.path.join(PKGDIR, 'icons/audio_tuner_logo_light.svg')
121
119
  LOGO_DARK = os.path.join(PKGDIR, 'icons/audio_tuner_logo_dark.svg')
@@ -133,7 +131,7 @@ try:
133
131
  ICON_CLEAR = QIcon.ThemeIcon.EditDelete
134
132
  ICON_EXIT = QIcon.ThemeIcon.ApplicationExit
135
133
  # ICON_OPTIONS = 'preferences-other'
136
- # ICON_CANCEL = QIcon.ThemeIcon.ProcessStop
134
+ ICON_CANCEL = QIcon.ThemeIcon.ProcessStop
137
135
  ICON_ABOUT = QIcon.ThemeIcon.HelpAbout
138
136
  ICON_MESSAGE_LOG = QIcon.ThemeIcon.FormatJustifyLeft
139
137
  # ICON_FILES = 'folder'
@@ -145,6 +143,8 @@ try:
145
143
  ICON_PLAYER_BACK = QIcon.ThemeIcon.MediaSeekBackward
146
144
  ICON_PLAYER_FORWARD = QIcon.ThemeIcon.MediaSeekForward
147
145
  ICON_AUDIO_DEVICE = QIcon.ThemeIcon.AudioCard
146
+ ICON_SAVE = QIcon.ThemeIcon.DocumentSave
147
+ ICON_DEL = QIcon.ThemeIcon.EditDelete
148
148
  except AttributeError:
149
149
  ICON_BACK = 'go-previous'
150
150
  ICON_FORWARD = 'go-next'
@@ -154,7 +154,7 @@ except AttributeError:
154
154
  ICON_CLEAR = 'edit-delete'
155
155
  ICON_EXIT = 'application-exit'
156
156
  # ICON_OPTIONS = 'preferences-other'
157
- # ICON_CANCEL = 'process-stop'
157
+ ICON_CANCEL = 'process-stop'
158
158
  ICON_ABOUT = 'help-about'
159
159
  ICON_MESSAGE_LOG = 'format-justify-left'
160
160
  # ICON_FILES = 'folder'
@@ -166,6 +166,8 @@ except AttributeError:
166
166
  ICON_PLAYER_BACK = 'media-seek-backward'
167
167
  ICON_PLAYER_FORWARD = 'media-seek-forward'
168
168
  ICON_AUDIO_DEVICE = 'audio-card'
169
+ ICON_SAVE = 'document-save'
170
+ ICON_DEL = 'edit-delete'
169
171
 
170
172
 
171
173
  class RowData(TypedDict):
@@ -275,25 +277,76 @@ class Options(dict):
275
277
 
276
278
  def __init__(self, args):
277
279
  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
280
+
281
+ self._options_and_args = (
282
+ (OPTION_TUNING_SYSTEM, 'tuning'),
283
+ (OPTION_REF_FREQ, 'ref_freq'),
284
+ (OPTION_REF_NOTE, 'ref_note'),
285
+ (OPTION_START, 'start'),
286
+ (OPTION_END, 'end'),
287
+ (OPTION_LOW_CUT, 'low_cut'),
288
+ (OPTION_HIGH_CUT, 'high_cut'),
289
+ (OPTION_DB_RANGE, 'dB_range'),
290
+ (OPTION_MAX_PEAKS, 'max_peaks'),
291
+ (OPTION_SIZE_EXP, 'size_exp'),
292
+ (OPTION_SAMPLERATE, 'samplerate'),
293
+ (OPTION_BACKENDS, 'backends'),
294
+ (OPTION_PITCH, 'pitch'),
295
+ (OPTION_TEMPO, 'tempo'),
296
+ )
297
+
298
+ for opt, arg in self._options_and_args:
299
+ if arg == 'tuning':
300
+ val = getattr(args, arg)
301
+ self[opt] = self._guify_tuning_system(val)
302
+ else:
303
+ self[opt] = getattr(args, arg)
293
304
 
294
305
  def _guify_tuning_system(self, tuning_system):
295
306
  return tuning_system.replace('_', ' ').title()
296
307
 
308
+ def _unguify_tuning_system(self, tuning_system):
309
+ return tuning_system.replace(' ', '_').lower()
310
+
311
+ def argparse_namespace(self) -> Namespace:
312
+ """Return an argparse.Namespace object containing the options.
313
+
314
+ Returns
315
+ -------
316
+ argparse.Namespace
317
+ The options.
318
+ """
319
+
320
+ args = Namespace()
321
+
322
+ for opt, arg in self._options_and_args:
323
+ if arg == 'tuning':
324
+ val = self[opt]
325
+ setattr(args, arg, self._unguify_tuning_system(val))
326
+ else:
327
+ setattr(args, arg, self[opt])
328
+
329
+ return args
330
+
331
+ def merge_args(self, args: Namespace):
332
+ """Modify the options according to the arguments in an
333
+ argparse.Namespace object.
334
+
335
+ Parameters
336
+ ----------
337
+ args : argparse.Namespace
338
+ The arguments.
339
+ """
340
+
341
+ for opt, arg in self._options_and_args:
342
+ if arg == 'tuning':
343
+ if (val := getattr(args, arg, None)) is not None:
344
+ self[opt] = self._guify_tuning_system(val)
345
+ else:
346
+ if (val := getattr(args, arg, None)) is not None:
347
+ self[opt] = val
348
+
349
+
297
350
  def redo_level(self, old_options):
298
351
  """Compare the options stored in this instance of Options to the
299
352
  options stored in an older one to find out how much analysis
@@ -313,7 +366,6 @@ class Options(dict):
313
366
  if (old_options is None
314
367
  or self[OPTION_START] != old_options[OPTION_START]
315
368
  or self[OPTION_END] != old_options[OPTION_END]
316
- or self[OPTION_PAD] != old_options[OPTION_PAD]
317
369
  or self[OPTION_SIZE_EXP] != old_options[OPTION_SIZE_EXP]
318
370
  or self[OPTION_BACKENDS] != old_options[OPTION_BACKENDS]
319
371
  or self[OPTION_SAMPLERATE] != old_options[OPTION_SAMPLERATE]):