290 lines
11 KiB
Python
290 lines
11 KiB
Python
import os
|
|
|
|
import soundfile
|
|
|
|
from typing import List
|
|
from PySide6.QtCore import *
|
|
from PySide6.QtWidgets import *
|
|
from PySide6.QtGui import *
|
|
|
|
from gui.Ui_MainWindow import Ui_MainWindow
|
|
from gui.slicing_tasks import SlicingResult, SlicingSettings, parse_slicing_settings, run_slicing_task
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super(MainWindow, self).__init__()
|
|
|
|
self.ui = Ui_MainWindow()
|
|
self.ui.setupUi(self)
|
|
|
|
self.ui.pushButtonAddFiles.clicked.connect(self._q_add_audio_files)
|
|
self.ui.pushButtonBrowse.clicked.connect(self._q_browse_output_dir)
|
|
self.ui.pushButtonClearList.clicked.connect(self._q_clear_audio_list)
|
|
self.ui.pushButtonAbout.clicked.connect(self._q_about)
|
|
self.ui.pushButtonStart.clicked.connect(self._q_start)
|
|
|
|
self.ui.progressBar.setMinimum(0)
|
|
self.ui.progressBar.setMaximum(100)
|
|
self.ui.progressBar.setValue(0)
|
|
self.ui.pushButtonStart.setDefault(True)
|
|
|
|
validator = QRegularExpressionValidator(QRegularExpression(r"\d+"))
|
|
self.ui.lineEditThreshold.setValidator(QDoubleValidator())
|
|
self.ui.lineEditMinLen.setValidator(validator)
|
|
self.ui.lineEditMinInterval.setValidator(validator)
|
|
self.ui.lineEditHopSize.setValidator(validator)
|
|
self.ui.lineEditMaxSilence.setValidator(validator)
|
|
|
|
self.ui.listWidgetTaskList.setAlternatingRowColors(True)
|
|
|
|
# State variables
|
|
self.workers: list[QThread] = []
|
|
self.workCount = 0
|
|
self.workFinished = 0
|
|
self.workSucceeded = 0
|
|
self.workFailed = 0
|
|
self.workResults: list[SlicingResult] = []
|
|
self.processing = False
|
|
|
|
self.setWindowTitle(QApplication.applicationName())
|
|
|
|
# Must set to accept drag and drop events
|
|
self.setAcceptDrops(True)
|
|
|
|
# Get available formats/extensions supported
|
|
self.availableFormats = [str(formatExt).lower(
|
|
) for formatExt in soundfile.available_formats().keys()]
|
|
# libsndfile supports Opus in Ogg container
|
|
# .opus is a valid extension and recommended for Ogg Opus (see RFC 7845, Section 9)
|
|
# append opus for convenience as tools like youtube-dl(p) extract to .opus by default
|
|
self.availableFormats.append("opus")
|
|
|
|
self.formatAllFilter = " ".join(
|
|
[f"*.{formatExt}" for formatExt in self.availableFormats])
|
|
self.formatIndividualFilter = ";;".join(
|
|
[f"{formatExt} (*.{formatExt})" for formatExt in sorted(self.availableFormats)])
|
|
|
|
def _q_browse_output_dir(self):
|
|
path = QFileDialog.getExistingDirectory(
|
|
self, "Browse Output Directory", ".")
|
|
if path != "":
|
|
self.ui.lineEditOutputDir.setText(QDir.toNativeSeparators(path))
|
|
|
|
def _q_add_audio_files(self):
|
|
if self.processing:
|
|
self.warningProcessNotFinished()
|
|
return
|
|
|
|
paths, _ = QFileDialog.getOpenFileNames(
|
|
self, 'Select Audio Files', ".", f'Audio ({self.formatAllFilter});;{self.formatIndividualFilter}')
|
|
for path in paths:
|
|
item = QListWidgetItem()
|
|
item.setSizeHint(QSize(200, 24))
|
|
item.setText(QFileInfo(path).fileName())
|
|
# Save full path at custom role
|
|
item.setData(Qt.ItemDataRole.UserRole + 1, path)
|
|
self.ui.listWidgetTaskList.addItem(item)
|
|
|
|
def _q_clear_audio_list(self):
|
|
if self.processing:
|
|
self.warningProcessNotFinished()
|
|
return
|
|
|
|
self.ui.listWidgetTaskList.clear()
|
|
|
|
def _q_about(self):
|
|
QMessageBox.information(
|
|
self, "About", "Audio Slicer v1.4.0\nCopyright 2020-2026 OpenVPI Team")
|
|
|
|
def _q_start(self):
|
|
if self.processing:
|
|
self.warningProcessNotFinished()
|
|
return
|
|
|
|
item_count = self.ui.listWidgetTaskList.count()
|
|
if item_count == 0:
|
|
return
|
|
|
|
settings, error_message = parse_slicing_settings(
|
|
self.ui.lineEditThreshold.text(),
|
|
self.ui.lineEditMinLen.text(),
|
|
self.ui.lineEditMinInterval.text(),
|
|
self.ui.lineEditHopSize.text(),
|
|
self.ui.lineEditMaxSilence.text(),
|
|
)
|
|
if error_message:
|
|
QMessageBox.warning(self, QApplication.applicationName(), error_message)
|
|
return
|
|
|
|
output_format = self.ui.buttonGroup.checkedButton().text()
|
|
if output_format == "mp3":
|
|
ret = QMessageBox.warning(self, "Warning",
|
|
"MP3 is not recommended for saving vocals as it is lossy. "
|
|
"If you want to save disk space, consider using FLAC instead. "
|
|
"Do you want to continue?",
|
|
QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel)
|
|
if ret == QMessageBox.Cancel:
|
|
return
|
|
|
|
output_dir = self.ui.lineEditOutputDir.text()
|
|
|
|
class WorkThread(QThread):
|
|
oneFinished = Signal(object)
|
|
|
|
def __init__(self, filenames: List[str], output_dir: str, output_format: str, settings: SlicingSettings):
|
|
super().__init__()
|
|
|
|
self.filenames = filenames
|
|
self.output_dir = output_dir
|
|
self.output_format = output_format
|
|
self.settings = settings
|
|
|
|
def run(self):
|
|
for filename in self.filenames:
|
|
result = run_slicing_task(
|
|
filename,
|
|
self.output_dir,
|
|
self.output_format,
|
|
self.settings,
|
|
)
|
|
self.oneFinished.emit(result)
|
|
|
|
# Collect paths
|
|
paths: list[str] = []
|
|
for i in range(0, item_count):
|
|
item = self.ui.listWidgetTaskList.item(i)
|
|
path = item.data(Qt.ItemDataRole.UserRole + 1) # Get full path
|
|
paths.append(path)
|
|
|
|
self.ui.progressBar.setMaximum(item_count)
|
|
self.ui.progressBar.setValue(0)
|
|
|
|
self.workCount = item_count
|
|
self.workFinished = 0
|
|
self.workSucceeded = 0
|
|
self.workFailed = 0
|
|
self.workResults = []
|
|
self.setProcessing(True)
|
|
|
|
# Start work thread
|
|
worker = WorkThread(paths, output_dir, output_format, settings)
|
|
worker.oneFinished.connect(self._q_oneFinished)
|
|
worker.finished.connect(self._q_threadFinished)
|
|
worker.start()
|
|
|
|
self.workers.append(worker) # Collect in case of auto deletion
|
|
|
|
def _q_oneFinished(self, result: SlicingResult):
|
|
self.workResults.append(result)
|
|
self.workFinished += 1
|
|
if result.success:
|
|
self.workSucceeded += 1
|
|
else:
|
|
self.workFailed += 1
|
|
self.ui.progressBar.setValue(self.workFinished)
|
|
|
|
def _q_threadFinished(self):
|
|
# Join all workers
|
|
for worker in self.workers:
|
|
worker.wait()
|
|
self.workers.clear()
|
|
self.setProcessing(False)
|
|
self._show_completion_message(self.workResults)
|
|
|
|
def _show_completion_message(self, results: list[SlicingResult]):
|
|
processed_count = len(results)
|
|
success_count = sum(1 for result in results if result.success)
|
|
failed_count = processed_count - success_count
|
|
total_outputs = sum(result.output_count for result in results)
|
|
if failed_count == 0:
|
|
QMessageBox.information(
|
|
self,
|
|
QApplication.applicationName(),
|
|
f"Slicing complete!\nProcessed {processed_count} file(s).\nGenerated {total_outputs} output file(s).",
|
|
)
|
|
return
|
|
|
|
first_failure = next(result for result in results if not result.success)
|
|
failure_name = QFileInfo(first_failure.source_path).fileName() or first_failure.source_path
|
|
failure_message = (
|
|
f"Failed: {failure_name}\n{first_failure.error}"
|
|
if first_failure.error
|
|
else f"Failed: {failure_name}"
|
|
)
|
|
|
|
if success_count == 0:
|
|
QMessageBox.critical(
|
|
self,
|
|
QApplication.applicationName(),
|
|
f"Slicing failed for all {processed_count} file(s).\n{failure_message}",
|
|
)
|
|
return
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
QApplication.applicationName(),
|
|
"Slicing finished with errors.\n"
|
|
f"Succeeded: {success_count}\n"
|
|
f"Failed: {failed_count}\n"
|
|
f"Generated {total_outputs} output file(s).\n"
|
|
f"{failure_message}",
|
|
)
|
|
|
|
def warningProcessNotFinished(self):
|
|
QMessageBox.warning(self, QApplication.applicationName(),
|
|
"Please wait for slicing to complete!")
|
|
|
|
def setProcessing(self, processing: bool):
|
|
enabled = not processing
|
|
self.ui.pushButtonStart.setText(
|
|
"Slicing..." if processing else "Start")
|
|
self.ui.pushButtonStart.setEnabled(enabled)
|
|
self.ui.pushButtonAddFiles.setEnabled(enabled)
|
|
self.ui.listWidgetTaskList.setEnabled(enabled)
|
|
self.ui.pushButtonClearList.setEnabled(enabled)
|
|
self.ui.lineEditThreshold.setEnabled(enabled)
|
|
self.ui.lineEditMinLen.setEnabled(enabled)
|
|
self.ui.lineEditMinInterval.setEnabled(enabled)
|
|
self.ui.lineEditHopSize.setEnabled(enabled)
|
|
self.ui.lineEditMaxSilence.setEnabled(enabled)
|
|
self.ui.lineEditOutputDir.setEnabled(enabled)
|
|
self.ui.pushButtonBrowse.setEnabled(enabled)
|
|
self.processing = processing
|
|
|
|
# Event Handlers
|
|
def closeEvent(self, event):
|
|
if self.processing:
|
|
self.warningProcessNotFinished()
|
|
event.ignore()
|
|
|
|
def dragEnterEvent(self, event):
|
|
urls = event.mimeData().urls()
|
|
valid = False
|
|
for url in urls:
|
|
if not url.isLocalFile():
|
|
continue
|
|
path = url.toLocalFile()
|
|
ext = os.path.splitext(path)[1]
|
|
if ext[1:].lower() in self.availableFormats:
|
|
valid = True
|
|
break
|
|
if valid:
|
|
event.accept()
|
|
|
|
def dropEvent(self, event):
|
|
urls = event.mimeData().urls()
|
|
for url in urls:
|
|
if not url.isLocalFile():
|
|
continue
|
|
path = url.toLocalFile()
|
|
ext = os.path.splitext(path)[1]
|
|
if ext[1:].lower() not in self.availableFormats:
|
|
continue
|
|
item = QListWidgetItem()
|
|
item.setSizeHint(QSize(200, 24))
|
|
item.setText(QFileInfo(path).fileName())
|
|
item.setData(Qt.ItemDataRole.UserRole + 1,
|
|
path)
|
|
self.ui.listWidgetTaskList.addItem(item)
|