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,563 @@
|
|
|
1
|
+
# This file is part of Audio Tuner.
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025, 2026 Jessie Blue Cassell <bluesloth600@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
"""Audio export functionality for the GUI."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__author__ = 'Jessie Blue Cassell'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
'ExportWindow',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from contextlib import contextmanager
|
|
33
|
+
|
|
34
|
+
from PyQt6.QtCore import (
|
|
35
|
+
Qt,
|
|
36
|
+
pyqtSignal,
|
|
37
|
+
QDir,
|
|
38
|
+
QObject,
|
|
39
|
+
QThread,
|
|
40
|
+
)
|
|
41
|
+
from PyQt6.QtWidgets import (
|
|
42
|
+
QWidget,
|
|
43
|
+
QPushButton,
|
|
44
|
+
QVBoxLayout,
|
|
45
|
+
QHBoxLayout,
|
|
46
|
+
QLineEdit,
|
|
47
|
+
QFileDialog,
|
|
48
|
+
QComboBox,
|
|
49
|
+
QCheckBox,
|
|
50
|
+
QTableView,
|
|
51
|
+
)
|
|
52
|
+
from PyQt6.QtGui import (
|
|
53
|
+
QStandardItemModel,
|
|
54
|
+
QStandardItem,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
import audio_tuner.analysis as anal
|
|
58
|
+
import audio_tuner.common as com
|
|
59
|
+
|
|
60
|
+
import audio_tuner_gui.common as gcom
|
|
61
|
+
import audio_tuner_gui.log_viewer as lv
|
|
62
|
+
|
|
63
|
+
mpv_error = com.mpv_error
|
|
64
|
+
if mpv_error is None:
|
|
65
|
+
import mpv
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_OUTPUT_FORMATS = [
|
|
69
|
+
{'name': 'flac', 'ext': 'flac', 'desc': 'Free Lossless Audio Codec'},
|
|
70
|
+
{'name': 'ogg', 'ext': 'ogg', 'desc': 'Ogg Vorbis'},
|
|
71
|
+
{'name': 'opus', 'ext': 'opus', 'desc': 'Ogg Opus'},
|
|
72
|
+
{'name': 'spx', 'ext': 'spx', 'desc': 'Ogg Speex'},
|
|
73
|
+
{'name': 'aiff', 'ext': 'aiff', 'desc': 'Audio Interchange File Format'},
|
|
74
|
+
{'name': 'au', 'ext': 'au', 'desc': 'Sun Au'},
|
|
75
|
+
{'name': 'mp3', 'ext': 'mp3', 'desc': 'MPEG audio layer 3'},
|
|
76
|
+
{'name': 'wav', 'ext': 'wav', 'desc': 'Waveform Audio'},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
_DEFAULT_EXT = 'flac'
|
|
80
|
+
|
|
81
|
+
_ENCODER_TAGS = (
|
|
82
|
+
'encoder',
|
|
83
|
+
'encoded_by',
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class _ExportOptions():
|
|
89
|
+
input_file: str
|
|
90
|
+
output_file: str
|
|
91
|
+
output_format: str
|
|
92
|
+
tags: dict = None
|
|
93
|
+
pitch: float = 1.0
|
|
94
|
+
tempo: float = 1.0
|
|
95
|
+
start: str = None
|
|
96
|
+
end: str = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def _in_progress(start_signal, finish_signal):
|
|
101
|
+
try:
|
|
102
|
+
start_signal.emit()
|
|
103
|
+
yield None
|
|
104
|
+
finally:
|
|
105
|
+
finish_signal.emit()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _Worker(QObject):
|
|
109
|
+
AddToLog = pyqtSignal(str, int)
|
|
110
|
+
Starting = pyqtSignal()
|
|
111
|
+
Finished = pyqtSignal()
|
|
112
|
+
Success = pyqtSignal()
|
|
113
|
+
|
|
114
|
+
def _mpv_error_checker(self, evt):
|
|
115
|
+
self._mpv_err = b'unknown error'
|
|
116
|
+
evt_dict = evt.as_dict()
|
|
117
|
+
self._loaded = evt_dict['event'] == b'file-loaded'
|
|
118
|
+
if not self._loaded:
|
|
119
|
+
try:
|
|
120
|
+
self._mpv_err = evt_dict['file_error']
|
|
121
|
+
except KeyError:
|
|
122
|
+
pass
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
def _log_handler(self, level, prefix, text):
|
|
126
|
+
if level == 'warn':
|
|
127
|
+
if 'Estimating duration from bitrate' in text:
|
|
128
|
+
return
|
|
129
|
+
self.AddToLog.emit(f'libmpv: {prefix}: {text}'.rstrip('\n'),
|
|
130
|
+
lv.LOG_LEVEL_WARNING)
|
|
131
|
+
if level in ['fatal', 'error']:
|
|
132
|
+
self.AddToLog.emit(f'libmpv: {prefix}: {text}'.rstrip('\n'),
|
|
133
|
+
lv.LOG_LEVEL_ERROR)
|
|
134
|
+
|
|
135
|
+
def export(self, export_options: _ExportOptions):
|
|
136
|
+
with _in_progress(self.Starting, self.Finished):
|
|
137
|
+
s = f'\nExport {export_options.input_file}'
|
|
138
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
139
|
+
s = f' as {export_options.output_file}'
|
|
140
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
141
|
+
if export_options.start:
|
|
142
|
+
s = f'start {export_options.start}'
|
|
143
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
144
|
+
if export_options.end:
|
|
145
|
+
s = f'end {export_options.end}'
|
|
146
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
147
|
+
s = f'pitch {export_options.pitch:.3f}'
|
|
148
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
149
|
+
s = f'tempo {export_options.tempo:.3f}'
|
|
150
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
151
|
+
s = 'Exporting...'
|
|
152
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_NORMAL)
|
|
153
|
+
|
|
154
|
+
pitch = export_options.pitch
|
|
155
|
+
tempo = export_options.tempo
|
|
156
|
+
pitch_t = pitch / tempo
|
|
157
|
+
|
|
158
|
+
mpv_opts = {
|
|
159
|
+
'video': 'no',
|
|
160
|
+
'o': export_options.output_file,
|
|
161
|
+
'of': export_options.output_format,
|
|
162
|
+
'audio_pitch_correction': 'no',
|
|
163
|
+
'ocopy_metadata': 'no',
|
|
164
|
+
}
|
|
165
|
+
if export_options.tags:
|
|
166
|
+
t = export_options.tags
|
|
167
|
+
tagstring = ','.join([f'{k}="{v}"' for k, v in t.items()])
|
|
168
|
+
mpv_opts['oset_metadata'] = tagstring
|
|
169
|
+
if abs(tempo - 1.0) > .0005:
|
|
170
|
+
mpv_opts['speed'] = f'{tempo:.3f}'
|
|
171
|
+
if abs(pitch_t - 1.0) > .0005:
|
|
172
|
+
mpv_opts['af'] = f'@rb:rubberband=pitch-scale={pitch_t:.3f}'
|
|
173
|
+
if export_options.start:
|
|
174
|
+
mpv_opts['start'] = export_options.start
|
|
175
|
+
if export_options.end:
|
|
176
|
+
mpv_opts['end'] = export_options.end
|
|
177
|
+
|
|
178
|
+
out_dir = os.path.dirname(export_options.output_file)
|
|
179
|
+
if not os.path.isdir(out_dir):
|
|
180
|
+
s = f'No such directory: {out_dir}'
|
|
181
|
+
self.AddToLog.emit(s, lv.LOG_LEVEL_ERROR)
|
|
182
|
+
self.AddToLog.emit('Aborting.', lv.LOG_LEVEL_WARNING)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Create the file now instead of letting mpv create it to
|
|
186
|
+
# avoid a race condition in existence checking.
|
|
187
|
+
try:
|
|
188
|
+
with open(export_options.output_file, 'x'):
|
|
189
|
+
pass
|
|
190
|
+
except OSError as err:
|
|
191
|
+
self.AddToLog.emit(str(err), lv.LOG_LEVEL_ERROR)
|
|
192
|
+
self.AddToLog.emit('Aborting.', lv.LOG_LEVEL_WARNING)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
with anal.mpv_cm(log_handler=self._log_handler,
|
|
196
|
+
**mpv_opts) as (err, player):
|
|
197
|
+
if err is not None:
|
|
198
|
+
self.AddToLog.emit('Aborting.', lv.LOG_LEVEL_WARNING)
|
|
199
|
+
return
|
|
200
|
+
with player.prepare_and_wait_for_event(
|
|
201
|
+
'file_loaded',
|
|
202
|
+
'end_file',
|
|
203
|
+
cond=self._mpv_error_checker,
|
|
204
|
+
timeout=5):
|
|
205
|
+
player.command('loadfile',
|
|
206
|
+
com.string_to_raw(export_options.input_file),
|
|
207
|
+
'replace')
|
|
208
|
+
if self._loaded:
|
|
209
|
+
player.wait_for_playback()
|
|
210
|
+
file = export_options.output_file
|
|
211
|
+
if os.path.exists(file) and os.path.getsize(file) > 0:
|
|
212
|
+
self.AddToLog.emit('Done', lv.LOG_LEVEL_NORMAL)
|
|
213
|
+
self.Success.emit()
|
|
214
|
+
return
|
|
215
|
+
self.AddToLog.emit('Export failed', lv.LOG_LEVEL_ERROR)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _split_extension(path):
|
|
219
|
+
directory, file = os.path.split(path)
|
|
220
|
+
name, dot, ext = file.rpartition('.')
|
|
221
|
+
return (os.path.join(directory, name if name else file), ext)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class _TagEditor(QWidget):
|
|
225
|
+
def __init__(self, *args, **kwargs):
|
|
226
|
+
super().__init__(*args, **kwargs)
|
|
227
|
+
|
|
228
|
+
model = QStandardItemModel(0, 2, self)
|
|
229
|
+
headers = ('Tag', 'Value')
|
|
230
|
+
model.setHorizontalHeaderLabels(headers)
|
|
231
|
+
|
|
232
|
+
vbox = QVBoxLayout()
|
|
233
|
+
self.setLayout(vbox)
|
|
234
|
+
|
|
235
|
+
view = QTableView()
|
|
236
|
+
view.setModel(model)
|
|
237
|
+
view.setColumnWidth(0, 200)
|
|
238
|
+
view.setColumnWidth(1, 450)
|
|
239
|
+
view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
240
|
+
view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
|
241
|
+
|
|
242
|
+
vbox.addWidget(view)
|
|
243
|
+
|
|
244
|
+
buttons = QWidget()
|
|
245
|
+
hbox = QHBoxLayout()
|
|
246
|
+
buttons.setLayout(hbox)
|
|
247
|
+
|
|
248
|
+
delete_button = QPushButton('Delete selected tag')
|
|
249
|
+
delete_button.clicked.connect(self.delete_tag)
|
|
250
|
+
hbox.addWidget(delete_button)
|
|
251
|
+
add_button = QPushButton('Insert empty tag before selection')
|
|
252
|
+
add_button.clicked.connect(self.insert_before)
|
|
253
|
+
hbox.addWidget(add_button)
|
|
254
|
+
add_button = QPushButton('Insert empty tag after selection')
|
|
255
|
+
add_button.clicked.connect(self.insert_after)
|
|
256
|
+
hbox.addWidget(add_button)
|
|
257
|
+
|
|
258
|
+
vbox.addWidget(buttons)
|
|
259
|
+
|
|
260
|
+
self.model = model
|
|
261
|
+
self.view = view
|
|
262
|
+
|
|
263
|
+
def add_tag(self, tag, value):
|
|
264
|
+
tag_item = QStandardItem(tag)
|
|
265
|
+
value_item = QStandardItem(value)
|
|
266
|
+
self.model.appendRow((tag_item, value_item))
|
|
267
|
+
|
|
268
|
+
def insert_before(self):
|
|
269
|
+
indexes = self.view.selectionModel().selectedRows()
|
|
270
|
+
if indexes:
|
|
271
|
+
index = max([x.row() for x in indexes])
|
|
272
|
+
else:
|
|
273
|
+
index = 0
|
|
274
|
+
self.model.insertRow(index)
|
|
275
|
+
|
|
276
|
+
def insert_after(self):
|
|
277
|
+
indexes = self.view.selectionModel().selectedRows()
|
|
278
|
+
if indexes:
|
|
279
|
+
index = max([x.row() for x in indexes])
|
|
280
|
+
else:
|
|
281
|
+
index = -1
|
|
282
|
+
self.model.insertRow(index + 1)
|
|
283
|
+
|
|
284
|
+
def delete_tag(self):
|
|
285
|
+
indexes = self.view.selectionModel().selectedRows()
|
|
286
|
+
for index in indexes:
|
|
287
|
+
self.model.removeRow(index.row())
|
|
288
|
+
|
|
289
|
+
def clear_tags(self):
|
|
290
|
+
self.model.setRowCount(0)
|
|
291
|
+
|
|
292
|
+
def tags(self):
|
|
293
|
+
n = self.model.rowCount()
|
|
294
|
+
i = 0
|
|
295
|
+
while i < n:
|
|
296
|
+
k = self.model.item(i, 0).text()
|
|
297
|
+
v = self.model.item(i, 1).text()
|
|
298
|
+
i += 1
|
|
299
|
+
yield (k, v)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ExportWindow(QWidget):
|
|
303
|
+
"""A window that handles exporting audio, including editing tags.
|
|
304
|
+
Inherits from QWidget.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
parent
|
|
309
|
+
The parent widget.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
AddToLog = pyqtSignal(str, int)
|
|
313
|
+
"""Signal emitted to request a message be added to the log.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
str
|
|
318
|
+
The message.
|
|
319
|
+
int
|
|
320
|
+
The severity level as defined in audio_tuner_gui.log_viewer
|
|
321
|
+
(LOG_LEVEL_ERROR, LOG_LEVEL_WARNING or LOG_LEVEL_NORMAL).
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
Export = pyqtSignal(_ExportOptions)
|
|
325
|
+
"""Signal emitted to tell the worker thread to do an export.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
_ExportOptions
|
|
330
|
+
The export options.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
Starting = pyqtSignal()
|
|
334
|
+
"""Signal emitted when exporting is starting."""
|
|
335
|
+
|
|
336
|
+
Finished = pyqtSignal()
|
|
337
|
+
"""Signal emitted when exporting is finished."""
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def __init__(self, parent):
|
|
341
|
+
super().__init__(parent)
|
|
342
|
+
|
|
343
|
+
self._ext = _DEFAULT_EXT
|
|
344
|
+
self._line_edit_update_needed = False
|
|
345
|
+
self._thread_started = False
|
|
346
|
+
|
|
347
|
+
self.setWindowFlag(Qt.WindowType.Window)
|
|
348
|
+
self.setWindowTitle('Export')
|
|
349
|
+
|
|
350
|
+
vbox = QVBoxLayout(self)
|
|
351
|
+
|
|
352
|
+
self._path_chooser = QWidget()
|
|
353
|
+
self._path_chooser_layout = QHBoxLayout(self._path_chooser)
|
|
354
|
+
self._path_chooser_layout.setContentsMargins(0, 0, 0, 0)
|
|
355
|
+
self._line_edit = QLineEdit()
|
|
356
|
+
self._path_chooser_layout.addWidget(self._line_edit)
|
|
357
|
+
self._path_button = QPushButton('Choose path...')
|
|
358
|
+
self._path_button.clicked.connect(self._choose_path)
|
|
359
|
+
self._path_chooser_layout.addWidget(self._path_button)
|
|
360
|
+
vbox.addWidget(self._path_chooser)
|
|
361
|
+
|
|
362
|
+
self._format_chooser = QComboBox()
|
|
363
|
+
for item in _OUTPUT_FORMATS:
|
|
364
|
+
s = f'{item["name"]} ({item["desc"]})'
|
|
365
|
+
self._format_chooser.addItem(s, item)
|
|
366
|
+
vbox.addWidget(self._format_chooser)
|
|
367
|
+
self._format_chooser.currentIndexChanged.connect(self._format_selected)
|
|
368
|
+
self._line_edit.editingFinished.connect(self._text_edited)
|
|
369
|
+
|
|
370
|
+
self._option_controls = QWidget()
|
|
371
|
+
self._option_controls_layout = QHBoxLayout(self._option_controls)
|
|
372
|
+
self._start_end = QCheckBox(
|
|
373
|
+
f'Only export between {gcom.OPTION_START} and {gcom.OPTION_END}')
|
|
374
|
+
self._option_controls_layout.addWidget(self._start_end)
|
|
375
|
+
|
|
376
|
+
vbox.addWidget(self._option_controls)
|
|
377
|
+
|
|
378
|
+
self._tag_editor = _TagEditor()
|
|
379
|
+
vbox.addWidget(self._tag_editor)
|
|
380
|
+
|
|
381
|
+
self._buttons = QWidget()
|
|
382
|
+
self._buttons_layout = QHBoxLayout(self._buttons)
|
|
383
|
+
self._buttons_layout.setContentsMargins(0, 0, 0, 0)
|
|
384
|
+
self._close_button = QPushButton('Close')
|
|
385
|
+
self._close_button.setShortcut('Escape')
|
|
386
|
+
self._close_button.clicked.connect(self.hide)
|
|
387
|
+
self._buttons_layout.addWidget(self._close_button)
|
|
388
|
+
self._export_button = QPushButton('Export')
|
|
389
|
+
self._export_button.clicked.connect(self._export,
|
|
390
|
+
Qt.ConnectionType.SingleShotConnection)
|
|
391
|
+
self._buttons_layout.addWidget(self._export_button)
|
|
392
|
+
vbox.addWidget(self._buttons)
|
|
393
|
+
|
|
394
|
+
self._set_format_from_ext(_DEFAULT_EXT)
|
|
395
|
+
|
|
396
|
+
self.resize(720, 400)
|
|
397
|
+
|
|
398
|
+
def show(self, original_path, title, options, metadata: dict={}):
|
|
399
|
+
"""Show the window, or try to give it focus if it's already
|
|
400
|
+
shown.
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
original_path : str or QDir
|
|
405
|
+
The path of the original audio file to be exported.
|
|
406
|
+
title : str
|
|
407
|
+
The title of the audio. This overrides the `title` or
|
|
408
|
+
`TITLE` tag.
|
|
409
|
+
options : audio_tuner_gui.common.Options
|
|
410
|
+
The options set for the audio.
|
|
411
|
+
metadata : dict
|
|
412
|
+
The tags to fill in the tag editor with. Tags in
|
|
413
|
+
`_ENCODER_TAGS` are ignored, and `title` and `TITLE` tags
|
|
414
|
+
have their value replaced with the value of the title
|
|
415
|
+
parameter.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
if mpv_error is not None:
|
|
419
|
+
self.AddToLog.emit(mpv_error, lv.LOG_LEVEL_ERROR)
|
|
420
|
+
return
|
|
421
|
+
if isinstance(original_path, QDir):
|
|
422
|
+
self._original_path = original_path.path()
|
|
423
|
+
else:
|
|
424
|
+
self._original_path = original_path
|
|
425
|
+
file = os.path.basename(self._original_path)
|
|
426
|
+
self._file_name, self._orig_ext = _split_extension(file)
|
|
427
|
+
self._options = options
|
|
428
|
+
|
|
429
|
+
self._line_edit_update_needed = True
|
|
430
|
+
self._update_line_edit()
|
|
431
|
+
if not self._thread_started:
|
|
432
|
+
self._start_worker_thread()
|
|
433
|
+
|
|
434
|
+
self._tag_editor.clear_tags()
|
|
435
|
+
if title is not None and not ('title' in metadata
|
|
436
|
+
or 'TITLE' in metadata):
|
|
437
|
+
self._tag_editor.add_tag('title', title)
|
|
438
|
+
for tag, value in metadata.items():
|
|
439
|
+
if tag in ('title', 'TITLE'):
|
|
440
|
+
value = title
|
|
441
|
+
if tag not in _ENCODER_TAGS:
|
|
442
|
+
self._tag_editor.add_tag(tag, value)
|
|
443
|
+
|
|
444
|
+
super().show()
|
|
445
|
+
self.activateWindow()
|
|
446
|
+
|
|
447
|
+
def _start_worker_thread(self):
|
|
448
|
+
worker_thread = QThread(self)
|
|
449
|
+
self._worker_thread = worker_thread
|
|
450
|
+
self._worker = _Worker()
|
|
451
|
+
self._worker.moveToThread(worker_thread)
|
|
452
|
+
self.Export.connect(self._worker.export,
|
|
453
|
+
Qt.ConnectionType.QueuedConnection)
|
|
454
|
+
self._worker.AddToLog.connect(self._handle_log_message,
|
|
455
|
+
Qt.ConnectionType.QueuedConnection)
|
|
456
|
+
self._worker.Starting.connect(self._starting,
|
|
457
|
+
Qt.ConnectionType.QueuedConnection)
|
|
458
|
+
self._worker.Finished.connect(self._finished,
|
|
459
|
+
Qt.ConnectionType.QueuedConnection)
|
|
460
|
+
self._worker.Success.connect(self._success,
|
|
461
|
+
Qt.ConnectionType.QueuedConnection)
|
|
462
|
+
worker_thread.start()
|
|
463
|
+
self._thread_started = True
|
|
464
|
+
|
|
465
|
+
def _handle_log_message(self, message, level):
|
|
466
|
+
self.AddToLog.emit(message, level)
|
|
467
|
+
|
|
468
|
+
def set_dir(self, directory):
|
|
469
|
+
"""Set the directory. This does not immediately update the path
|
|
470
|
+
shown in the line edit widget, but does affect what directory is
|
|
471
|
+
shown the next time it's updated.
|
|
472
|
+
|
|
473
|
+
Parameters
|
|
474
|
+
----------
|
|
475
|
+
directory : str or QDir
|
|
476
|
+
The directory.
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
if isinstance(directory, QDir):
|
|
480
|
+
d = directory.path()
|
|
481
|
+
else:
|
|
482
|
+
d = directory
|
|
483
|
+
self._directory = d
|
|
484
|
+
|
|
485
|
+
def _export(self):
|
|
486
|
+
output_file = self._line_edit.text()
|
|
487
|
+
output_format = self._format_chooser.currentData()['name']
|
|
488
|
+
pitch = self._options[gcom.OPTION_PITCH]
|
|
489
|
+
tempo = self._options[gcom.OPTION_TEMPO]
|
|
490
|
+
tags = {k: v for k, v in self._tag_editor.tags()}
|
|
491
|
+
export_options = _ExportOptions(input_file=self._original_path,
|
|
492
|
+
output_file=output_file,
|
|
493
|
+
output_format=output_format,
|
|
494
|
+
tags=tags,
|
|
495
|
+
pitch=pitch,
|
|
496
|
+
tempo=tempo)
|
|
497
|
+
if self._start_end.isChecked():
|
|
498
|
+
export_options.start = self._options[gcom.OPTION_START]
|
|
499
|
+
export_options.end = self._options[gcom.OPTION_END]
|
|
500
|
+
self.Export.emit(export_options)
|
|
501
|
+
|
|
502
|
+
def _starting(self):
|
|
503
|
+
self._export_button.setEnabled(False)
|
|
504
|
+
self.Starting.emit()
|
|
505
|
+
|
|
506
|
+
def _finished(self):
|
|
507
|
+
self._export_button.clicked.connect(self._export,
|
|
508
|
+
Qt.ConnectionType.SingleShotConnection)
|
|
509
|
+
self.Finished.emit()
|
|
510
|
+
self._export_button.setEnabled(True)
|
|
511
|
+
|
|
512
|
+
def _success(self):
|
|
513
|
+
self.hide()
|
|
514
|
+
|
|
515
|
+
def _set_format_from_ext(self, ext):
|
|
516
|
+
for item in _OUTPUT_FORMATS:
|
|
517
|
+
if item['ext'] == ext:
|
|
518
|
+
s = f'{item["name"]} ({item["desc"]})'
|
|
519
|
+
self._format_chooser.setCurrentText(s)
|
|
520
|
+
return True
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
def _text_edited(self):
|
|
524
|
+
path = self._line_edit.text()
|
|
525
|
+
spath, ext = _split_extension(path)
|
|
526
|
+
if spath:
|
|
527
|
+
directory, self._file_name = os.path.split(spath)
|
|
528
|
+
self.set_dir(directory)
|
|
529
|
+
self._line_edit_update_needed = True
|
|
530
|
+
self._set_format_from_ext(ext)
|
|
531
|
+
if self._line_edit_update_needed:
|
|
532
|
+
self._update_line_edit()
|
|
533
|
+
|
|
534
|
+
def _format_selected(self, index):
|
|
535
|
+
self._ext = self._format_chooser.itemData(index)['ext']
|
|
536
|
+
self._update_line_edit()
|
|
537
|
+
|
|
538
|
+
def _update_line_edit(self):
|
|
539
|
+
new_file = f'{self._file_name}.{self._ext}'
|
|
540
|
+
new_path = os.path.join(self._directory, new_file)
|
|
541
|
+
self._line_edit.setText(QDir.toNativeSeparators(new_path))
|
|
542
|
+
self._line_edit_update_needed = False
|
|
543
|
+
|
|
544
|
+
def _choose_path(self):
|
|
545
|
+
p = os.path.join(self._directory, f'{self._file_name}.{self._ext}')
|
|
546
|
+
path = QFileDialog.getSaveFileName(parent=self,
|
|
547
|
+
caption='Choose path',
|
|
548
|
+
directory=p)
|
|
549
|
+
spath, ext = _split_extension(path[0])
|
|
550
|
+
if spath:
|
|
551
|
+
directory, self._file_name = os.path.split(spath)
|
|
552
|
+
self.set_dir(directory)
|
|
553
|
+
self._line_edit_update_needed = True
|
|
554
|
+
self._set_format_from_ext(ext)
|
|
555
|
+
if self._line_edit_update_needed:
|
|
556
|
+
self._update_line_edit()
|
|
557
|
+
|
|
558
|
+
def quit_thread(self):
|
|
559
|
+
"""Stop the worker thread. Call this before closing the app."""
|
|
560
|
+
|
|
561
|
+
if self._thread_started:
|
|
562
|
+
self._worker_thread.quit()
|
|
563
|
+
self._thread_started = False
|