【开源工具】PyQt6录音神器:高颜值多功能音频录制工具开发全解析

article/2025/7/29 6:04:41

【开源工具】🎙️ PyQt6录音神器:高颜值多功能音频录制工具开发全解析

请添加图片描述

🌈 个人主页:创客白泽 - CSDN博客
🔥 系列专栏:🐍《Python开源项目实战》
💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

请添加图片描述

在这里插入图片描述

📌 概述

在当今数字化时代,音频录制工具已经成为内容创作者、会议记录者和音乐爱好者的必备工具。本文将详细介绍如何使用Python的PyQt6库开发一款功能全面、界面美观的桌面录音工具。这款工具不仅支持常规麦克风输入,还能录制系统音频,并提供了丰富的设置选项和精美的用户界面。

本项目的核心特点:

  • 🎯 基于PyQt6的现代化UI设计
  • 🎤 支持麦克风和系统音频录制
  • ⏯️ 提供暂停/继续功能
  • ⚙️ 可配置的音频质量和保存格式
  • 📁 智能文件保存管理
  • 🗄️ 系统托盘支持后台运行

🛠️ 功能详解

1. 核心录音功能

  • 多设备支持:自动检测系统音频输入设备
  • 高精度计时:毫秒级录音时长显示
  • 状态管理:实时显示录制状态(录制中/暂停/停止)

2. 音频处理能力

  • 支持多种采样率(44.1kHz/48kHz/96kHz)
  • 可调位深度(16bit/24bit/32bit)
  • 多种输出格式(WAV/MP3/FLAC/OGG)

3. 用户体验优化

  • 系统托盘图标控制
  • 快捷键支持(Ctrl+R开始,Ctrl+P暂停,Ctrl+S停止)
  • 最小化到托盘选项
  • 音频设备自动刷新

🖥️ 界面展示效果

主界面布局

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

界面采用分页设计,分为"录音"和"设置"两大板块:

  1. 录音页面

    • 大尺寸计时器显示
    • 醒目的控制按钮
    • 设备选择区域
      在这里插入图片描述
  2. 设置页面

    • 保存路径配置
    • 音频质量设置
    • 其他偏好选项
      在这里插入图片描述

状态指示系统

  • 🟢 绿色:准备就绪
  • 🔴 红色:正在录制
  • 🟡 黄色:已暂停

🧩 软件实现步骤

1. 环境准备

pip install PyQt6 pyaudio

2. 项目结构设计

AudioRecorder/
│── main.py          # 程序入口
│── settings.ini     # 配置文件
│── recordings/      # 默认保存目录

3. 核心类架构

class AudioRecorder(QMainWindow):def __init__(self):# 初始化录音状态、UI和系统托盘passdef init_ui(self):# 创建主界面和分页passdef create_recording_tab(self):# 构建录音页面passdef create_settings_tab(self):# 构建设置页面pass

🔍 关键代码解析

1. 音频设备管理

def update_device_list(self):"""动态更新音频输入设备列表"""self.audio.terminate()self.audio = pyaudio.PyAudio()# 获取所有输入设备for i in range(self.audio.get_device_count()):device_info = self.audio.get_device_info_by_index(i)if device_info.get('maxInputChannels', 0) > 0:# 添加到下拉菜单pass

2. 录音控制逻辑

def start_recording(self):"""启动录音的核心逻辑"""self.stream = self.audio.open(format=self.format,channels=self.channels,rate=self.sample_rate,input=True,frames_per_buffer=self.chunk,input_device_index=device_index,stream_callback=self.audio_callback)self.timer.start(20)  # 50fps刷新

3. 音频数据回调

def audio_callback(self, in_data, frame_count, time_info, status):"""实时音频数据采集回调"""if self.is_recording and not self.is_paused:self.frames.append(in_data)return (in_data, pyaudio.paContinue)

4. 时间显示优化

def update_display_time(self):"""高精度时间显示(毫秒级)"""elapsed = (datetime.now().timestamp() - self.recording_start_time - self.paused_duration)# HTML格式化显示self.time_label.setText(f"<span style='font-size:28pt;'>{hours:02d}:{minutes:02d}:{seconds:02d}."f"<span style='font-size:20pt;'>{milliseconds:03d}</span></span>")

💾 文件保存机制

1. 智能路径管理

def save_recording(self, duration):"""处理文件保存逻辑"""save_dir = self.save_path_edit.text() or os.path.join(os.path.expanduser("~"), "Recordings")os.makedirs(save_dir, exist_ok=True)# 根据格式选择扩展名ext = "wav" if "WAV" in selected_format else "mp3"  # 其他格式类似

2. 临时文件处理

# 先保存为WAV再转换
temp_wav = os.path.join(save_dir, f"temp_recording.wav")
with wave.open(temp_wav, 'wb') as wf:wf.writeframes(b''.join(self.frames))# 格式转换处理(伪代码)
if ext != "wav":convert_audio(temp_wav, filename, ext)os.remove(temp_wav)

🚀 高级功能实现

