【开源工具】🎙️ 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. 环境准备
pip install PyQt6 pyaudio
2. 项目结构设计
AudioRecorder/
│── main.py # 程序入口
│── settings.ini # 配置文件
│── recordings/ # 默认保存目录
3. 核心类架构
class AudioRecorder(QMainWindow):
def __init__(self):
# 初始化录音状态、UI和系统托盘
pass
def init_ui(self):
# 创建主界面和分页
pass
def create_recording_tab(self):
# 构建录音页面
pass
def 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 = False
self.is_paused = False
self.frames = []
self.stream = None
self.audio = pyaudio.PyAudio()
self.recording_start_time = 0
self.paused_duration = 0
self.last_pause_time = 0
# 初始化UI
self.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 pixmap
def 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:
return
try:
# 检查设备是否有效
device_index = self.input_device_combo.currentData()
if device_index == -1:
QMessageBox.warning(self, "设备错误", "请选择有效的输入设备")
return
# 重置状态
self.is_recording = True
self.is_paused = False
self.frames = []
self.paused_duration = 0
self.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.paInt16
self.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刷新率
# 更新UI
self.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 = False
QMessageBox.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 pixmap
def toggle_pause(self):
"""暂停/继续录音"""
if not self.is_recording:
return
if self.is_paused:
# 继续录音
self.is_paused = False
self.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 = True
self.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_duration
else:
# 运行状态下计算精确时间
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:
return
self.is_recording = False
self.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, "保存错误", "没有录音数据可保存")
return
try:
# 获取保存路径
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: # 默认为WAV
ext = "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-bit
wf.setframerate(self.sample_rate)
wf.writeframes(b''.join(self.frames))
# 根据选择的格式进行转换
if ext != "wav":
try:
# 这里应该添加实际的音频格式转换代码
# 例如使用pydub或其他音频处理库
# 由于代码示例中未包含实际转换逻辑,这里只是模拟
import shutil
shutil.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()
return
self.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())
🎯 开发难点与解决方案
- 设备兼容性问题:
- 问题:不同系统音频设备API差异
- 方案:使用PyAudio的跨平台抽象层
- 精确计时挑战:
- 问题:系统时钟不精确
- 方案:结合QElapsedTimer和实际音频帧数计算
- 格式转换实现:
- 问题:原生Python缺乏高效音频编码库
- 方案:可扩展为调用FFmpeg等外部工具
- UI性能优化:
- 问题:频繁更新导致界面卡顿
- 方案:使用HTML格式化文本减少重绘
🔮 未来扩展方向
- 音频编辑功能:
- 添加简单的剪切、合并功能
- 支持添加标记点
- 云存储集成:
- 自动上传到Google Drive/OneDrive
- AI增强:
- 自动降噪
- 语音转文字
- 多平台支持:
- 打包为Windows/macOS原生应用
- 开发移动端版本
📝 总结
本文详细介绍了如何使用PyQt6开发功能完善的音频录制工具。通过这个项目,我们不仅学习了:
- PyQt6的现代化UI开发技巧
- PyAudio的音频采集和处理
- 系统托盘集成等高级功能
- 健壮的错误处理机制
这个项目展示了Python在多媒体应用开发中的强大能力,代码结构清晰,易于扩展,是学习GUI编程和音频处理的优秀范例。
开发心得:
“好的音频工具应该在精确性和用户体验之间找到平衡。通过这个项目,我深刻理解了实时系统开发中时间管理的重要性,以及如何通过巧妙的UI设计提升工具的专业感。”
希望本文能帮助读者掌握桌面音频应用的开发技巧,期待看到大家基于此项目的创新改进!