Python使用FFmpeg實現(xiàn)高效音頻格式轉(zhuǎn)換工具
概述
在數(shù)字音頻處理領(lǐng)域,音頻格式轉(zhuǎn)換是一項基礎(chǔ)但至關(guān)重要的功能。無論是音樂制作、播客編輯還是日常多媒體處理,我們經(jīng)常需要在不同音頻格式之間進行轉(zhuǎn)換。本文介紹的全能音頻轉(zhuǎn)換大師是一款基于Python PyQt5框架開發(fā),結(jié)合FFmpeg強大功能的圖形化音頻轉(zhuǎn)換工具。
相較于市面上其他轉(zhuǎn)換工具,本程序具有以下顯著優(yōu)勢:
- 多格式支持:支持MP3、WAV、FLAC、AAC、OGG、M4A等主流音頻格式互轉(zhuǎn)
- 智能音質(zhì)預(yù)設(shè):提供高中低三檔音質(zhì)預(yù)設(shè)及自定義參數(shù)選項
- 批量處理:支持文件/文件夾批量導(dǎo)入,高效處理大量音頻文件
- 可視化進度:實時顯示轉(zhuǎn)換進度和詳細(xì)狀態(tài)信息
- 智能預(yù)估:提前計算輸出文件大小,合理規(guī)劃存儲空間
- 跨平臺:基于Python開發(fā),可在Windows、macOS、Linux系統(tǒng)運行
功能詳解
1. 文件管理功能
多種添加方式:支持文件添加、文件夾添加和拖拽添加三種方式
格式過濾:自動過濾非音頻文件,確保輸入文件有效性
列表管理:可查看已添加文件列表,支持清空列表操作
2. 輸出設(shè)置
輸出格式選擇:通過下拉框選擇目標(biāo)格式
輸出目錄設(shè)置:可指定輸出目錄,默認(rèn)使用源文件目錄
原文件處理:可選轉(zhuǎn)換后刪除原文件以節(jié)省空間
3. 音質(zhì)控制
預(yù)設(shè)方案:
- 高質(zhì)量:320kbps比特率,48kHz采樣率
- 中等質(zhì)量:192kbps比特率,44.1kHz采樣率
- 低質(zhì)量:128kbps比特率,22kHz采樣率
自定義參數(shù):可自由設(shè)置比特率和采樣率
4. 智能預(yù)估系統(tǒng)
基于文件時長和編碼參數(shù)預(yù)估輸出文件大小
計算壓縮率,幫助用戶做出合理決策
可視化顯示輸入輸出總大小對比
5. 轉(zhuǎn)換引擎
基于FFmpeg實現(xiàn)高質(zhì)量音頻轉(zhuǎn)換
多線程處理,不阻塞UI界面
實時進度反饋,支持取消操作
軟件效果展示
主界面布局

轉(zhuǎn)換過程截圖

完成提示

開發(fā)步驟詳解
1. 環(huán)境準(zhǔn)備
# 必需依賴 pip install PyQt5 # FFmpeg需要單獨安裝 # Windows: 下載并添加至PATH # macOS: brew install ffmpeg # Linux: sudo apt install ffmpeg
2. 項目功能結(jié)構(gòu)設(shè)計

