Python使用FFmpeg實現(xiàn)高效音頻格式轉(zhuǎn)換工具
概述
在數(shù)字音頻處理領域,音頻格式轉(zhuǎn)換是一項基礎但至關(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ì)預設:提供高中低三檔音質(zhì)預設及自定義參數(shù)選項
- 批量處理:支持文件/文件夾批量導入,高效處理大量音頻文件
- 可視化進度:實時顯示轉(zhuǎn)換進度和詳細狀態(tài)信息
- 智能預估:提前計算輸出文件大小,合理規(guī)劃存儲空間
- 跨平臺:基于Python開發(fā),可在Windows、macOS、Linux系統(tǒng)運行
功能詳解
1. 文件管理功能
多種添加方式:支持文件添加、文件夾添加和拖拽添加三種方式
格式過濾:自動過濾非音頻文件,確保輸入文件有效性
列表管理:可查看已添加文件列表,支持清空列表操作
2. 輸出設置
輸出格式選擇:通過下拉框選擇目標格式
輸出目錄設置:可指定輸出目錄,默認使用源文件目錄
原文件處理:可選轉(zhuǎn)換后刪除原文件以節(jié)省空間
3. 音質(zhì)控制
預設方案:
- 高質(zhì)量:320kbps比特率,48kHz采樣率
- 中等質(zhì)量:192kbps比特率,44.1kHz采樣率
- 低質(zhì)量:128kbps比特率,22kHz采樣率
自定義參數(shù):可自由設置比特率和采樣率
4. 智能預估系統(tǒng)
基于文件時長和編碼參數(shù)預估輸出文件大小
計算壓縮率,幫助用戶做出合理決策
可視化顯示輸入輸出總大小對比
5. 轉(zhuǎn)換引擎
基于FFmpeg實現(xiàn)高質(zhì)量音頻轉(zhuǎn)換
多線程處理,不阻塞UI界面
實時進度反饋,支持取消操作
軟件效果展示
主界面布局
轉(zhuǎn)換過程截圖
完成提示
開發(fā)步驟詳解
1. 環(huán)境準備
# 必需依賴 pip install PyQt5 # FFmpeg需要單獨安裝 # Windows: 下載并添加至PATH # macOS: brew install ffmpeg # Linux: sudo apt install ffmpeg
2. 項目功能結(jié)構(gòu)設計
3. 核心類設計
AudioConverterThread (QThread)
處理音頻轉(zhuǎn)換的核心線程類,主要功能:
- 執(zhí)行FFmpeg命令
- 解析進度信息
- 處理文件刪除等后續(xù)操作
- 發(fā)送進度信號更新UI
AudioConverterApp (QMainWindow)
主窗口類,負責:
- 用戶界面構(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)設計
音頻轉(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ù)設置... # 執(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)
提供預設和自定義兩種參數(shù)設置方式:
def set_quality_preset(self): """根據(jù)預設設置默認參數(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. 文件大小預估算法
根據(jù)音頻時長和編碼參數(shù)預估輸出大?。?/p>
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
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ì)預設設置默認參數(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) # 如果是預估模式 if self.estimate_only: # 簡化的預估算法 (實際大小會因編碼效率而異) 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 # 默認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 # 設置樣式 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; } """) # 設置調(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) # 標題 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) # 輸出設置區(qū)域 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("??? 轉(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ì)設置區(qū)域 quality_group = QGroupBox("??? 音質(zhì)設置") quality_layout = QHBoxLayout() # 音質(zhì)預設 preset_layout = QVBoxLayout() preset_label = QLabel("?? 音質(zhì)預設:") 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) # 文件大小預估區(qū)域 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) # 轉(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("?? 準備就緒") # 初始化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("?? 添加文件后自動預估輸出大小") 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): """根據(jù)選擇的音質(zhì)預設更新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("?? 請先添加要轉(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]) # 獲取當前選擇的音質(zhì)預設 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): """更新大小預估顯示""" 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("?? 請先添加要轉(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ì)預設 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("?? 添加文件后自動預估輸出大小") 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é)與展望
本文詳細介紹了基于PyQt5和FFmpeg的音頻轉(zhuǎn)換工具的開發(fā)全過程。通過這個項目,我們實現(xiàn)了:
- 現(xiàn)代化GUI界面:直觀易用的圖形界面,支持拖拽等便捷操作
- 高效轉(zhuǎn)換引擎:利用FFmpeg實現(xiàn)高質(zhì)量音頻轉(zhuǎn)換
- 良好的用戶體驗:進度顯示、預估系統(tǒng)、錯誤處理等細節(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),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-04-04Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法
這篇文章主要介紹了Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法,編寫一個函數(shù),其作用是將輸入的字符串反轉(zhuǎn)過來。下面文章關(guān)于其詳細介紹,需要的小伙伴可以參考一下2022-05-05