1. 系统托盘集成

def init_system_tray(self):"""创建系统托盘图标和菜单"""self.tray_icon = QSystemTrayIcon(self)self.tray_menu = QMenu()# 添加菜单项actions = [("显示窗口", self.show_normal),("开始录制", self.start_recording),("退出", self.close)]# ... 添加到菜单

2. 设置持久化

def save_settings(self):"""使用QSettings保存配置"""self.settings = QSettings("AudioRecorder", "RecorderApp")self.settings.setValue("save_path", self.save_path_edit.text())self.settings.setValue("audio/format_index", self.format_combo.currentIndex())# ... 其他设置

3. 异常处理机制

try:self.stream = self.audio.open(...)
except Exception as e:QMessageBox.warning(self, "设备错误", f"无法打开音频流: {str(e)}")self.reset_recording_state()

📥 源码下载

import sys
import os
import pyaudio
import wave
from datetime import datetime
from PyQt6.QtCore import QSettings, Qt, QTimer, QSize, QElapsedTimer
from PyQt6.QtGui import (QIcon, QAction, QPixmap, QColor, QShortcut, QPainter, QFont)
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QComboBox, QLabel, QCheckBox, QSystemTrayIcon,QMenu, QMessageBox, QHBoxLayout, QStyle, QFrame,QTabWidget, QLineEdit, QFileDialog, QGroupBox)class AudioRecorder(QMainWindow):def __init__(self):super().__init__()# 初始化设置self.settings = QSettings("AudioRecorder", "RecorderApp")# 录音状态self.is_recording = Falseself.is_paused = Falseself.frames = []self.stream = Noneself.audio = pyaudio.PyAudio()self.recording_start_time = 0self.paused_duration = 0self.last_pause_time = 0# 初始化UIself.init_ui()# 初始化系统托盘self.init_system_tray()# 加载设置self.load_settings()# 更新设备列表self.update_device_list()# 设置窗口属性self.setWindowTitle("录音工具-BY 创客白泽")self.setWindowIcon(QIcon(self.create_icon_pixmap()))self.setMinimumSize(500, 400)def init_ui(self):# 主窗口布局central_widget = QWidget()self.setCentralWidget(central_widget)main_layout = QVBoxLayout(central_widget)main_layout.setContentsMargins(15, 15, 15, 15)main_layout.setSpacing(15)# 创建分页self.tabs = QTabWidget()main_layout.addWidget(self.tabs)# 创建录音分页self.create_recording_tab()# 创建设置分页self.create_settings_tab()# 添加快捷键self.setup_shortcuts()def create_recording_tab(self):"""创建录音分页"""recording_tab = QWidget()layout = QVBoxLayout(recording_tab)layout.setContentsMargins(10, 10, 10, 10)layout.setSpacing(15)# 时间显示组time_group = QGroupBox("录音时间")time_layout = QVBoxLayout(time_group)# 设置等宽字体用于计时器显示mono_font = QFont("Consolas" if sys.platform == "win32" else "Monospace")mono_font.setPointSize(24)# 录音时间显示 - 优化显示质量self.time_label = QLabel("00:00:00.000")self.time_label.setFont(mono_font)self.time_label.setStyleSheet("""QLabel {font-weight: bold;color: #E53935;qproperty-alignment: AlignCenter;padding: 10px;background-color: #FAFAFA;border-radius: 5px;border: 1px solid #E0E0E0;}""")time_layout.addWidget(self.time_label)layout.addWidget(time_group)# 状态显示self.status_label = QLabel("🟢 准备就绪")self.status_label.setStyleSheet("""font-size: 14px; qproperty-alignment: AlignCenter;padding: 5px;""")layout.addWidget(self.status_label)# 添加分隔线separator = QFrame()separator.setFrameShape(QFrame.Shape.HLine)separator.setFrameShadow(QFrame.Shadow.Sunken)layout.addWidget(separator)# 高精度计时器self.timer = QTimer(self)self.timer.setTimerType(Qt.TimerType.PreciseTimer)self.timer.timeout.connect(self.update_display_time)self.elapsed_timer = QElapsedTimer()# 按钮布局button_layout = QHBoxLayout()button_layout.setSpacing(10)# 开始录音按钮self.start_button = QPushButton("🎤 开始录制")self.start_button.setStyleSheet(self.get_button_style("#4CAF50"))self.start_button.clicked.connect(self.start_recording)button_layout.addWidget(self.start_button)# 暂停/继续按钮self.pause_button = QPushButton("⏸ 暂停")self.pause_button.setStyleSheet(self.get_button_style("#FFC107"))self.pause_button.clicked.connect(self.toggle_pause)self.pause_button.setEnabled(False)button_layout.addWidget(self.pause_button)# 停止录音按钮self.stop_button = QPushButton("🛑 停止并保存")self.stop_button.setStyleSheet(self.get_button_style("#F44336"))self.stop_button.clicked.connect(self.stop_recording)self.stop_button.setEnabled(False)button_layout.addWidget(self.stop_button)layout.addLayout(button_layout)# 添加分隔线separator = QFrame()separator.setFrameShape(QFrame.Shape.HLine)separator.setFrameShadow(QFrame.Shadow.Sunken)layout.addWidget(separator)# 设备设置区域device_group = QGroupBox("录音设置")device_layout = QVBoxLayout(device_group)# 系统音频录制选项self.system_audio_check = QCheckBox("录制系统音频")self.system_audio_check.setChecked(True)  # 默认启用系统音频录制device_layout.addWidget(self.system_audio_check)# 输入设备选择device_layout.addWidget(QLabel("🎧 输入设备:"))self.input_device_combo = QComboBox()self.input_device_combo.setStyleSheet("""QComboBox {padding: 5px;border: 1px solid #BDBDBD;border-radius: 3px;}""")device_layout.addWidget(self.input_device_combo)# 刷新设备按钮refresh_button = QPushButton("🔄 刷新设备列表")refresh_button.setStyleSheet(self.get_button_style("#2196F3"))refresh_button.clicked.connect(self.update_device_list)device_layout.addWidget(refresh_button)layout.addWidget(device_group)# 添加弹簧使内容顶部对齐layout.addStretch()self.tabs.addTab(recording_tab, "🎙️ 录音")def create_settings_tab(self):"""创建设置分页"""settings_tab = QWidget()layout = QVBoxLayout(settings_tab)layout.setContentsMargins(10, 10, 10, 10)layout.setSpacing(15)# 保存路径设置path_group = QGroupBox("保存设置")path_layout = QVBoxLayout(path_group)path_layout.addWidget(QLabel("📁 默认保存路径:"))# 路径选择和浏览按钮path_control_layout = QHBoxLayout()self.save_path_edit = QLineEdit()self.save_path_edit.setPlaceholderText("选择录音文件保存路径")path_control_layout.addWidget(self.save_path_edit)browse_button = QPushButton("浏览...")browse_button.setStyleSheet(self.get_button_style("#2196F3"))browse_button.clicked.connect(self.browse_save_path)path_control_layout.addWidget(browse_button)path_layout.addLayout(path_control_layout)# 添加文件格式选择path_layout.addWidget(QLabel("📄 保存格式:"))self.format_combo = QComboBox()self.format_combo.addItems(["WAV (无损)", "MP3 (高压缩)", "FLAC (无损压缩)", "OGG (开放格式)"])path_layout.addWidget(self.format_combo)layout.addWidget(path_group)# 音频质量设置quality_group = QGroupBox("音频质量")quality_layout = QVBoxLayout(quality_group)# 采样率设置sample_rate_layout = QHBoxLayout()sample_rate_layout.addWidget(QLabel("采样率:"))self.sample_rate_combo = QComboBox()self.sample_rate_combo.addItems(["44100 Hz (CD质量)", "48000 Hz (专业音频)", "96000 Hz (高清音频)"])sample_rate_layout.addWidget(self.sample_rate_combo)quality_layout.addLayout(sample_rate_layout)# 位深度设置bit_depth_layout = QHBoxLayout()bit_depth_layout.addWidget(QLabel("位深度:"))self.bit_depth_combo = QComboBox()self.bit_depth_combo.addItems(["16 bit (标准)", "24 bit (高精度)", "32 bit (专业级)"])bit_depth_layout.addWidget(self.bit_depth_combo)quality_layout.addLayout(bit_depth_layout)# MP3质量设置 (仅在MP3格式选中时显示)self.mp3_quality_layout = QHBoxLayout()self.mp3_quality_layout.addWidget(QLabel("MP3质量:"))self.mp3_quality_combo = QComboBox()self.mp3_quality_combo.addItems(["128 kbps (标准)", "192 kbps (高质量)", "256 kbps (极高)", "320 kbps (最佳)"])self.mp3_quality_layout.addWidget(self.mp3_quality_combo)quality_layout.addLayout(self.mp3_quality_layout)# 根据格式选择显示/隐藏MP3质量设置self.format_combo.currentIndexChanged.connect(self.update_format_settings_visibility)self.update_format_settings_visibility()layout.addWidget(quality_group)# 其他设置other_group = QGroupBox("其他设置")other_layout = QVBoxLayout(other_group)# 最小化到托盘self.minimize_to_tray_check = QCheckBox("最小化到系统托盘")other_layout.addWidget(self.minimize_to_tray_check)# 开机自启动self.auto_start_check = QCheckBox("开机自动启动")other_layout.addWidget(self.auto_start_check)layout.addWidget(other_group)# 添加弹簧使设置内容顶部对齐layout.addStretch()# 保存设置按钮save_settings_button = QPushButton("💾 保存设置")save_settings_button.setStyleSheet(self.get_button_style("#4CAF50"))save_settings_button.clicked.connect(self.save_settings)layout.addWidget(save_settings_button)self.tabs.addTab(settings_tab, "⚙️ 设置")def update_format_settings_visibility(self):"""根据选择的格式更新设置可见性"""selected_format = self.format_combo.currentText()is_mp3 = "MP3" in selected_format# 显示/隐藏MP3质量设置for i in range(self.mp3_quality_layout.count()):widget = self.mp3_quality_layout.itemAt(i).widget()if widget:widget.setVisible(is_mp3)def browse_save_path(self):"""浏览保存路径"""path = QFileDialog.getExistingDirectory(self,"选择保存目录",self.save_path_edit.text() or os.path.expanduser("~"))if path:self.save_path_edit.setText(path)def get_button_style(self, color):return f"""QPushButton {{background-color: {color};color: white;border: none;padding: 8px 12px;font-size: 14px;border-radius: 4px;min-width: 80px;}}QPushButton:hover {{background-color: {self.darken_color(color)};}}QPushButton:disabled {{background-color: #cccccc;}}"""def darken_color(self, hex_color, factor=0.8):"""使颜色变暗"""color = QColor(hex_color)return color.darker(int(100 + (100 - 100 * factor))).name()def create_icon_pixmap(self):"""创建应用图标"""pixmap = QPixmap(64, 64)pixmap.fill(Qt.GlobalColor.transparent)painter = QPainter(pixmap)painter.setRenderHint(QPainter.RenderHint.Antialiasing)painter.setBrush(QColor("#4285F4"))painter.setPen(Qt.PenStyle.NoPen)painter.drawEllipse(12, 12, 40, 40)painter.setBrush(Qt.GlobalColor.white)painter.drawEllipse(22, 22, 20, 20)painter.drawRect(28, 42, 8, 10)painter.end()return pixmapdef setup_shortcuts(self):"""设置快捷键"""QShortcut("Ctrl+R", self, self.start_recording)QShortcut("Ctrl+P", self, self.toggle_pause)QShortcut("Ctrl+S", self, self.stop_recording)def init_system_tray(self):"""初始化系统托盘"""self.tray_icon = QSystemTrayIcon(self)self.tray_menu = QMenu()actions = [("🪟 显示窗口", self.show_normal),("🎤 开始录制", self.start_recording),("🛑 停止录制", self.stop_recording),("🚪 退出", self.close)]for text, callback in actions:action = QAction(text, self)action.triggered.connect(callback)self.tray_menu.addAction(action)self.tray_icon.setContextMenu(self.tray_menu)self.tray_icon.setIcon(QIcon(self.create_icon_pixmap()))self.tray_icon.show()self.tray_icon.activated.connect(self.tray_icon_clicked)def tray_icon_clicked(self, reason):"""托盘图标点击事件处理"""if reason == QSystemTrayIcon.ActivationReason.Trigger:if self.isHidden():self.show_normal()else:self.hide()def show_normal(self):"""正常显示窗口"""self.show()self.setWindowState(self.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive)self.activateWindow()def update_device_list(self):"""更新输入设备列表,优化搜索并避免重复设备"""self.input_device_combo.clear()try:# 重新初始化PyAudio对象,确保获取最新的设备列表if hasattr(self, 'audio'):self.audio.terminate()self.audio = pyaudio.PyAudio()# 获取所有音频设备count = self.audio.get_device_count()unique_devices = set()  # 用于跟踪已添加的设备default_input_index = self.audio.get_default_input_device_info().get('index', -1)for i in range(count):try:device_info = self.audio.get_device_info_by_index(i)if device_info.get('maxInputChannels', 0) > 0:device_name = device_info.get('name', 'Unknown Device')device_channels = device_info.get('maxInputChannels', 1)# 标准化设备名称(去除多余空格和特殊字符)normalized_name = ' '.join(device_name.strip().split())# 检查是否已经添加过这个设备device_key = f"{normalized_name}_{device_channels}"if device_key not in unique_devices:unique_devices.add(device_key)# 添加设备到下拉列表display_name = f"{normalized_name} (Ch:{device_channels})"self.input_device_combo.addItem(display_name, i)# 如果是默认输入设备,设置为当前选择if i == default_input_index:self.input_device_combo.setCurrentIndex(self.input_device_combo.count() - 1)except Exception as e:print(f"Error getting device info for index {i}: {str(e)}")continue# 如果没有找到任何设备,添加一个默认选项if self.input_device_combo.count() == 0:self.input_device_combo.addItem("未找到输入设备", -1)QMessageBox.warning(self, "设备错误", "未找到可用的音频输入设备")except Exception as e:print(f"Error updating device list: {str(e)}")QMessageBox.warning(self, "设备错误", f"无法获取音频设备列表: {str(e)}")# 添加一个默认选项self.input_device_combo.addItem("默认设备", 0)def start_recording(self):"""开始录音"""if self.is_recording:returntry:# 检查设备是否有效device_index = self.input_device_combo.currentData()if device_index == -1:QMessageBox.warning(self, "设备错误", "请选择有效的输入设备")return# 重置状态self.is_recording = Trueself.is_paused = Falseself.frames = []self.paused_duration = 0self.last_pause_time = 0# 获取设备参数try:device_info = self.audio.get_device_info_by_index(device_index)except Exception as e:QMessageBox.warning(self, "设备错误", f"无法获取设备信息: {str(e)}")self.reset_recording_state()return# 设置音频参数sample_rate_text = self.sample_rate_combo.currentText()self.sample_rate = int(sample_rate_text.split()[0])self.channels = min(2, device_info.get('maxInputChannels', 1))self.format = pyaudio.paInt16self.chunk = 1024# 尝试打开音频流try:if self.system_audio_check.isChecked():# 尝试使用WASAPI loopback模式录制系统音频try:self.stream = self.audio.open(format=self.format,channels=self.channels,rate=self.sample_rate,input=True,frames_per_buffer=self.chunk,input_device_index=device_index,stream_callback=self.audio_callback,as_loopback=True)except:# 如果WASAPI loopback失败,尝试普通模式self.stream = self.audio.open(format=self.format,channels=self.channels,rate=self.sample_rate,input=True,frames_per_buffer=self.chunk,input_device_index=device_index,stream_callback=self.audio_callback)else:# 普通麦克风录音self.stream = self.audio.open(format=self.format,channels=self.channels,rate=self.sample_rate,input=True,frames_per_buffer=self.chunk,input_device_index=device_index,stream_callback=self.audio_callback)except Exception as e:QMessageBox.warning(self, "录音错误", f"无法开始录音: {str(e)}\n请检查设备是否被其他程序占用或尝试选择其他设备。")self.reset_recording_state()return# 启动计时器self.recording_start_time = datetime.now().timestamp()self.elapsed_timer.start()self.timer.start(20)  # 50fps刷新率# 更新UIself.status_label.setText("🔴 正在录制...")self.start_button.setEnabled(False)self.pause_button.setEnabled(True)self.stop_button.setEnabled(True)self.tray_icon.setIcon(QIcon(self.create_recording_icon_pixmap()))except Exception as e:self.is_recording = FalseQMessageBox.critical(self, "录音错误", f"无法开始录音: {str(e)}")self.reset_recording_state()def audio_callback(self, in_data, frame_count, time_info, status):"""音频回调函数,确保实时采集"""if self.is_recording and not self.is_paused:self.frames.append(in_data)return (in_data, pyaudio.paContinue)def create_recording_icon_pixmap(self):"""创建录音状态图标"""pixmap = QPixmap(64, 64)pixmap.fill(Qt.GlobalColor.transparent)painter = QPainter(pixmap)painter.setRenderHint(QPainter.RenderHint.Antialiasing)painter.setBrush(QColor("#F44336"))painter.setPen(Qt.PenStyle.NoPen)painter.drawEllipse(12, 12, 40, 40)painter.setBrush(Qt.GlobalColor.white)painter.drawEllipse(22, 22, 20, 20)painter.drawRect(28, 42, 8, 10)painter.end()return pixmapdef toggle_pause(self):"""暂停/继续录音"""if not self.is_recording:returnif self.is_paused:# 继续录音self.is_paused = Falseself.paused_duration += (datetime.now().timestamp() - self.last_pause_time)self.status_label.setText("🔴 正在录制...")self.pause_button.setText("⏸ 暂停")self.elapsed_timer.start()  # 重新开始计时else:# 暂停录音self.is_paused = Trueself.last_pause_time = datetime.now().timestamp()self.status_label.setText("🟡 已暂停")self.pause_button.setText("▶ 继续")self.elapsed_timer.invalidate()  # 停止计时def update_display_time(self):"""更新显示的时间 - 优化显示质量"""if self.is_recording:if self.is_paused:# 暂停状态下显示已记录的时间elapsed = self.last_pause_time - self.recording_start_time - self.paused_durationelse:# 运行状态下计算精确时间elapsed = (datetime.now().timestamp() - self.recording_start_time - self.paused_duration)# 格式化时间显示hours, remainder = divmod(int(elapsed), 3600)minutes, seconds = divmod(remainder, 60)milliseconds = int((elapsed - int(elapsed)) * 1000)# 使用HTML格式优化显示质量self.time_label.setText(f"<html><head/><body>"f"<span style='font-size:28pt; font-weight:bold; color:#E53935;'>"f"{hours:02d}:{minutes:02d}:{seconds:02d}.<span style='font-size:20pt;'>{milliseconds:03d}</span>"f"</span></body></html>")def stop_recording(self):"""停止录音并保存"""if not self.is_recording:returnself.is_recording = Falseself.timer.stop()try:# 停止音频流if self.stream:self.stream.stop_stream()self.stream.close()# 计算实际录制时长actual_duration = (datetime.now().timestamp() - self.recording_start_time - self.paused_duration)# 保存文件self.save_recording(actual_duration)except Exception as e:QMessageBox.warning(self, "保存错误", f"保存录音时出错: {str(e)}")finally:self.reset_recording_state()def save_recording(self, duration):"""保存录音文件"""if not self.frames:QMessageBox.warning(self, "保存错误", "没有录音数据可保存")returntry:# 获取保存路径save_dir = self.save_path_edit.text() or os.path.join(os.path.expanduser("~"), "Recordings")os.makedirs(save_dir, exist_ok=True)# 生成文件名timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")selected_format = self.format_combo.currentText()# 根据选择的格式确定文件扩展名if "MP3" in selected_format:ext = "mp3"elif "FLAC" in selected_format:ext = "flac"elif "OGG" in selected_format:ext = "ogg"else:  # 默认为WAVext = "wav"filename = os.path.join(save_dir, f"recording_{timestamp}.{ext}")# 计算实际音频数据时长total_bytes = len(b''.join(self.frames))calculated_duration = total_bytes / (self.sample_rate * self.channels * 2)  # 16-bit = 2字节# 首先保存为WAV文件temp_wav = os.path.join(save_dir, f"temp_recording_{timestamp}.wav")with wave.open(temp_wav, 'wb') as wf:wf.setnchannels(self.channels)wf.setsampwidth(2)  # 16-bitwf.setframerate(self.sample_rate)wf.writeframes(b''.join(self.frames))# 根据选择的格式进行转换if ext != "wav":try:# 这里应该添加实际的音频格式转换代码# 例如使用pydub或其他音频处理库# 由于代码示例中未包含实际转换逻辑,这里只是模拟import shutilshutil.copy(temp_wav, filename)os.remove(temp_wav)except Exception as e:# 如果转换失败,保留WAV文件os.rename(temp_wav, filename)QMessageBox.warning(self, "格式转换", f"无法转换为{ext.upper()}格式,已保存为WAV文件: {str(e)}")# 显示保存信息QMessageBox.information(self, "保存成功", f"录音已保存到:\n{filename}\n"f"格式: {selected_format.split()[0]}\n"f"计时器时长: {duration:.3f}秒\n"f"音频数据时长: {calculated_duration:.3f}秒\n"f"采样率: {self.sample_rate}Hz\n"f"声道数: {self.channels}")except Exception as e:raise Exception(f"保存录音文件时出错: {str(e)}")def reset_recording_state(self):"""重置录音状态"""self.status_label.setText("🟢 准备就绪")self.time_label.setText("00:00:00.000")self.start_button.setEnabled(True)self.pause_button.setEnabled(False)self.stop_button.setEnabled(False)self.pause_button.setText("⏸ 暂停")self.tray_icon.setIcon(QIcon(self.create_icon_pixmap()))def load_settings(self):"""加载设置"""# 加载保存路径default_path = os.path.join(os.path.expanduser("~"), "Recordings")self.save_path_edit.setText(self.settings.value("save_path", default_path))# 加载文件格式设置format_index = self.settings.value("audio/format_index", 0, type=int)if 0 <= format_index < self.format_combo.count():self.format_combo.setCurrentIndex(format_index)# 加载MP3质量设置mp3_quality_index = self.settings.value("audio/mp3_quality_index", 0, type=int)if 0 <= mp3_quality_index < self.mp3_quality_combo.count():self.mp3_quality_combo.setCurrentIndex(mp3_quality_index)# 加载采样率设置sample_rate_index = self.settings.value("audio/sample_rate_index", 0, type=int)if 0 <= sample_rate_index < self.sample_rate_combo.count():self.sample_rate_combo.setCurrentIndex(sample_rate_index)# 加载位深度设置bit_depth_index = self.settings.value("audio/bit_depth_index", 0, type=int)if 0 <= bit_depth_index < self.bit_depth_combo.count():self.bit_depth_combo.setCurrentIndex(bit_depth_index)# 加载其他设置self.minimize_to_tray_check.setChecked(self.settings.value("ui/minimize_to_tray", True, type=bool))self.auto_start_check.setChecked(self.settings.value("ui/auto_start", False, type=bool))# 加载系统音频录制设置self.system_audio_check.setChecked(self.settings.value("audio/system_audio", True, type=bool)  # 默认启用系统音频录制)def save_settings(self):"""保存设置"""# 保存路径设置self.settings.setValue("save_path", self.save_path_edit.text())# 保存音频设置self.settings.setValue("audio/format_index", self.format_combo.currentIndex())self.settings.setValue("audio/mp3_quality_index", self.mp3_quality_combo.currentIndex())self.settings.setValue("audio/device_index", self.input_device_combo.currentData())self.settings.setValue("audio/sample_rate_index", self.sample_rate_combo.currentIndex())self.settings.setValue("audio/bit_depth_index", self.bit_depth_combo.currentIndex())self.settings.setValue("audio/system_audio", self.system_audio_check.isChecked())# 保存其他设置self.settings.setValue("ui/minimize_to_tray", self.minimize_to_tray_check.isChecked())self.settings.setValue("ui/auto_start", self.auto_start_check.isChecked())QMessageBox.information(self, "设置保存", "设置已成功保存!")def closeEvent(self, event):"""关闭事件处理"""if self.is_recording:reply = QMessageBox.question(self, '正在录音',"当前正在录音,确定要退出吗?",QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,QMessageBox.StandardButton.No)if reply == QMessageBox.StandardButton.No:event.ignore()returnself.save_settings()if self.minimize_to_tray_check.isChecked():self.hide()event.ignore()else:if self.stream:self.stream.stop_stream()self.stream.close()self.audio.terminate()event.accept()if __name__ == "__main__":app = QApplication(sys.argv)app.setStyle("Fusion")# 设置应用程序字体font = app.font()font.setPointSize(10)app.setFont(font)recorder = AudioRecorder()recorder.show()sys.exit(app.exec())