3. 核心類設(shè)計
AudioConverterThread (QThread)
處理音頻轉(zhuǎn)換的核心線程類,主要功能:
- 執(zhí)行FFmpeg命令
- 解析進度信息
- 處理文件刪除等后續(xù)操作
- 發(fā)送進度信號更新UI
AudioConverterApp (QMainWindow)
主窗口類,負(fù)責(zé):
- 用戶界面構(gòu)建
- 事件處理
- 線程管理
- 狀態(tài)更新
4. 關(guān)鍵技術(shù)實現(xiàn)
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()
# 處理文件或文件夾
代碼深度解析
1. 多線程架構(gòu)設(shè)計
音頻轉(zhuǎn)換是耗時操作,必須使用多線程避免界面凍結(jié)。我們繼承QThread創(chuàng)建專門的工作線程:
class AudioConverterThread(QThread):
progress_updated = pyqtSignal(int, str, str) # 信號定義
conversion_finished = pyqtSignal(str, bool, str)
def run(self):
# 轉(zhuǎn)換邏輯實現(xiàn)
for input_file in self.input_files:
# 構(gòu)建FFmpeg命令
cmd = ['ffmpeg', '-i', input_file, '-y']
# ...參數(shù)設(shè)置...
# 執(zhí)行轉(zhuǎn)換
process = subprocess.Popen(cmd, stderr=subprocess.PIPE)
# 進度解析循環(huán)
while True:
line = process.stderr.readline()
# ...解析進度...
self.progress_updated.emit(progress, message, filename)2. 音質(zhì)參數(shù)系統(tǒng)
提供預(yù)設(shè)和自定義兩種參數(shù)設(shè)置方式:
def set_quality_preset(self):
"""根據(jù)預(yù)設(shè)設(shè)置默認(rèn)參數(shù)"""
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
3. 文件大小預(yù)估算法
根據(jù)音頻時長和編碼參數(shù)預(yù)估輸出大?。?/p>
def estimate_sizes(self):
# 對于WAV格式,大小與時長和采樣率成正比
if self.output_format == "wav":
estimated_size = input_size * 1.2 # 粗略估計
else:
# 對于有損壓縮格式,大小=比特率×?xí)r長
duration = self.get_audio_duration(input_file)
estimated_size = (self.bitrate * 1000 * duration) / 8 # bit to bytes
4. UI美化技巧
使用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
# 根據(jù)品質(zhì)預(yù)設(shè)設(shè)置默認(rèn)參數(shù)
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)
# 如果是預(yù)估模式
if self.estimate_only:
# 簡化的預(yù)估算法 (實際大小會因編碼效率而異)
if self.output_format == "wav":
# WAV通常是未壓縮的,大小與采樣率/位深相關(guān)
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
# 構(gòu)建FFmpeg命令
cmd = ['ffmpeg', '-i', input_file, '-y'] # -y 覆蓋已存在文件
# 添加音頻參數(shù)
if self.bitrate:
cmd.extend(['-b:a', f'{self.bitrate}k'])
if self.samplerate:
cmd.extend(['-ar', str(self.samplerate)])
cmd.append(output_file)
# 執(zhí)行轉(zhuǎn)換
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 # 默認(rèn)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("音頻格式轉(zhuǎn)換工具")
self.setGeometry(100, 100, 900, 700)
self.setWindowIcon(QIcon.fromTheme("multimedia-volume-control"))
# 初始化變量
self.input_files = []
self.output_dir = ""
self.converter_thread = None
# 設(shè)置樣式
self.setup_ui_style()
# 初始化UI
self.init_ui()
self.setAcceptDrops(True)
def setup_ui_style(self):
# 設(shè)置應(yīng)用程序樣式
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;
}
""")
# 設(shè)置調(diào)色板
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)
# 標(biāo)題
title_label = QLabel("?? 音頻格式轉(zhuǎn)換工具")
title_label.setObjectName("titleLabel")
title_label.setFont(QFont("Arial", 18, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title_label)
# 文件選擇區(qū)域
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)
# 輸出設(shè)置區(qū)域
output_group = QGroupBox("?? 輸出設(shè)置")
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("(默認(rèn): 原文件目錄)")
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("??? 轉(zhuǎn)換后刪除原文件")
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)
# 音質(zhì)設(shè)置區(qū)域
quality_group = QGroupBox("??? 音質(zhì)設(shè)置")
quality_layout = QHBoxLayout()
# 音質(zhì)預(yù)設(shè)
preset_layout = QVBoxLayout()
preset_label = QLabel("?? 音質(zhì)預(yù)設(shè):")
self.quality_group = QButtonGroup()
self.high_quality_rb = QRadioButton("?? 高質(zhì)量 (320kbps, 48kHz)")
self.medium_quality_rb = QRadioButton("?? 中等質(zhì)量 (192kbps, 44.1kHz)")
self.low_quality_rb = QRadioButton("?? 低質(zhì)量 (128kbps, 22kHz)")
self.custom_quality_rb = QRadioButton("?? 自定義參數(shù)")
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)
# 自定義參數(shù)
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)
# 文件大小預(yù)估區(qū)域
size_group = QGroupBox("?? 文件大小預(yù)估")
size_layout = QVBoxLayout()
self.size_label = QLabel("?? 添加文件后自動預(yù)估輸出大小")
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)
# 轉(zhuǎn)換按鈕
button_layout = QHBoxLayout()
self.convert_btn = QPushButton("? 開始轉(zhuǎn)換")
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)
# 狀態(tài)欄
self.statusBar().showMessage("?? 準(zhǔn)備就緒")
# 初始化UI狀態(tài)
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("?? 添加文件后自動預(yù)估輸出大小")
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"輸出目錄設(shè)置為: {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):
"""根據(jù)選擇的音質(zhì)預(yù)設(shè)更新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):
"""預(yù)估輸出文件大小"""
if not self.input_files:
self.size_label.setText("?? 請先添加要轉(zhuǎn)換的文件")
return
output_format = self.format_combo.currentText()
# 如果沒有指定輸出目錄,使用原文件目錄
output_dir = self.output_dir if self.output_dir else os.path.dirname(self.input_files[0])
# 獲取當(dāng)前選擇的音質(zhì)預(yù)設(shè)
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"
# 創(chuàng)建估算線程
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):
"""更新大小預(yù)估顯示"""
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"?? 預(yù)估輸出大小:\n"
f"輸入總大小: {self.format_size(total_input)}\n"
f"預(yù)估輸出總大小: {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("?? 請先添加要轉(zhuǎn)換的文件", 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("(使用原文件目錄)")
# 獲取音質(zhì)預(yù)設(shè)
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"
# 獲取其他參數(shù)
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)
# 創(chuàng)建并啟動轉(zhuǎn)換線程
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("?? 開始轉(zhuǎn)換文件...")
def cancel_conversion(self):
if self.converter_thread and self.converter_thread.isRunning():
self.converter_thread.canceled = True
self.update_status("?? 正在取消轉(zhuǎn)換...")
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("?? 轉(zhuǎn)換已取消", is_error=True)
else:
self.update_status("?? 所有文件轉(zhuǎn)換完成!")
# 重置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("?? 添加文件后自動預(yù)估輸出大小")
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, '轉(zhuǎn)換正在進行中',
"轉(zhuǎn)換仍在進行中,確定要退出嗎?",
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并確保它在系統(tǒng)路徑中。\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_())
總結(jié)與展望
本文詳細(xì)介紹了基于PyQt5和FFmpeg的音頻轉(zhuǎn)換工具的開發(fā)全過程。通過這個項目,我們實現(xiàn)了:
- 現(xiàn)代化GUI界面:直觀易用的圖形界面,支持拖拽等便捷操作
- 高效轉(zhuǎn)換引擎:利用FFmpeg實現(xiàn)高質(zhì)量音頻轉(zhuǎn)換
- 良好的用戶體驗:進度顯示、預(yù)估系統(tǒng)、錯誤處理等細(xì)節(jié)完善
未來可能的改進方向:
- 添加音頻元數(shù)據(jù)編輯功能
- 支持更多音頻格式(如OPUS、WMA等)
- 實現(xiàn)音頻剪輯、合并等高級功能
- 增加云端轉(zhuǎn)換支持
到此這篇關(guān)于Python使用FFmpeg實現(xiàn)高效音頻格式轉(zhuǎn)換工具的文章就介紹到這了,更多相關(guān)Python FFmpeg音頻格式轉(zhuǎn)換內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python讀取excel文件中帶公式的值的實現(xiàn)
這篇文章主要介紹了Python讀取excel文件中帶公式的值的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04
Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法
這篇文章主要介紹了Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法,編寫一個函數(shù),其作用是將輸入的字符串反轉(zhuǎn)過來。下面文章關(guān)于其詳細(xì)介紹,需要的小伙伴可以參考一下2022-05-05

