个人主页:创客白泽 - CSDN博客
系列专栏:《Python开源项目实战》
热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。
如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦
在数字音频处理领域,音频格式转换是一项基础但至关重要的功能。无论是音乐制作、播客编辑还是日常多媒体处理,我们经常需要在不同音频格式之间进行转换。本文介绍的全能音频转换大师是一款基于Python PyQt5框架开发,结合FFmpeg强大功能的图形化音频转换工具。
相较于市面上其他转换工具,本程序具有以下显著优势:
# 必需依赖
pip install PyQt5
# FFmpeg需要单独安装
# Windows: 下载并添加至PATH
# macOS: brew install ffmpeg
# Linux: sudo apt install ffmpeg
处理音频转换的核心线程类,主要功能:
主窗口类,负责:
def get_audio_duration(self, file_path):
"""使用ffprobe获取音频时长"""
cmd = ['ffprobe', '-v', 'error', '-show_entries',
'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return float(result.stdout.strip())
# 正则表达式匹配FFmpeg输出
self.duration_regex = re.compile(r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}")
self.time_regex = re.compile(r"time=(\d{2}):(\d{2}):(\d{2})\.\d{2}")
# 在输出中匹配时间信息
time_match = self.time_regex.search(line)
if time_match:
hours, minutes, seconds = map(int, time_match.groups())
current_time = hours * 3600 + minutes * 60 + seconds
progress = min(100, int((current_time / duration) * 100))
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
urls = event.mimeData().urls()
for url in urls:
file_path = url.toLocalFile()
# 处理文件或文件夹
音频转换是耗时操作,必须使用多线程避免界面冻结。我们继承QThread创建专门的工作线程:
class AudioConverterThread(QThread):
progress_updated = pyqtSignal(int, str, str) # 信号定义
conversion_finished = pyqtSignal(str, bool, str)
def run(self):
# 转换逻辑实现
for input_file in self.input_files:
# 构建FFmpeg命令
cmd = ['ffmpeg', '-i', input_file, '-y']
# ...参数设置...
# 执行转换
process = subprocess.Popen(cmd, stderr=subprocess.PIPE)
# 进度解析循环
while True:
line = process.stderr.readline()
# ...解析进度...
self.progress_updated.emit(progress, message, filename)
提供预设和自定义两种参数设置方式:
def set_quality_preset(self):
"""根据预设设置默认参数"""
if self.quality_preset == "high":
self.bitrate = self.bitrate or 320
self.samplerate = self.samplerate or 48000
elif self.quality_preset == "medium":
self.bitrate = self.bitrate or 192
self.samplerate = self.samplerate or 44100
elif self.quality_preset == "low":
self.bitrate = self.bitrate or 128
self.samplerate = self.samplerate or 22050
根据音频时长和编码参数预估输出大小:
def estimate_sizes(self):
# 对于WAV格式,大小与时长和采样率成正比
if self.output_format == "wav":
estimated_size = input_size * 1.2 # 粗略估计
else:
# 对于有损压缩格式,大小=比特率×时长
duration = self.get_audio_duration(input_file)
estimated_size = (self.bitrate * 1000 * duration) / 8 # bit to bytes
使用QSS样式表提升界面美观度:
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
border: 1px solid #ddd;
border-radius: 8px;
margin-top: 10px;
}
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 4px;
}
QProgressBar::chunk {
background-color: #4CAF50;
}
""")
import os
import sys
import subprocess
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QListWidget, QFileDialog, QComboBox,
QProgressBar, QMessageBox, QGroupBox, QSpinBox, QCheckBox,
QSizePolicy, QRadioButton, QButtonGroup)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl, QMimeData
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QDragEnterEvent, QDropEvent
class AudioConverterThread(QThread):
progress_updated = pyqtSignal(int, str)
conversion_finished = pyqtSignal(str, bool, str)
estimation_ready = pyqtSignal(dict)
def __init__(self, input_files, output_format, output_dir, quality_preset="medium",
bitrate=None, samplerate=None, remove_original=False, estimate_only=False):
super().__init__()
self.input_files = input_files
self.output_format = output_format
self.output_dir = output_dir
self.quality_preset = quality_preset
self.bitrate = bitrate
self.samplerate = samplerate
self.remove_original = remove_original
self.estimate_only = estimate_only
self.canceled = False
# 根据品质预设设置默认参数
self.set_quality_preset()
def set_quality_preset(self):
if self.quality_preset == "high":
self.bitrate = self.bitrate or 320
self.samplerate = self.samplerate or 48000
elif self.quality_preset == "medium":
self.bitrate = self.bitrate or 192
self.samplerate = self.samplerate or 44100
elif self.quality_preset == "low":
self.bitrate = self.bitrate or 128
self.samplerate = self.samplerate or 22050
def run(self):
total_files = len(self.input_files)
total_size = 0
estimated_sizes = {}
for i, input_file in enumerate(self.input_files):
if self.canceled:
break
try:
# 获取文件信息
filename = os.path.basename(input_file)
base_name = os.path.splitext(filename)[0]
output_file = os.path.join(self.output_dir, f"{base_name}.{self.output_format}")
input_size = os.path.getsize(input_file)
# 如果是预估模式
if self.estimate_only:
# 简化的预估算法 (实际大小会因编码效率而异)
if self.output_format == "wav":
# WAV通常是未压缩的,大小与采样率/位深相关
estimated_size = input_size * 1.2 # 粗略估计
else:
# 压缩格式基于比特率估算
duration = self.get_audio_duration(input_file)
estimated_size = (self.bitrate * 1000 * duration) / 8 # bit to bytes
estimated_sizes[filename] = {
'input_size': input_size,
'estimated_size': int(estimated_size),
'input_path': input_file,
'output_path': output_file
}
continue
# 构建FFmpeg命令
cmd = ['ffmpeg', '-i', input_file, '-y'] # -y 覆盖已存在文件
# 添加音频参数
if self.bitrate:
cmd.extend(['-b:a', f'{self.bitrate}k'])
if self.samplerate:
cmd.extend(['-ar', str(self.samplerate)])
cmd.append(output_file)
# 执行转换
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, bufsize=1)
# 读取进度
for line in process.stderr:
if self.canceled:
process.terminate()
break
# 解析进度信息
if 'time=' in line:
time_pos = line.find('time=')
time_str = line[time_pos+5:time_pos+14]
self.progress_updated.emit(int((i + 1) / total_files * 100), f"处理: {filename} ({time_str})")
process.wait()
if process.returncode == 0:
# 如果选择删除原文件
if self.remove_original:
os.remove(input_file)
output_size = os.path.getsize(output_file)
total_size += output_size
self.conversion_finished.emit(input_file, True,
f"成功: {filename} ({self.format_size(output_size)})")
else:
self.conversion_finished.emit(input_file, False,
f"失败: {filename} (错误代码: {process.returncode})")
except Exception as e:
self.conversion_finished.emit(input_file, False, f"错误: {filename} ({str(e)})")
# 更新进度
if not self.estimate_only:
progress = int((i + 1) / total_files * 100)
self.progress_updated.emit(progress, f"处理文件 {i+1}/{total_files}")
if self.estimate_only:
self.estimation_ready.emit(estimated_sizes)
def get_audio_duration(self, file_path):
"""获取音频文件时长(秒)"""
try:
cmd = ['ffprobe', '-v', 'error', '-show_entries',
'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return float(result.stdout.strip())
except:
return 180 # 默认3分钟 (如果无法获取时长)
@staticmethod
def format_size(size):
"""格式化文件大小显示"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
class AudioConverterApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("音频格式转换工具")
self.setGeometry(100, 100, 900, 700)
self.setWindowIcon(QIcon.fromTheme("multimedia-volume-control"))
# 初始化变量
self.input_files = []
self.output_dir = ""
self.converter_thread = None
# 设置样式
self.setup_ui_style()
# 初始化UI
self.init_ui()
self.setAcceptDrops(True)
def setup_ui_style(self):
# 设置应用程序样式
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
border: 1px solid #ddd;
border-radius: 8px;
margin-top: 10px;
padding-top: 15px;
font-weight: bold;
color: #555;
background-color: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 3px;
}
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
min-width: 100px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
QPushButton#cancelButton {
background-color: #f44336;
}
QPushButton#cancelButton:hover {
background-color: #d32f2f;
}
QListWidget {
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
}
QProgressBar {
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
height: 20px;
}
QProgressBar::chunk {
background-color: #4CAF50;
width: 10px;
}
QComboBox, QSpinBox {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
min-width: 120px;
}
QRadioButton {
spacing: 5px;
}
QLabel#sizeLabel {
color: #666;
font-size: 13px;
}
QLabel#titleLabel {
color: #2c3e50;
}
""")
# 设置调色板
palette = self.palette()
palette.setColor(QPalette.Window, QColor(245, 245, 245))
palette.setColor(QPalette.WindowText, QColor(51, 51, 51))
palette.setColor(QPalette.Base, QColor(255, 255, 255))
palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240))
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 220))
palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0))
palette.setColor(QPalette.Text, QColor(0, 0, 0))
palette.setColor(QPalette.Button, QColor(240, 240, 240))
palette.setColor(QPalette.ButtonText, QColor(0, 0, 0))
palette.setColor(QPalette.BrightText, QColor(255, 0, 0))
palette.setColor(QPalette.Highlight, QColor(76, 175, 80))
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
self.setPalette(palette)
def init_ui(self):
# 主窗口部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
# 标题
title_label = QLabel(" 音频格式转换工具")
title_label.setObjectName("titleLabel")
title_label.setFont(QFont("Arial", 18, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title_label)
# 文件选择区域
file_group = QGroupBox(" 选择音频文件 (支持拖拽文件到此处)")
file_layout = QVBoxLayout()
self.file_list = QListWidget()
self.file_list.setSelectionMode(QListWidget.ExtendedSelection)
file_button_layout = QHBoxLayout()
self.add_file_btn = QPushButton("➕ 添加文件")
self.add_file_btn.clicked.connect(self.add_files)
self.add_folder_btn = QPushButton(" 添加文件夹")
self.add_folder_btn.clicked.connect(self.add_folder)
self.clear_btn = QPushButton("❌ 清空列表")
self.clear_btn.clicked.connect(self.clear_files)
file_button_layout.addWidget(self.add_file_btn)
file_button_layout.addWidget(self.add_folder_btn)
file_button_layout.addWidget(self.clear_btn)
file_layout.addWidget(self.file_list)
file_layout.addLayout(file_button_layout)
file_group.setLayout(file_layout)
main_layout.addWidget(file_group)
# 输出设置区域
output_group = QGroupBox("⚙️ 输出设置")
output_layout = QHBoxLayout()
# 输出格式
format_layout = QVBoxLayout()
format_label = QLabel("️ 输出格式:")
self.format_combo = QComboBox()
self.format_combo.addItems(["mp3", "wav", "flac", "aac", "ogg", "m4a"])
format_layout.addWidget(format_label)
format_layout.addWidget(self.format_combo)
# 输出目录
dir_layout = QVBoxLayout()
dir_label = QLabel(" 输出目录:")
self.dir_btn = QPushButton("选择目录")
self.dir_btn.clicked.connect(self.select_output_dir)
self.dir_label = QLabel("(默认: 原文件目录)")
self.dir_label.setWordWrap(True)
dir_layout.addWidget(dir_label)
dir_layout.addWidget(self.dir_btn)
dir_layout.addWidget(self.dir_label)
# 其他选项
options_layout = QVBoxLayout()
self.remove_original_cb = QCheckBox("️ 转换后删除原文件")
options_layout.addWidget(self.remove_original_cb)
output_layout.addLayout(format_layout)
output_layout.addLayout(dir_layout)
output_layout.addLayout(options_layout)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
# 音质设置区域
quality_group = QGroupBox("️ 音质设置")
quality_layout = QHBoxLayout()
# 音质预设
preset_layout = QVBoxLayout()
preset_label = QLabel(" 音质预设:")
self.quality_group = QButtonGroup()
self.high_quality_rb = QRadioButton(" 高质量 (320kbps, 48kHz)")
self.medium_quality_rb = QRadioButton(" 中等质量 (192kbps, 44.1kHz)")
self.low_quality_rb = QRadioButton(" 低质量 (128kbps, 22kHz)")
self.custom_quality_rb = QRadioButton("⚙️ 自定义参数")
self.quality_group.addButton(self.high_quality_rb, 0)
self.quality_group.addButton(self.medium_quality_rb, 1)
self.quality_group.addButton(self.low_quality_rb, 2)
self.quality_group.addButton(self.custom_quality_rb, 3)
self.medium_quality_rb.setChecked(True)
self.quality_group.buttonClicked.connect(self.update_quality_settings)
preset_layout.addWidget(preset_label)
preset_layout.addWidget(self.high_quality_rb)
preset_layout.addWidget(self.medium_quality_rb)
preset_layout.addWidget(self.low_quality_rb)
preset_layout.addWidget(self.custom_quality_rb)
# 自定义参数
custom_layout = QVBoxLayout()
bitrate_layout = QHBoxLayout()
bitrate_label = QLabel(" 比特率 (kbps):")
self.bitrate_spin = QSpinBox()
self.bitrate_spin.setRange(32, 320)
self.bitrate_spin.setValue(192)
self.bitrate_spin.setSpecialValueText("自动")
bitrate_layout.addWidget(bitrate_label)
bitrate_layout.addWidget(self.bitrate_spin)
samplerate_layout = QHBoxLayout()
samplerate_label = QLabel(" 采样率 (Hz):")
self.samplerate_spin = QSpinBox()
self.samplerate_spin.setRange(8000, 48000)
self.samplerate_spin.setValue(44100)
self.samplerate_spin.setSingleStep(1000)
self.samplerate_spin.setSpecialValueText("自动")
samplerate_layout.addWidget(samplerate_label)
samplerate_layout.addWidget(self.samplerate_spin)
custom_layout.addLayout(bitrate_layout)
custom_layout.addLayout(samplerate_layout)
quality_layout.addLayout(preset_layout)
quality_layout.addLayout(custom_layout)
quality_group.setLayout(quality_layout)
main_layout.addWidget(quality_group)
# 文件大小预估区域
size_group = QGroupBox(" 文件大小预估")
size_layout = QVBoxLayout()
self.size_label = QLabel("ℹ️ 添加文件后自动预估输出大小")
self.size_label.setObjectName("sizeLabel")
self.size_label.setWordWrap(True)
self.estimate_btn = QPushButton(" 重新估算大小")
self.estimate_btn.clicked.connect(self.estimate_sizes)
self.estimate_btn.setEnabled(False)
size_layout.addWidget(self.size_label)
size_layout.addWidget(self.estimate_btn)
size_group.setLayout(size_layout)
main_layout.addWidget(size_group)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setAlignment(Qt.AlignCenter)
main_layout.addWidget(self.progress_bar)
# 转换按钮
button_layout = QHBoxLayout()
self.convert_btn = QPushButton("✨ 开始转换")
self.convert_btn.setFont(QFont("Arial", 12, QFont.Bold))
self.convert_btn.clicked.connect(self.start_conversion)
self.cancel_btn = QPushButton("⏹️ 取消")
self.cancel_btn.setObjectName("cancelButton")
self.cancel_btn.setFont(QFont("Arial", 12))
self.cancel_btn.clicked.connect(self.cancel_conversion)
self.cancel_btn.setEnabled(False)
button_layout.addStretch()
button_layout.addWidget(self.convert_btn)
button_layout.addWidget(self.cancel_btn)
button_layout.addStretch()
main_layout.addLayout(button_layout)
# 状态栏
self.statusBar().showMessage(" 准备就绪")
# 初始化UI状态
self.update_quality_settings()
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
urls = event.mimeData().urls()
new_files = []
for url in urls:
file_path = url.toLocalFile()
if os.path.isdir(file_path):
# 处理文件夹
audio_files = self.scan_audio_files(file_path)
new_files.extend(audio_files)
elif file_path.lower().endswith(('.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a')):
# 处理单个文件
new_files.append(file_path)
if new_files:
self.input_files.extend(new_files)
self.file_list.addItems([os.path.basename(f) for f in new_files])
self.update_status(f"添加了 {len(new_files)} 个文件")
self.estimate_sizes()
def scan_audio_files(self, folder):
"""扫描文件夹中的音频文件"""
audio_files = []
for root, _, files in os.walk(folder):
for file in files:
if file.lower().endswith(('.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a')):
audio_files.append(os.path.join(root, file))
return audio_files
def add_files(self):
files, _ = QFileDialog.getOpenFileNames(
self, "选择音频文件", "",
"音频文件 (*.mp3 *.wav *.flac *.aac *.ogg *.m4a);;所有文件 (*.*)"
)
if files:
self.input_files.extend(files)
self.file_list.addItems([os.path.basename(f) for f in files])
self.update_status(f"添加了 {len(files)} 个文件")
self.estimate_sizes()
def add_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
if folder:
audio_files = self.scan_audio_files(folder)
if audio_files:
self.input_files.extend(audio_files)
self.file_list.addItems([os.path.basename(f) for f in audio_files])
self.update_status(f"从文件夹添加了 {len(audio_files)} 个音频文件")
self.estimate_sizes()
else:
self.update_status("⚠️ 所选文件夹中没有找到音频文件", is_error=True)
def clear_files(self):
self.input_files = []
self.file_list.clear()
self.size_label.setText("ℹ️ 添加文件后自动预估输出大小")
self.update_status("文件列表已清空")
self.estimate_btn.setEnabled(False)
def select_output_dir(self):
dir_path = QFileDialog.getExistingDirectory(self, "选择输出目录")
if dir_path:
self.output_dir = dir_path
self.dir_label.setText(dir_path)
self.update_status(f"输出目录设置为: {dir_path}")
def update_status(self, message, is_error=False):
emoji = "⚠️" if is_error else "ℹ️"
self.statusBar().showMessage(f"{emoji} {message}")
def update_quality_settings(self):
"""根据选择的音质预设更新UI"""
if self.high_quality_rb.isChecked():
self.bitrate_spin.setValue(320)
self.samplerate_spin.setValue(48000)
self.bitrate_spin.setEnabled(False)
self.samplerate_spin.setEnabled(False)
elif self.medium_quality_rb.isChecked():
self.bitrate_spin.setValue(192)
self.samplerate_spin.setValue(44100)
self.bitrate_spin.setEnabled(False)
self.samplerate_spin.setEnabled(False)
elif self.low_quality_rb.isChecked():
self.bitrate_spin.setValue(128)
self.samplerate_spin.setValue(22050)
self.bitrate_spin.setEnabled(False)
self.samplerate_spin.setEnabled(False)
else: # 自定义
self.bitrate_spin.setEnabled(True)
self.samplerate_spin.setEnabled(True)
# 只有在有文件时才尝试估算大小
if hasattr(self, 'input_files') and self.input_files:
self.estimate_sizes()
def estimate_sizes(self):
"""预估输出文件大小"""
if not self.input_files:
self.size_label.setText("ℹ️ 请先添加要转换的文件")
return
output_format = self.format_combo.currentText()
# 如果没有指定输出目录,使用原文件目录
output_dir = self.output_dir if self.output_dir else os.path.dirname(self.input_files[0])
# 获取当前选择的音质预设
if self.high_quality_rb.isChecked():
quality_preset = "high"
elif self.medium_quality_rb.isChecked():
quality_preset = "medium"
elif self.low_quality_rb.isChecked():
quality_preset = "low"
else:
quality_preset = "custom"
# 创建估算线程
self.size_label.setText(" 正在估算输出文件大小...")
self.estimate_btn.setEnabled(False)
self.converter_thread = AudioConverterThread(
self.input_files, output_format, output_dir,
quality_preset=quality_preset,
bitrate=self.bitrate_spin.value() if self.bitrate_spin.value() > 0 else None,
samplerate=self.samplerate_spin.value() if self.samplerate_spin.value() > 0 else None,
estimate_only=True
)
self.converter_thread.estimation_ready.connect(self.update_size_estimation)
self.converter_thread.finished.connect(lambda: self.estimate_btn.setEnabled(True))
self.converter_thread.start()
def update_size_estimation(self, estimations):
"""更新大小预估显示"""
total_input = sum(info['input_size'] for info in estimations.values())
total_output = sum(info['estimated_size'] for info in estimations.values())
ratio = (total_output / total_input) if total_input > 0 else 0
ratio_text = f"{ratio:.1%}" if ratio > 0 else "N/A"
text = (f" 预估输出大小:\n"
f"输入总大小: {self.format_size(total_input)}\n"
f"预估输出总大小: {self.format_size(total_output)}\n"
f"压缩率: {ratio_text}")
self.size_label.setText(text)
self.estimate_btn.setEnabled(True)
@staticmethod
def format_size(size):
"""格式化文件大小显示"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def start_conversion(self):
if not self.input_files:
self.update_status("⚠️ 请先添加要转换的文件", is_error=True)
return
output_format = self.format_combo.currentText()
# 如果没有指定输出目录,使用原文件目录
if not self.output_dir:
self.output_dir = os.path.dirname(self.input_files[0])
self.dir_label.setText("(使用原文件目录)")
# 获取音质预设
if self.high_quality_rb.isChecked():
quality_preset = "high"
elif self.medium_quality_rb.isChecked():
quality_preset = "medium"
elif self.low_quality_rb.isChecked():
quality_preset = "low"
else:
quality_preset = "custom"
# 获取其他参数
bitrate = self.bitrate_spin.value() if self.bitrate_spin.value() > 0 else None
samplerate = self.samplerate_spin.value() if self.samplerate_spin.value() > 0 else None
remove_original = self.remove_original_cb.isChecked()
# 禁用UI控件
self.toggle_ui(False)
# 创建并启动转换线程
self.converter_thread = AudioConverterThread(
self.input_files, output_format, self.output_dir,
quality_preset=quality_preset,
bitrate=bitrate, samplerate=samplerate,
remove_original=remove_original
)
self.converter_thread.progress_updated.connect(self.update_progress)
self.converter_thread.conversion_finished.connect(self.conversion_result)
self.converter_thread.finished.connect(self.conversion_complete)
self.converter_thread.start()
self.update_status(" 开始转换文件...")
def cancel_conversion(self):
if self.converter_thread and self.converter_thread.isRunning():
self.converter_thread.canceled = True
self.update_status("⏹️ 正在取消转换...")
self.cancel_btn.setEnabled(False)
def update_progress(self, value, message):
self.progress_bar.setValue(value)
self.update_status(message)
def conversion_result(self, filename, success, message):
base_name = os.path.basename(filename)
item = self.file_list.findItems(base_name, Qt.MatchExactly)
if item:
if success:
item[0].setForeground(QColor(0, 128, 0)) # 绿色表示成功
else:
item[0].setForeground(QColor(255, 0, 0)) # 红色表示失败
self.update_status(message, not success)
def conversion_complete(self):
if self.converter_thread.canceled:
self.update_status("⏹️ 转换已取消", is_error=True)
else:
self.update_status(" 所有文件转换完成!")
# 重置UI
self.progress_bar.setValue(0)
self.toggle_ui(True)
# 如果选择了删除原文件,清空列表
if self.remove_original_cb.isChecked():
self.input_files = []
self.file_list.clear()
self.size_label.setText("ℹ️ 添加文件后自动预估输出大小")
def toggle_ui(self, enabled):
self.add_file_btn.setEnabled(enabled)
self.add_folder_btn.setEnabled(enabled)
self.clear_btn.setEnabled(enabled)
self.format_combo.setEnabled(enabled)
self.dir_btn.setEnabled(enabled)
self.high_quality_rb.setEnabled(enabled)
self.medium_quality_rb.setEnabled(enabled)
self.low_quality_rb.setEnabled(enabled)
self.custom_quality_rb.setEnabled(enabled)
self.bitrate_spin.setEnabled(enabled and self.custom_quality_rb.isChecked())
self.samplerate_spin.setEnabled(enabled and self.custom_quality_rb.isChecked())
self.remove_original_cb.setEnabled(enabled)
self.convert_btn.setEnabled(enabled)
self.cancel_btn.setEnabled(not enabled)
self.estimate_btn.setEnabled(enabled and bool(self.input_files))
def closeEvent(self, event):
if self.converter_thread and self.converter_thread.isRunning():
reply = QMessageBox.question(
self, '转换正在进行中',
"转换仍在进行中,确定要退出吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.converter_thread.canceled = True
event.accept()
else:
event.ignore()
else:
event.accept()
if __name__ == "__main__":
# 检查FFmpeg是否可用
try:
subprocess.run(['ffmpeg', '-version'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except FileNotFoundError:
app = QApplication(sys.argv)
QMessageBox.critical(
None, "错误",
"未找到FFmpeg,请先安装FFmpeg并确保它在系统路径中。\n\n"
"Windows用户可以从 https://ffmpeg.org/download.html 下载\n"
"macOS: brew install ffmpeg\n"
"Linux: sudo apt install ffmpeg"
)
sys.exit(1)
app = QApplication(sys.argv)
converter = AudioConverterApp()
converter.show()
sys.exit(app.exec_())
本文详细介绍了基于PyQt5和FFmpeg的音频转换工具的开发全过程。通过这个项目,我们实现了:
未来可能的改进方向:
希望本文能帮助读者掌握PyQt5应用开发的核心技术,理解如何将专业音视频处理工具(FFmpeg)集成到Python应用中。这种开发模式可以扩展到其他多媒体处理领域,如视频转换、图片处理等。
作者:创客白泽
版权声明:本文采用CC BY-NC-SA 4.0许可协议,转载请注明出处
更新日期:2025年5月30日
推荐阅读:
问题交流:欢迎在评论区留言讨论,作者会及时回复解答!