import os from PyQt5.QtCore import QProcess, Qt from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QMessageBox, QComboBox, QRadioButton class SAC(QWidget): def __init__(self): super().__init__() self.setWindowTitle('SAC - Simple Audio Converter') self.source_file_btn = QPushButton('Browse') self.dest_dir_btn = QPushButton('Browse') self.convert_btn = QPushButton('Convert') self.abort_btn = QPushButton('Abort') self.out_codec_dropdown = QComboBox() self.bitrate_dropdown = QComboBox() self.stereo_radio = QRadioButton('Stereo') self.mono_radio = QRadioButton('Mono') self.source_file_input = QLineEdit() self.dest_dir_input = QLineEdit() self.out_codec_dropdown.addItems(['mp3', 'aac', 'wav', 'flac', 'opus']) self.out_codec_dropdown.setCurrentText('mp3') self.bitrate_dropdown.addItems(['64K', '128K', '256K', '320K']) self.bitrate_dropdown.setCurrentText('128K') self.stereo_radio.setChecked(True) self.source_file_btn.clicked.connect(self.browse_source_file) self.dest_dir_btn.clicked.connect(self.browse_dest_dir) self.convert_btn.clicked.connect(self.convert_audio) self.abort_btn.clicked.connect(self.abort_audio) self.out_codec_dropdown.currentTextChanged.connect(self.toggle_bitrate) self.source_file_input.setAcceptDrops(True) self.dest_dir_input.setAcceptDrops(True) layout = QGridLayout() layout.addWidget(QLabel('Source File'), 0, 0), layout.addWidget(self.source_file_input, 0, 1), layout.addWidget(self.source_file_btn, 0, 2) layout.addWidget(QLabel('Destination Directory'), 1, 0), layout.addWidget(self.dest_dir_input, 1, 1), layout.addWidget(self.dest_dir_btn, 1, 2) layout.addWidget(QLabel('Output Codec'), 2, 0), layout.addWidget(self.out_codec_dropdown, 2, 1) layout.addWidget(QLabel('Bitrate'), 3, 0), layout.addWidget(self.bitrate_dropdown, 3, 1) layout.addWidget(QLabel('Channel'), 4, 0), layout.addWidget(self.stereo_radio, 4, 1), layout.addWidget(self.mono_radio, 4, 2) layout.addWidget(self.convert_btn, 5, 0), layout.addWidget(self.abort_btn, 5, 1) self.setLayout(layout) self.process = None self.abort_btn.setDisabled(True) def browse_source_file(self): filename, _ = QFileDialog.getOpenFileName(self, 'Select Source File', os.getenv('HOME'), 'Audio File (*.*)') if filename: self.source_file_input.setText(filename) self.dest_dir_input.setText(os.path.dirname(filename)) def browse_dest_dir(self): dirpath = QFileDialog.getExistingDirectory(self, 'Select Destination Directory', self.dest_dir_input.text()) if dirpath: self.dest_dir_input.setText(dirpath) def convert_audio(self): input_path = self.source_file_input.text() output_dir = self.dest_dir_input.text() out_codec = self.out_codec_dropdown.currentText() bitrate = self.bitrate_dropdown.currentText() if not out_codec in ['flac'] else '' channel = '2' if self.stereo_radio.isChecked() else '1' if not os.path.isfile(input_path) or not os.path.isdir(output_dir): return output_filename = os.path.splitext(os.path.basename(input_path))[0] + '.' + out_codec output_path = os.path.join(output_dir, output_filename) if out_codec == 'opus': cmd = f'ffmpeg -i "{input_path}" -ac {channel} -b:a {bitrate.lower()} -c:a libopus -application audio -strict -2 "{output_path}"' elif out_codec == 'wav': cmd = f'ffmpeg -i "{input_path}" -ac {channel} -c:a pcm_s16le "{output_path}"' else: cmd = f'ffmpeg -i "{input_path}" -ac {channel} "{output_path}"' [btn.setDisabled(True) for btn in [self.convert_btn, self.source_file_btn, self.dest_dir_btn, self.out_codec_dropdown, self.bitrate_dropdown, self.stereo_radio, self.mono_radio]] self.abort_btn.setDisabled(False) try: self.process = QProcess() self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.start(cmd) if not self.process.waitForStarted(): self.show_error('Error', 'Failed to start process') else: while self.process.state() == QProcess.Running: QApplication.processEvents() if self.process.exitCode() == 0: QMessageBox.information(self, 'Success', f'Audio file converted successfully: {output_path}') else: self.show_error('Error', f'Failed to convert audio file: {output_path}') except Exception as e: self.show_error('Error', str(e)) finally: [btn.setDisabled(False) for btn in [self.convert_btn, self.source_file_btn, self.dest_dir_btn, self.out_codec_dropdown, self.bitrate_dropdown, self.stereo_radio, self.mono_radio]] self.abort_btn.setDisabled(True) def abort_audio(self): self.process.terminate() def toggle_bitrate(self): is_opus = self.out_codec_dropdown.currentText() == 'opus' self.bitrate_dropdown.setDisabled(not is_opus) def show_error(self, title, message): QMessageBox.critical(self, title, message) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): if event.mimeData().hasUrls(): url = event.mimeData().urls()[0] path = url.toLocalFile() if os.path.isfile(path): self.source_file_input.setText(path) self.dest_dir_input.setText(os.path.dirname(path)) event.acceptProposedAction() if __name__ == '__main__': app = QApplication([]) sac = SAC() sac.show() app.exec_()