基于Python?PyQt6打造高顏值多功能音頻錄制工具
概述
在當(dāng)今數(shù)字化時(shí)代,音頻錄制工具已經(jīng)成為內(nèi)容創(chuàng)作者、會(huì)議記錄者和音樂(lè)愛(ài)好者的必備工具。本文將詳細(xì)介紹如何使用Python的PyQt6庫(kù)開(kāi)發(fā)一款功能全面、界面美觀的桌面錄音工具。這款工具不僅支持常規(guī)麥克風(fēng)輸入,還能錄制系統(tǒng)音頻,并提供了豐富的設(shè)置選項(xiàng)和精美的用戶(hù)界面。
本項(xiàng)目的核心特點(diǎn):
- 基于PyQt6的現(xiàn)代化UI設(shè)計(jì)
- 支持麥克風(fēng)和系統(tǒng)音頻錄制
- 提供暫停/繼續(xù)功能
- 可配置的音頻質(zhì)量和保存格式
- 智能文件保存管理
- 系統(tǒng)托盤(pán)支持后臺(tái)運(yùn)行
功能詳解
1. 核心錄音功能
多設(shè)備支持:自動(dòng)檢測(cè)系統(tǒng)音頻輸入設(shè)備
高精度計(jì)時(shí):毫秒級(jí)錄音時(shí)長(zhǎng)顯示
狀態(tài)管理:實(shí)時(shí)顯示錄制狀態(tài)(錄制中/暫停/停止)
2. 音頻處理能力
支持多種采樣率(44.1kHz/48kHz/96kHz)
可調(diào)位深度(16bit/24bit/32bit)
多種輸出格式(WAV/MP3/FLAC/OGG)
3. 用戶(hù)體驗(yàn)優(yōu)化
系統(tǒng)托盤(pán)圖標(biāo)控制
快捷鍵支持(Ctrl+R開(kāi)始,Ctrl+P暫停,Ctrl+S停止)
最小化到托盤(pán)選項(xiàng)
音頻設(shè)備自動(dòng)刷新
界面展示效果
主界面布局



界面采用分頁(yè)設(shè)計(jì),分為"錄音"和"設(shè)置"兩大板塊:
錄音頁(yè)面:
- 大尺寸計(jì)時(shí)器顯示
- 醒目的控制按鈕
- 設(shè)備選擇區(qū)域

設(shè)置頁(yè)面:
- 保存路徑配置
- 音頻質(zhì)量設(shè)置
- 其他偏好選項(xiàng)