🎯 开发难点与解决方案

  1. 设备兼容性问题
    • 问题:不同系统音频设备API差异
    • 方案:使用PyAudio的跨平台抽象层
  2. 精确计时挑战
    • 问题:系统时钟不精确
    • 方案:结合QElapsedTimer和实际音频帧数计算
  3. 格式转换实现
    • 问题:原生Python缺乏高效音频编码库
    • 方案:可扩展为调用FFmpeg等外部工具
  4. UI性能优化
    • 问题:频繁更新导致界面卡顿
    • 方案:使用HTML格式化文本减少重绘

🔮 未来扩展方向

  1. 音频编辑功能
    • 添加简单的剪切、合并功能
    • 支持添加标记点
  2. 云存储集成
    • 自动上传到Google Drive/OneDrive
  3. AI增强
    • 自动降噪
    • 语音转文字
  4. 多平台支持
    • 打包为Windows/macOS原生应用
    • 开发移动端版本

📝 总结

本文详细介绍了如何使用PyQt6开发功能完善的音频录制工具。通过这个项目,我们不仅学习了:

  1. PyQt6的现代化UI开发技巧
  2. PyAudio的音频采集和处理
  3. 系统托盘集成等高级功能
  4. 健壮的错误处理机制

这个项目展示了Python在多媒体应用开发中的强大能力,代码结构清晰,易于扩展,是学习GUI编程和音频处理的优秀范例。

