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



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

設置頁面:
- 保存路徑配置
- 音頻質量設置
- 其他偏好選項

狀態(tài)指示系統(tǒng)
- 綠色:準備就緒
- 紅色:正在錄制
- 黃色:已暫停
軟件實現(xiàn)步驟
1. 環(huán)境準備
pip install PyQt6 pyaudio
2. 項目結構設計
AudioRecorder/
│── main.py # 程序入口
│── settings.ini # 配置文件
│── recordings/ # 默認保存目錄
3. 核心類架構
class AudioRecorder(QMainWindow):
def __init__(self):
# 初始化錄音狀態(tài)、UI和系統(tǒng)托盤
pass
def init_ui(self):
# 創(chuàng)建主界面和分頁
pass
def create_recording_tab(self):
# 構建錄音頁面
pass
def create_settings_tab(self):
# 構建設置頁面
pass關鍵代碼解析
1. 音頻設備管理
def update_device_list(self):
"""動態(tài)更新音頻輸入設備列表"""
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. 時間顯示優(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)
# 根據格式選擇擴展名
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)
高級功能實現(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. 設置持久化
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")
# 錄音狀態(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()
# 加載設置
self.load_settings()
# 更新設備列表
self.update_device_list()
# 設置窗口屬性
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)建設置分頁
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)
# 設置等寬字體用于計時器顯示
mono_font = QFont("Consolas" if sys.platform == "win32" else "Monospace")
mono_font.setPointSize(24)
# 錄音時間顯示 - 優(yōu)化顯示質量
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)
# 設備設置區(qū)域
device_group = QGroupBox("錄音設置")
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)
# 輸入設備選擇
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):
"""創(chuàng)建設置分頁"""
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 (專業(yè)音頻)", "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 (專業(yè)級)"])
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("最小化到系統(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)
# 添加彈簧使設置內容頂部對齊
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):
"""創(chuà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):
"""設置快捷鍵"""
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):
"""更新輸入設備列表,優(yōu)化搜索并避免重復設備"""
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
# 重置狀態(tài)
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模式錄制系統(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請檢查設備是否被其他程序占用或嘗試選擇其他設備。")
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):
"""創(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)化顯示質量"""
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)化顯示質量
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字節(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))
# 根據選擇的格式進行轉換
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):
"""重置錄音狀態(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):
"""加載設置"""
# 加載保存路徑
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)
)
# 加載系統(tǒng)音頻錄制設置
self.system_audio_check.setChecked(
self.settings.value("audio/system_audio", True, type=bool) # 默認啟用系統(tǒng)音頻錄制
)
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())
開發(fā)難點與解決方案
設備兼容性問題:
- 問題:不同系統(tǒng)音頻設備API差異
- 方案:使用PyAudio的跨平臺抽象層
精確計時挑戰(zhàn):
- 問題:系統(tǒng)時鐘不精確
- 方案:結合QElapsedTimer和實際音頻幀數計算
格式轉換實現(xiàn):
- 問題:原生Python缺乏高效音頻編碼庫
- 方案:可擴展為調用FFmpeg等外部工具
UI性能優(yōu)化:
- 問題:頻繁更新導致界面卡頓
- 方案:使用HTML格式化文本減少重繪
未來擴展方向
音頻編輯功能:
- 添加簡單的剪切、合并功能
- 支持添加標記點
云存儲集成:
自動上傳到Google Drive/OneDrive
AI增強:
- 自動降噪
- 語音轉文字
多平臺支持:
- 打包為Windows/macOS原生應用
- 開發(fā)移動端版本
總結
本文詳細介紹了如何使用PyQt6開發(fā)功能完善的音頻錄制工具。通過這個項目,我們不僅學習了:
PyQt6的現(xiàn)代化UI開發(fā)技巧
PyAudio的音頻采集和處理
系統(tǒng)托盤集成等高級功能
健壯的錯誤處理機制
到此這篇關于基于Python PyQt6打造高顏值多功能音頻錄制工具的文章就介紹到這了,更多相關Python音頻錄制 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Python3 main函數使用sys.argv傳入多個參數的實現(xiàn)
今天小編就為大家分享一篇Python3 main函數使用sys.argv傳入多個參數的實現(xiàn),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12
Python pexpect模塊及shell腳本except原理解析
這篇文章主要介紹了Python pexpect模塊及shell腳本except原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08