狀態(tài)指示系統(tǒng)
- 綠色:準(zhǔn)備就緒
- 紅色:正在錄制
- 黃色:已暫停
軟件實(shí)現(xiàn)步驟
1. 環(huán)境準(zhǔn)備
pip install PyQt6 pyaudio
2. 項(xiàng)目結(jié)構(gòu)設(shè)計(jì)
AudioRecorder/
│── main.py # 程序入口
│── settings.ini # 配置文件
│── recordings/ # 默認(rèn)保存目錄
3. 核心類(lèi)架構(gòu)
class AudioRecorder(QMainWindow):
def __init__(self):
# 初始化錄音狀態(tài)、UI和系統(tǒng)托盤(pán)
pass
def init_ui(self):
# 創(chuàng)建主界面和分頁(yè)
pass
def create_recording_tab(self):
# 構(gòu)建錄音頁(yè)面
pass
def create_settings_tab(self):
# 構(gòu)建設(shè)置頁(yè)面
pass關(guān)鍵代碼解析
1. 音頻設(shè)備管理
def update_device_list(self):
"""動(dòng)態(tài)更新音頻輸入設(shè)備列表"""
self.audio.terminate()
self.audio = pyaudio.PyAudio()
# 獲取所有輸入設(shè)備
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):
"""啟動(dòng)錄音的核心邏輯"""
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. 音頻數(shù)據(jù)回調(diào)
def audio_callback(self, in_data, frame_count, time_info, status):
"""實(shí)時(shí)音頻數(shù)據(jù)采集回調(diào)"""
if self.is_recording and not self.is_paused:
self.frames.append(in_data)
return (in_data, pyaudio.paContinue)
4. 時(shí)間顯示優(yōu)化
def update_display_time(self):
"""高精度時(shí)間顯示(毫秒級(jí))"""
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>"
)
文件保存機(jī)制
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)
# 根據(jù)格式選擇擴(kuò)展名
ext = "wav" if "WAV" in selected_format else "mp3" # 其他格式類(lèi)似
2. 臨時(shí)文件處理
# 先保存為WAV再轉(zhuǎn)換
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))
# 格式轉(zhuǎn)換處理(偽代碼)
if ext != "wav":
convert_audio(temp_wav, filename, ext)
os.remove(temp_wav)
高級(jí)功能實(shí)現(xiàn)
1. 系統(tǒng)托盤(pán)集成
def init_system_tray(self):
"""創(chuàng)建系統(tǒng)托盤(pán)圖標(biāo)和菜單"""
self.tray_icon = QSystemTrayIcon(self)
self.tray_menu = QMenu()
# 添加菜單項(xiàng)
actions = [
("顯示窗口", self.show_normal),
("開(kāi)始錄制", self.start_recording),
("退出", self.close)
]
# ... 添加到菜單
2. 設(shè)置持久化
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())
# ... 其他設(shè)置
3. 異常處理機(jī)制
try:
self.stream = self.audio.open(...)
except Exception as e:
QMessageBox.warning(self, "設(shè)備錯(cuò)誤",
f"無(wú)法打開(kāi)音頻流: {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__()
# 初始化設(shè)置
self.settings = QSettings("AudioRecorder", "RecorderApp")
# 錄音狀態(tài)
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()
# 初始化系統(tǒng)托盤(pán)
self.init_system_tray()
# 加載設(shè)置
self.load_settings()
# 更新設(shè)備列表
self.update_device_list()
# 設(shè)置窗口屬性
self.setWindowTitle("錄音工具-BY 創(chuàng)客白澤")
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)
# 創(chuàng)建分頁(yè)
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
# 創(chuàng)建錄音分頁(yè)
self.create_recording_tab()
# 創(chuàng)建設(shè)置分頁(yè)
self.create_settings_tab()
# 添加快捷鍵
self.setup_shortcuts()
def create_recording_tab(self):
"""創(chuàng)建錄音分頁(yè)"""
recording_tab = QWidget()
layout = QVBoxLayout(recording_tab)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(15)
# 時(shí)間顯示組
time_group = QGroupBox("錄音時(shí)間")
time_layout = QVBoxLayout(time_group)
# 設(shè)置等寬字體用于計(jì)時(shí)器顯示
mono_font = QFont("Consolas" if sys.platform == "win32" else "Monospace")
mono_font.setPointSize(24)
# 錄音時(shí)間顯示 - 優(yōu)化顯示質(zhì)量
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)
# 狀態(tài)顯示
self.status_label = QLabel("?? 準(zhǔn)備就緒")
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)
# 高精度計(jì)時(shí)器
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)
# 開(kāi)始錄音按鈕
self.start_button = QPushButton("?? 開(kāi)始錄制")
self.start_button.setStyleSheet(self.get_button_style("#4CAF50"))
self.start_button.clicked.connect(self.start_recording)
button_layout.addWidget(self.start_button)
# 暫停/繼續(xù)按鈕
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)
# 設(shè)備設(shè)置區(qū)域
device_group = QGroupBox("錄音設(shè)置")
device_layout = QVBoxLayout(device_group)
# 系統(tǒng)音頻錄制選項(xiàng)
self.system_audio_check = QCheckBox("錄制系統(tǒng)音頻")
self.system_audio_check.setChecked(True) # 默認(rèn)啟用系統(tǒng)音頻錄制
device_layout.addWidget(self.system_audio_check)
# 輸入設(shè)備選擇
device_layout.addWidget(QLabel("?? 輸入設(shè)備:"))
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)
# 刷新設(shè)備按鈕
refresh_button = QPushButton("?? 刷新設(shè)備列表")
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)
# 添加彈簧使內(nèi)容頂部對(duì)齊
layout.addStretch()
self.tabs.addTab(recording_tab, "??? 錄音")
def create_settings_tab(self):
"""創(chuàng)建設(shè)置分頁(yè)"""
settings_tab = QWidget()
layout = QVBoxLayout(settings_tab)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(15)
# 保存路徑設(shè)置
path_group = QGroupBox("保存設(shè)置")
path_layout = QVBoxLayout(path_group)
path_layout.addWidget(QLabel("?? 默認(rèn)保存路徑:"))
# 路徑選擇和瀏覽按鈕
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 (無(wú)損)", "MP3 (高壓縮)", "FLAC (無(wú)損壓縮)", "OGG (開(kāi)放格式)"])
path_layout.addWidget(self.format_combo)
layout.addWidget(path_group)
# 音頻質(zhì)量設(shè)置
quality_group = QGroupBox("音頻質(zhì)量")
quality_layout = QVBoxLayout(quality_group)
# 采樣率設(shè)置
sample_rate_layout = QHBoxLayout()
sample_rate_layout.addWidget(QLabel("采樣率:"))
self.sample_rate_combo = QComboBox()
self.sample_rate_combo.addItems(["44100 Hz (CD質(zhì)量)", "48000 Hz (專(zhuān)業(yè)音頻)", "96000 Hz (高清音頻)"])
sample_rate_layout.addWidget(self.sample_rate_combo)
quality_layout.addLayout(sample_rate_layout)
# 位深度設(shè)置
bit_depth_layout = QHBoxLayout()
bit_depth_layout.addWidget(QLabel("位深度:"))
self.bit_depth_combo = QComboBox()
self.bit_depth_combo.addItems(["16 bit (標(biāo)準(zhǔn))", "24 bit (高精度)", "32 bit (專(zhuān)業(yè)級(jí))"])
bit_depth_layout.addWidget(self.bit_depth_combo)
quality_layout.addLayout(bit_depth_layout)
# MP3質(zhì)量設(shè)置 (僅在MP3格式選中時(shí)顯示)
self.mp3_quality_layout = QHBoxLayout()
self.mp3_quality_layout.addWidget(QLabel("MP3質(zhì)量:"))
self.mp3_quality_combo = QComboBox()
self.mp3_quality_combo.addItems(["128 kbps (標(biāo)準(zhǔn))", "192 kbps (高質(zhì)量)", "256 kbps (極高)", "320 kbps (最佳)"])
self.mp3_quality_layout.addWidget(self.mp3_quality_combo)
quality_layout.addLayout(self.mp3_quality_layout)
# 根據(jù)格式選擇顯示/隱藏MP3質(zhì)量設(shè)置
self.format_combo.currentIndexChanged.connect(self.update_format_settings_visibility)
self.update_format_settings_visibility()
layout.addWidget(quality_group)
# 其他設(shè)置
other_group = QGroupBox("其他設(shè)置")
other_layout = QVBoxLayout(other_group)
# 最小化到托盤(pán)
self.minimize_to_tray_check = QCheckBox("最小化到系統(tǒng)托盤(pán)")
other_layout.addWidget(self.minimize_to_tray_check)
# 開(kāi)機(jī)自啟動(dòng)
self.auto_start_check = QCheckBox("開(kāi)機(jī)自動(dòng)啟動(dòng)")
other_layout.addWidget(self.auto_start_check)
layout.addWidget(other_group)
# 添加彈簧使設(shè)置內(nèi)容頂部對(duì)齊
layout.addStretch()
# 保存設(shè)置按鈕
save_settings_button = QPushButton("?? 保存設(shè)置")
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, "?? 設(shè)置")
def update_format_settings_visibility(self):
"""根據(jù)選擇的格式更新設(shè)置可見(jiàn)性"""
selected_format = self.format_combo.currentText()
is_mp3 = "MP3" in selected_format
# 顯示/隱藏MP3質(zhì)量設(shè)置
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):
"""創(chuàng)建應(yīng)用圖標(biāo)"""
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):
"""設(shè)置快捷鍵"""
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):
"""初始化系統(tǒng)托盤(pán)"""
self.tray_icon = QSystemTrayIcon(self)
self.tray_menu = QMenu()
actions = [
("?? 顯示窗口", self.show_normal),
("?? 開(kāi)始錄制", 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):
"""托盤(pán)圖標(biāo)點(diǎn)擊事件處理"""
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):
"""更新輸入設(shè)備列表,優(yōu)化搜索并避免重復(fù)設(shè)備"""
self.input_device_combo.clear()
try:
# 重新初始化PyAudio對(duì)象,確保獲取最新的設(shè)備列表
if hasattr(self, 'audio'):
self.audio.terminate()
self.audio = pyaudio.PyAudio()
# 獲取所有音頻設(shè)備
count = self.audio.get_device_count()
unique_devices = set() # 用于跟蹤已添加的設(shè)備
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)
# 標(biāo)準(zhǔn)化設(shè)備名稱(chēng)(去除多余空格和特殊字符)
normalized_name = ' '.join(device_name.strip().split())
# 檢查是否已經(jīng)添加過(guò)這個(gè)設(shè)備
device_key = f"{normalized_name}_{device_channels}"
if device_key not in unique_devices:
unique_devices.add(device_key)
# 添加設(shè)備到下拉列表
display_name = f"{normalized_name} (Ch:{device_channels})"
self.input_device_combo.addItem(display_name, i)
# 如果是默認(rèn)輸入設(shè)備,設(shè)置為當(dāng)前選擇
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
# 如果沒(méi)有找到任何設(shè)備,添加一個(gè)默認(rèn)選項(xiàng)
if self.input_device_combo.count() == 0:
self.input_device_combo.addItem("未找到輸入設(shè)備", -1)
QMessageBox.warning(self, "設(shè)備錯(cuò)誤", "未找到可用的音頻輸入設(shè)備")
except Exception as e:
print(f"Error updating device list: {str(e)}")
QMessageBox.warning(self, "設(shè)備錯(cuò)誤", f"無(wú)法獲取音頻設(shè)備列表: {str(e)}")
# 添加一個(gè)默認(rèn)選項(xiàng)
self.input_device_combo.addItem("默認(rèn)設(shè)備", 0)
def start_recording(self):
"""開(kāi)始錄音"""
if self.is_recording:
return
try:
# 檢查設(shè)備是否有效
device_index = self.input_device_combo.currentData()
if device_index == -1:
QMessageBox.warning(self, "設(shè)備錯(cuò)誤", "請(qǐng)選擇有效的輸入設(shè)備")
return
# 重置狀態(tài)
self.is_recording = True
self.is_paused = False
self.frames = []
self.paused_duration = 0
self.last_pause_time = 0
# 獲取設(shè)備參數(shù)
try:
device_info = self.audio.get_device_info_by_index(device_index)
except Exception as e:
QMessageBox.warning(self, "設(shè)備錯(cuò)誤", f"無(wú)法獲取設(shè)備信息: {str(e)}")
self.reset_recording_state()
return
# 設(shè)置音頻參數(shù)
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
# 嘗試打開(kāi)音頻流
try:
if self.system_audio_check.isChecked():
# 嘗試使用WASAPI loopback模式錄制系統(tǒng)音頻
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:
# 普通麥克風(fēng)錄音
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, "錄音錯(cuò)誤", f"無(wú)法開(kāi)始錄音: {str(e)}\n請(qǐng)檢查設(shè)備是否被其他程序占用或嘗試選擇其他設(shè)備。")
self.reset_recording_state()
return
# 啟動(dòng)計(jì)時(shí)器
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, "錄音錯(cuò)誤", f"無(wú)法開(kāi)始錄音: {str(e)}")
self.reset_recording_state()
def audio_callback(self, in_data, frame_count, time_info, status):
"""音頻回調(diào)函數(shù),確保實(shí)時(shí)采集"""
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):
"""創(chuàng)建錄音狀態(tài)圖標(biāo)"""
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):
"""暫停/繼續(xù)錄音"""
if not self.is_recording:
return
if self.is_paused:
# 繼續(xù)錄音
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() # 重新開(kāi)始計(jì)時(shí)
else:
# 暫停錄音
self.is_paused = True
self.last_pause_time = datetime.now().timestamp()
self.status_label.setText("?? 已暫停")
self.pause_button.setText("? 繼續(xù)")
self.elapsed_timer.invalidate() # 停止計(jì)時(shí)
def update_display_time(self):
"""更新顯示的時(shí)間 - 優(yōu)化顯示質(zhì)量"""
if self.is_recording:
if self.is_paused:
# 暫停狀態(tài)下顯示已記錄的時(shí)間
elapsed = self.last_pause_time - self.recording_start_time - self.paused_duration
else:
# 運(yùn)行狀態(tài)下計(jì)算精確時(shí)間
elapsed = (datetime.now().timestamp() - self.recording_start_time - self.paused_duration)
# 格式化時(shí)間顯示
hours, remainder = divmod(int(elapsed), 3600)
minutes, seconds = divmod(remainder, 60)
milliseconds = int((elapsed - int(elapsed)) * 1000)
# 使用HTML格式優(yōu)化顯示質(zhì)量
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()
# 計(jì)算實(shí)際錄制時(shí)長(zhǎng)
actual_duration = (datetime.now().timestamp() - self.recording_start_time - self.paused_duration)
# 保存文件
self.save_recording(actual_duration)
except Exception as e:
QMessageBox.warning(self, "保存錯(cuò)誤", f"保存錄音時(shí)出錯(cuò): {str(e)}")
finally:
self.reset_recording_state()
def save_recording(self, duration):
"""保存錄音文件"""
if not self.frames:
QMessageBox.warning(self, "保存錯(cuò)誤", "沒(méi)有錄音數(shù)據(jù)可保存")
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()
# 根據(jù)選擇的格式確定文件擴(kuò)展名
if "MP3" in selected_format:
ext = "mp3"
elif "FLAC" in selected_format:
ext = "flac"
elif "OGG" in selected_format:
ext = "ogg"
else: # 默認(rèn)為WAV
ext = "wav"
filename = os.path.join(save_dir, f"recording_{timestamp}.{ext}")
# 計(jì)算實(shí)際音頻數(shù)據(jù)時(shí)長(zhǎng)
total_bytes = len(b''.join(self.frames))
calculated_duration = total_bytes / (self.sample_rate * self.channels * 2) # 16-bit = 2字節(jié)
# 首先保存為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))
# 根據(jù)選擇的格式進(jìn)行轉(zhuǎn)換
if ext != "wav":
try:
# 這里應(yīng)該添加實(shí)際的音頻格式轉(zhuǎn)換代碼
# 例如使用pydub或其他音頻處理庫(kù)
# 由于代碼示例中未包含實(shí)際轉(zhuǎn)換邏輯,這里只是模擬
import shutil
shutil.copy(temp_wav, filename)
os.remove(temp_wav)
except Exception as e:
# 如果轉(zhuǎn)換失敗,保留WAV文件
os.rename(temp_wav, filename)
QMessageBox.warning(self, "格式轉(zhuǎn)換",
f"無(wú)法轉(zhuǎn)換為{ext.upper()}格式,已保存為WAV文件: {str(e)}")
# 顯示保存信息
QMessageBox.information(
self,
"保存成功",
f"錄音已保存到:\n{filename}\n"
f"格式: {selected_format.split()[0]}\n"
f"計(jì)時(shí)器時(shí)長(zhǎng): {duration:.3f}秒\n"
f"音頻數(shù)據(jù)時(shí)長(zhǎng): {calculated_duration:.3f}秒\n"
f"采樣率: {self.sample_rate}Hz\n"
f"聲道數(shù): {self.channels}"
)
except Exception as e:
raise Exception(f"保存錄音文件時(shí)出錯(cuò): {str(e)}")
def reset_recording_state(self):
"""重置錄音狀態(tài)"""
self.status_label.setText("?? 準(zhǔn)備就緒")
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):
"""加載設(shè)置"""
# 加載保存路徑
default_path = os.path.join(os.path.expanduser("~"), "Recordings")
self.save_path_edit.setText(self.settings.value("save_path", default_path))
# 加載文件格式設(shè)置
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質(zhì)量設(shè)置
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)
# 加載采樣率設(shè)置
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)
# 加載位深度設(shè)置
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)
# 加載其他設(shè)置
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)
)
# 加載系統(tǒng)音頻錄制設(shè)置
self.system_audio_check.setChecked(
self.settings.value("audio/system_audio", True, type=bool) # 默認(rèn)啟用系統(tǒng)音頻錄制
)
def save_settings(self):
"""保存設(shè)置"""
# 保存路徑設(shè)置
self.settings.setValue("save_path", self.save_path_edit.text())
# 保存音頻設(shè)置
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())
# 保存其他設(shè)置
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, "設(shè)置保存", "設(shè)置已成功保存!")
def closeEvent(self, event):
"""關(guān)閉事件處理"""
if self.is_recording:
reply = QMessageBox.question(
self, '正在錄音',
"當(dāng)前正在錄音,確定要退出嗎?",
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")
# 設(shè)置應(yīng)用程序字體
font = app.font()
font.setPointSize(10)
app.setFont(font)
recorder = AudioRecorder()
recorder.show()
sys.exit(app.exec())
開(kāi)發(fā)難點(diǎn)與解決方案
設(shè)備兼容性問(wèn)題:
- 問(wèn)題:不同系統(tǒng)音頻設(shè)備API差異
- 方案:使用PyAudio的跨平臺(tái)抽象層
精確計(jì)時(shí)挑戰(zhàn):
- 問(wèn)題:系統(tǒng)時(shí)鐘不精確
- 方案:結(jié)合QElapsedTimer和實(shí)際音頻幀數(shù)計(jì)算
格式轉(zhuǎn)換實(shí)現(xiàn):
- 問(wèn)題:原生Python缺乏高效音頻編碼庫(kù)
- 方案:可擴(kuò)展為調(diào)用FFmpeg等外部工具
UI性能優(yōu)化:
- 問(wèn)題:頻繁更新導(dǎo)致界面卡頓
- 方案:使用HTML格式化文本減少重繪
未來(lái)擴(kuò)展方向
音頻編輯功能:
- 添加簡(jiǎn)單的剪切、合并功能
- 支持添加標(biāo)記點(diǎn)
云存儲(chǔ)集成:
自動(dòng)上傳到Google Drive/OneDrive
AI增強(qiáng):
- 自動(dòng)降噪
- 語(yǔ)音轉(zhuǎn)文字
多平臺(tái)支持:
- 打包為Windows/macOS原生應(yīng)用
- 開(kāi)發(fā)移動(dòng)端版本
總結(jié)
本文詳細(xì)介紹了如何使用PyQt6開(kāi)發(fā)功能完善的音頻錄制工具。通過(guò)這個(gè)項(xiàng)目,我們不僅學(xué)習(xí)了:
PyQt6的現(xiàn)代化UI開(kāi)發(fā)技巧
PyAudio的音頻采集和處理
系統(tǒng)托盤(pán)集成等高級(jí)功能
健壯的錯(cuò)誤處理機(jī)制
到此這篇關(guān)于基于Python PyQt6打造高顏值多功能音頻錄制工具的文章就介紹到這了,更多相關(guān)Python音頻錄制 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python3 main函數(shù)使用sys.argv傳入多個(gè)參數(shù)的實(shí)現(xiàn)
今天小編就為大家分享一篇Python3 main函數(shù)使用sys.argv傳入多個(gè)參數(shù)的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-12-12
Python pexpect模塊及shell腳本except原理解析
這篇文章主要介紹了Python pexpect模塊及shell腳本except原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
Python爬蟲(chóng)教程知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理的是一篇關(guān)于Python爬蟲(chóng)教程知識(shí)點(diǎn)總結(jié),有興趣的朋友們可以學(xué)習(xí)參考下。2020-10-10
Python數(shù)據(jù)可視化庫(kù)seaborn的使用總結(jié)
這篇文章主要介紹了Python數(shù)據(jù)可視化庫(kù)seaborn的使用總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
python實(shí)現(xiàn)126郵箱發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)126郵箱發(fā)送郵件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
對(duì)于Python的框架中一些會(huì)話(huà)程序的管理
這篇文章主要介紹了對(duì)于Python的框架中一些會(huì)話(huà)程序的管理,會(huì)話(huà)的實(shí)現(xiàn)是Python框架的基本功能,本文主要講述了對(duì)其的一些管理維護(hù)要點(diǎn),需要的朋友可以參考下2015-04-04