开发心得

“好的音频工具应该在精确性和用户体验之间找到平衡。通过这个项目,我深刻理解了实时系统开发中时间管理的重要性,以及如何通过巧妙的UI设计提升工具的专业感。”

希望本文能帮助读者掌握桌面音频应用的开发技巧,期待看到大家基于此项目的创新改进!


http://www.hkcw.cn/article/sbGPccKGCW.shtml

相关文章

在PPT中同时自动播放多个视频的方法

在PPT中同时自动播放多个视频的方法 文章目录 在PPT中同时自动播放多个视频的方法1 准备视频2 设置动画为“出现”3 设置所有视频为“自动播放”4 最终效果与其他设置 在PPT制作的过程中&#xff0c;我们经常遇到需要同时自动播放多个视频的情况。本文将详细介绍实现这种效果的…

【智能驱蚊黑科技】基于OpenCV的蚊子雷达追踪打击系统(附完整Python源码)

【智能驱蚊黑科技】基于OpenCV的蚊子雷达追踪打击系统&#xff08;附完整Python源码&#xff09; &#x1f308; 个人主页&#xff1a;创客白泽 - CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f40d;《Python开源项目实战》 &#x1f4a1; 热爱不止于代码&#xff0c;热…

打造沉浸式古诗欣赏页面:HTML5视频背景与音频的完美结合

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

