Python使用FFmpeg实现高效音频格式转换工具

2025-05-31 03:50

本文主要是介绍Python使用FFmpeg实现高效音频格式转换工具,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Python使用FFmpeg实现高效音频格式转换工具》在数字音频处理领域,音频格式转换是一项基础但至关重要的功能,本文主要为大家介绍了Python如何使用FFmpeg实现强大功能的图形化音频转换工具...

概述

在数字音频处理领域,音频格式转换是一项基础但至关重要的功能。无论是音乐制作、播客编辑还是日常多媒体处理,我们经常需要在不同音频格式之间进行转换。本文介绍的全能音频转换大师是一款基于python PyQt5框架开发,结合FFmpeg强大功能的图形化音频转换工具。

相较于市面上其他转换工具,本程序具有以下显著优势:

  • 多格式支持:支持MP3、WAV、FLAC、AAC、OGG、M4A等主流音频格式互转
  • 智能音质预设:提供高中低三档音质预设及自定义参数选项
  • 批量处理:支持文件/文件夹批量导入,高效处理大量音频文件
  • 可视化进度:实时显示转换进度和详细状态信息
  • 智能预估:提前计算输出文件大小,合理规划存储空间
  • 跨平台:基于Python开发,可在WindowsMACOS、linux系统运行

功能详解

1. 文件管理功能

多种添加方式:支持文件添加、文件夹添加和拖拽添加三种方式

格式过滤:自动过滤非音频文件,确保输入文件有效性

列表管理:可查js看已添加文件列表,支持清空列表操作

2. 输出设置

输出格式选择:通过下拉框选择目标格式

输出目录设置:可指定输出目录,默认使用源文件目录

原文件处理:可选转换后删除原文件以节省空间

3. 音质控制

预设方案:

  • 高质量:320kbps比特率,48kHz采样率
  • 中等质量:192kbps比特率,44.1kHz采样率
  • 低质量:128kbps比特率,22kHz采样率

自定义参数:可自由设置比特率和采样率

4. 智能预估系统

基于文件时长和编码参数预估输出文件大小

计算压缩率,帮助用户做出合理决策

可视化显示输入输出总大小对比

5. 转换引擎

基于FFmpeg实现高质量音频转换

多线程处理,不阻塞UI界面

实时进度反馈,支持取消操作

软件效果展示

主界面布局

Python使用FFmpeg实现高效音频格式转换工具

转换过程截图

Python使用FFmpeg实现高效音频格式转换工具

完成提示

Python使用FFmpeg实现高效音频格式转换工具

开发步骤详解

1. 环境准备

# 必需依赖
pip install PyQt5
# FFmpeg需要单独安装
# Windows: 下载并添加至PATH
# macOS: brew install ffmpeg
# Linux: sudo apt install ffmpeg

2. 项目功能结构设计

Python使用FFmpeg实现高效音频格式转换工具

3. 核心类设计

AudioConverterThread (QThread)

处理音频转换的核心线程类,主要功能:

  • 执行FFmpeg命令
  • 解析进度信息
  • 处理文件删除等后续操作
  • 发送进度信号更新UI

AudioConverterApp (QMainWindow)

主窗口类,负责:

  • 用户界面构建
  • 事件处理
  • 线程管理
  • 状态更新

4. 关键技术实现

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. 多线程架构设计

音频转换是耗时操作,必须使用多线程避免界面冻结。我们继承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)

2. 音质参数系统

提供预设和自定义两种参数设置方式:

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

3. 文件大小预估算法

根据音频时长和编码参数预估输出大小:

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
        
        # 根据品质预设设置默认参数
        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.espythontimate_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("ℹ️ 添加文件js后自动预估输出大小")
        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(('.mppython3', '.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(Trjsue))
        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的音频转换工具的开发全过程。通过这个项目,我们实现了:

  • 现代化GUI界面:直观易用的图形界面,支持拖拽等便捷操作
  • 高效转换引擎:利用FFmpeg实现高质量音频转换
  • 良好的用户体验:进度显示、预估系统、错误处理等细节完善

未来可能的改进方向:

  • 添加音频元数据编辑功能
  • 支持更多音频格式(如OPUS、WMA等)
  • 实现音频剪辑、合并等高级功能
  • 增加云端转换支持

到此这篇关于Python使用FFmpeg实现高效音频格式转换工具的文章就介绍到这了,更多相关Python FFmpeg音频格式转换内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于Python使用FFmpeg实现高效音频格式转换工具的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154869

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置