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



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

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

狀態(tài)指示系統(tǒng)
- 綠色:準備就緒
- 紅色:正在錄制
- 黃色:已暫停
軟件實現(xiàn)步驟
1. 環(huán)境準備
pip install PyQt6 pyaudio
2. 項目結(jié)構(gòu)設(shè)計
AudioRecorder/
│── main.py # 程序入口
│── settings.ini # 配置文件
│── recordings/ # 默認保存目錄
3. 核心類架構(gòu)
class AudioRecorder(QMainWindow):
def __init__(self):
# 初始化錄音狀態(tài)、UI和系統(tǒng)托盤
pass
def init_ui(self):
# 創(chuàng)建主界面和分頁
pass
def create_recording_tab(self):
# 構(gòu)建錄音頁面
pass
def create_settings_tab(self):
# 構(gòu)建設(shè)置頁面
pass關(guān)鍵代碼解析
1. 音頻設(shè)備管理
def update_device_list(self):
"""動態(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):
"""啟動錄音的核心邏輯"""
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ù)據(jù)采集回調(diào)"""
if self.is_recording and not self.is_paused:
self.frames.append(in_data)
return (in_data, pyaudio.paContinue)
4. 時間顯示優(yōu)化
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)
# 根據(jù)格式選擇擴展名
ext = "wav" if "WAV" in selected_format else "mp3" # 其他格式類似
2. 臨時文件處理
# 先保存為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)
高級功能實現(xiàn)
1. 系統(tǒng)托盤集成
def init_system_tray(self):
"""創(chuàng)建系統(tǒng)托盤圖標和菜單"""
self.tray_icon = QSystemTrayIcon(self)
self.tray_menu = QMenu()
# 添加菜單項
actions = [
("顯示窗口", self.show_normal),
("開始錄制", 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. 異常處理機制
try:
self.stream = self.audio.open(...)
except Exception as e:
QMessageBox.warning(self, "設(shè)備錯誤",
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__()
# 初始化設(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)托盤
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)建分頁
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
# 創(chuàng)建錄音分頁
self.create_recording_tab()
# 創(chuàng)建設(shè)置分頁
self.create_settings_tab()
# 添加快捷鍵
self.setup_shortcuts()
def create_recording_tab(self):
"""創(chuàng)建錄音分頁"""
recording_tab = QWidget()
layout = QVBoxLayout(recording_tab)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(15)
# 時間顯示組
time_group = QGroupBox("錄音時間")
time_layout = QVBoxLayout(time_group)
# 設(shè)置等寬字體用于計時器顯示
mono_font = QFont("Consolas" if sys.platform == "win32" else "Monospace")
mono_font.setPointSize(24)
# 錄音時間顯示 - 優(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("?? 準備就緒")
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)
# 暫停/繼續(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)音頻錄制選項
self.system_audio_check = QCheckBox("錄制系統(tǒng)音頻")
self.system_audio_check.setChecked(True) # 默認啟用系統(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)容頂部對齊
layout.addStretch()
self.tabs.addTab(recording_tab, "??? 錄音")
def create_settings_tab(self):
"""創(chuàng)建設(shè)置分頁"""
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("?? 默認保存路徑:"))
# 路徑選擇和瀏覽按鈕
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)
# 音頻質(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 (專業(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 (標準)", "24 bit (高精度)", "32 bit (專業(yè)級)"])
bit_depth_layout.addWidget(self.bit_depth_combo)
quality_layout.addLayout(bit_depth_layout)
# MP3質(zhì)量設(shè)置 (僅在MP3格式選中時顯示)
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 (標準)", "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)
# 最小化到托盤
self.minimize_to_tray_check = QCheckBox("最小化到系統(tǒng)托盤")
other_layout.addWidget(self.minimize_to_tray_check)
# 開機自啟動
self.auto_start_check = QCheckBox("開機自動啟動")
other_layout.addWidget(self.auto_start_check)
layout.addWidget(other_group)
# 添加彈簧使設(shè)置內(nèi)容頂部對齊
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è)置可見性"""
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)用圖標"""
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)托盤"""
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):
"""更新輸入設(shè)備列表,優(yōu)化搜索并避免重復(fù)設(shè)備"""
self.input_device_combo.clear()
try:
# 重新初始化PyAudio對象,確保獲取最新的設(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)
# 標準化設(shè)備名稱(去除多余空格和特殊字符)
normalized_name = ' '.join(device_name.strip().split())
# 檢查是否已經(jīng)添加過這個設(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)
# 如果是默認輸入設(shè)備,設(shè)置為當前選擇
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
# 如果沒有找到任何設(shè)備,添加一個默認選項
if self.input_device_combo.count() == 0:
self.input_device_combo.addItem("未找到輸入設(shè)備", -1)
QMessageBox.warning(self, "設(shè)備錯誤", "未找到可用的音頻輸入設(shè)備")
except Exception as e:
print(f"Error updating device list: {str(e)}")
QMessageBox.warning(self, "設(shè)備錯誤", f"無法獲取音頻設(shè)備列表: {str(e)}")
# 添加一個默認選項
self.input_device_combo.addItem("默認設(shè)備", 0)
def start_recording(self):
"""開始錄音"""
if self.is_recording:
return
try:
# 檢查設(shè)備是否有效
device_index = self.input_device_combo.currentData()
if device_index == -1:
QMessageBox.warning(self, "設(shè)備錯誤", "請選擇有效的輸入設(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è)備錯誤", f"無法獲取設(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
# 嘗試打開音頻流
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:
# 普通麥克風錄音
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請檢查設(shè)備是否被其他程序占用或嘗試選擇其他設(shè)備。")
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):
"""音頻回調(diào)函數(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)圖標"""
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() # 重新開始計時
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() # 停止計時
def update_display_time(self):
"""更新顯示的時間 - 優(yōu)化顯示質(zhì)量"""
if self.is_recording:
if self.is_paused:
# 暫停狀態(tài)下顯示已記錄的時間
elapsed = self.last_pause_time - self.recording_start_time - self.paused_duration
else:
# 運行狀態(tài)下計算精確時間
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格式優(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()
# 計算實際錄制時長
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, "保存錯誤", "沒有錄音數(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ù)選擇的格式確定文件擴展名
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}")
# 計算實際音頻數(shù)據(jù)時長
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ù)選擇的格式進行轉(zhuǎn)換
if ext != "wav":
try:
# 這里應(yīng)該添加實際的音頻格式轉(zhuǎn)換代碼
# 例如使用pydub或其他音頻處理庫
# 由于代碼示例中未包含實際轉(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"無法轉(zhuǎn)換為{ext.upper()}格式,已保存為WAV文件: {str(e)}")
# 顯示保存信息
QMessageBox.information(
self,
"保存成功",
f"錄音已保存到:\n{filename}\n"
f"格式: {selected_format.split()[0]}\n"
f"計時器時長: {duration:.3f}秒\n"
f"音頻數(shù)據(jù)時長: {calculated_duration:.3f}秒\n"
f"采樣率: {self.sample_rate}Hz\n"
f"聲道數(shù): {self.channels}"
)
except Exception as e:
raise Exception(f"保存錄音文件時出錯: {str(e)}")
def reset_recording_state(self):
"""重置錄音狀態(tài)"""
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):
"""加載設(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) # 默認啟用系統(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, '正在錄音',
"當前正在錄音,確定要退出嗎?",
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())
開發(fā)難點與解決方案
設(shè)備兼容性問題:
- 問題:不同系統(tǒng)音頻設(shè)備API差異
- 方案:使用PyAudio的跨平臺抽象層
精確計時挑戰(zhàn):
- 問題:系統(tǒng)時鐘不精確
- 方案:結(jié)合QElapsedTimer和實際音頻幀數(shù)計算
格式轉(zhuǎn)換實現(xiàn):
- 問題:原生Python缺乏高效音頻編碼庫
- 方案:可擴展為調(diào)用FFmpeg等外部工具
UI性能優(yōu)化:
- 問題:頻繁更新導(dǎo)致界面卡頓
- 方案:使用HTML格式化文本減少重繪
未來擴展方向
音頻編輯功能:
- 添加簡單的剪切、合并功能
- 支持添加標記點
云存儲集成:
自動上傳到Google Drive/OneDrive
AI增強:
- 自動降噪
- 語音轉(zhuǎn)文字
多平臺支持:
- 打包為Windows/macOS原生應(yīng)用
- 開發(fā)移動端版本
總結(jié)
本文詳細介紹了如何使用PyQt6開發(fā)功能完善的音頻錄制工具。通過這個項目,我們不僅學習了:
PyQt6的現(xiàn)代化UI開發(fā)技巧
PyAudio的音頻采集和處理
系統(tǒng)托盤集成等高級功能
健壯的錯誤處理機制
到此這篇關(guān)于基于Python PyQt6打造高顏值多功能音頻錄制工具的文章就介紹到這了,更多相關(guān)Python音頻錄制 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
django為Form生成的label標簽添加class方式
這篇文章主要介紹了django為Form生成的label標簽添加class方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-05-05
采用Psyco實現(xiàn)python執(zhí)行速度提高到與編譯語言一樣的水平
Django實現(xiàn)文章詳情頁面跳轉(zhuǎn)代碼實例