Python - 爬虫;Scrapy框架之插件Extensions(四)

阅读本文前先参考 https://blog.csdn.net/MinggeQingchun/article/details/145904572 在 Scrapy 中&#xff0c;扩展&#xff08;Extensions&#xff09;是一种插件&#xff0c;允许你添加额外的功能到你的爬虫项目中。这些扩展可以在项目的不同阶段执行&#xff0c;比如启动…

vscode 连接远程服务器

文章目录 1. 背景2. vscode 连接 服务器步骤2.1 安装 remote-ssh 插件2.2 配置 ssh 秘钥2.3 连接 server vscode 连接远程服务器 1. 背景 有服务器的同学&#xff0c;或许都有这样的感觉&#xff0c;服务器是 linux 系统&#xff0c;且只给个人提供一个终端进行连接&#xff0c…

JavaScript 模块系统:CJS/AMD/UMD/ESM

文章目录 前言一、CommonJS (CJS) - Node.js 的同步模块系统1.1 设计背景1.2 浏览器兼容性问题1.3 Webpack 如何转换 CJS1.4 适用场景 二、AMD (Asynchronous Module Definition) - 浏览器异步加载方案2.1 设计背景2.2 为什么现代浏览器不原生支持 AMD2.3 Webpack/Rollup 如何处…

