亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Python使用FFmpeg實現(xiàn)高效音頻格式轉(zhuǎn)換工具

 更新時間:2025年05月30日 16:56:44   作者:創(chuàng)客白澤  
在數(shù)字音頻處理領域,音頻格式轉(zhuǎn)換是一項基礎但至關(guān)重要的功能,本文主要為大家介紹了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項目的五大SQL連接器

    適合Python項目的五大SQL連接器

    這篇文章,將要介紹當前流行的、適合大多數(shù)Python程序員的、五大Python SQL數(shù)據(jù)庫連接器,并討論它們安裝和各種的優(yōu)缺點。需要的噴朋友可以參考下面文章的具體內(nèi)容
    2021-09-09
  • 用Python編寫一個基于終端的實現(xiàn)翻譯的腳本

    用Python編寫一個基于終端的實現(xiàn)翻譯的腳本

    這篇文章主要介紹了用Python編寫一個基于終端的實現(xiàn)翻譯的腳本,代碼基于Python2.x,需要的朋友可以參考下
    2015-04-04
  • Python類多重繼承方式

    Python類多重繼承方式

    這篇文章主要介紹了Python類多重繼承方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • Python讀取excel文件中帶公式的值的實現(xiàn)

    Python讀取excel文件中帶公式的值的實現(xiàn)

    這篇文章主要介紹了Python讀取excel文件中帶公式的值的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-04-04
  • Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法

    Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法

    這篇文章主要介紹了Python中五種實現(xiàn)字符串反轉(zhuǎn)的方法,編寫一個函數(shù),其作用是將輸入的字符串反轉(zhuǎn)過來。下面文章關(guān)于其詳細介紹,需要的小伙伴可以參考一下
    2022-05-05
  • 淺談pytorch中的BN層的注意事項

    淺談pytorch中的BN層的注意事項

    這篇文章主要介紹了淺談pytorch中的BN層的注意事項,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-06-06
  • Python中replace方法實例分析

    Python中replace方法實例分析

    這篇文章主要介紹了Python中replace方法,有一定的實用價值,需要的朋友可以參考下
    2014-08-08
  • python分割列表(list)的方法示例

    python分割列表(list)的方法示例

    這篇文章主要給大家介紹了python分割列表(list)的方法,文中給出了詳細的示例代碼大家參考學習,對大家具有一定的參考學習價值,需要的朋友們下面來一起看看吧。
    2017-05-05
  • pandas的排序和排名的具體使用

    pandas的排序和排名的具體使用

    這篇文章主要介紹了pandas的排序和排名的具體使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-07-07
  • Python Scrapy框架第一個入門程序示例

    Python Scrapy框架第一個入門程序示例

    這篇文章主要介紹了Python Scrapy框架第一個入門程序,結(jié)合實例形式分析了Python Scrapy框架項目的搭建、抓取字段設置、數(shù)據(jù)庫保存等相關(guān)操作技巧,需要的朋友可以參考下
    2020-02-02

最新評論