乌称摧毁34%俄远程机队 俄媒否认 谎言蛛网行动

俄罗斯“与假新闻作战”网站发布文章称,通过分析乌克兰方面发布的视频可以确认,乌总统泽连斯基关于“已摧毁34%俄罗斯远程机队”的说法并不属实。俄方认为,乌克兰实际上可能仅摧毁了两架图-95战略轰炸机及一架安-12运输机,其余受损飞机在维修后均可恢复作战能力。乌克兰国家…

加沙停火协议为何一波三折 美斡旋遇阻

本周,美国就巴勒斯坦伊斯兰抵抗运动(哈马斯)和以色列的停火展开斡旋,提出一项为期60天的加沙地带停火方案。然而,围绕是否接受这份方案,哈马斯和以色列的态度不一,谈判频频出现变数。美国白宫5月29日表示,以色列已接受并签署美国提出的加沙地带临时停火方案。但该方案在…

基于springboot的宠物领养系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

中国王朝简史

文章目录 一、先秦时期&#xff1a;文明起点与制度雏形夏&#xff08;约前2070年–前1600年&#xff09;商&#xff08;约前1600年–前1046年&#xff09;周&#xff08;前1046年–前256年&#xff09; 二、大一统帝国的试验与成熟秦&#xff08;前221年–前207年&#xff09;汉…

Freefilesync配置windows与windows,windows与linux之间同步

说明 Freefilesync&#xff1a;用于windows与windows&#xff0c;windows与linux之间同步 linux 之间同步&#xff0c;使用系统的自带的 corn 软件&#xff0c;执行 sync 命名的脚本即可 一 、下载Freefilesync windows服务器上打开官网 https://freefilesync.org/&#xff0…

数字创新智慧园区建设及运维方案

该文档是 “数字创新智慧园区” 建设及运维方案,指出传统产业园区存在管理粗放等问题,“数字创新园区” 通过大数据、AI、物联网、云计算等数字化技术,旨在提升园区产业服务、运营管理水平,增强竞争力,实现绿色节能、高效管理等目标。建设内容包括智能设施、核心支撑平台、…

P1541 [NOIP 2010 提高组] 乌龟棋

P1541 [NOIP 2010 提高组] 乌龟棋 - 洛谷 题目背景 NOIP2010 提高组 T2 题目描述 小明过生日的时候&#xff0c;爸爸送给他一副乌龟棋当作礼物。 乌龟棋的棋盘是一行 N 个格子&#xff0c;每个格子上一个分数&#xff08;非负整数&#xff09;。棋盘第 1 格是唯一的起点&a…

设计模式——享元设计模式(结构型)

摘要 享元设计模式是一种结构型设计模式&#xff0c;旨在通过共享对象减少内存占用和提升性能。其核心思想是将对象状态分为内部状态&#xff08;可共享&#xff09;和外部状态&#xff08;不可共享&#xff09;&#xff0c;并通过享元工厂管理共享对象池。享元模式包含抽象享…

Qt OpenGL编程常用类

Qt提供了丰富的类来支持OpenGL编程&#xff0c;以下是常用的Qt OpenGL相关类&#xff1a; 一、QOpenGLWidget 功能&#xff1a;用于在 Qt 应用程序中嵌入 OpenGL 渲染的窗口部件。替代了旧版的QGLWidget。提供了OpenGL上下文和渲染表面。 继承关系&#xff1a;QWidget → QOp…

【JMeter】性能测试知识和工具

目录 何为系统性能 何为性能测试 性能测试分类 性能测试指标 性能测试流程 性能测试工具&#xff1a;JMeter&#xff08;主测web应用&#xff09; jmeter文件目录 启动方式 基本元件&#xff1a;元件内有很多组件 jmeter参数化 jmeter关联 自动录制脚本 直连数据库…

[Linux] nginx源码编译安装

初次学习&#xff0c;如有错误欢迎指正 目录 环境包部署 创建程序用户 软件包压缩 配置 编译 安装 建立快捷启动 启动nginx&#xff1f; 防火墙管理 查看规则 清空规则 关闭服务 开启服务 查看状态 开机自启 开机禁用 查看开机启动状态 nginx&#xff0c;启…

Spring AI Image Model、TTS,RAG

文章目录 Spring AI Alibaba聊天模型图像模型Image Model API接口及相关类实现生成图像 语音模型Text-to-Speech API概述实现文本转语音 实现RAG向量化RAGRAG工作流程概述实现基本 RAG 流程 Spring AI Alibaba Spring AI Alibaba实现了与阿里云通义模型的完整适配&#xff0c;…

多地机关食堂端午假期向社会开放 特色套餐迎客来

端午假期期间,全国多地政府机关食堂面向社会公众开放。5月31日中午,荣昌区政府机关食堂如约向游客开放,首日第一餐吸引了超过3000名游客前来体验。荣昌区特别推出了61元的“六一”家庭套餐,包含荣昌卤鹅、猪油泡粑、黄凉粉等特色菜品,还新增了粽子和儿童喜欢的薯条、鸡腿、…

韩国大选“5选1”投票将启 三强格局形成

6月3日,韩国将迎来新一届总统选举。最初有7名候选人登记参选,但截至6月2日,已有两名候选人宣布退出,形成了“5选1”的局面。目前李在明、金文洙和李俊锡基本形成三强格局。4名韩国前总统也各自进行着“路演”,通过各种方式表达对各自阵营候选人的支持。尹锡悦5月31日表态支…