import sys
import numpy as np
import pyaudio
from scipy.signal import get_window
from scipy.interpolate import interp1d
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QComboBox, QLabel, QPushButton,
QLineEdit, QMessageBox, QGroupBox, QRadioButton)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIntValidator, QDoubleValidator
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
# 从您提供的数据中提取的精确计权曲线
FREQUENCIES = [1, 1.25, 1.6, 2, 2.5, 3.15, 4, 5, 6.3, 8, 10, 12.5, 16, 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200,
250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500,
16000, 20000, 25000, 31500, 40000, 50000, 63000, 80000, 100000]
A_WEIGHTING = [-148.58, -140.83, -132.28, -124.55, -116.85, -108.89, -100.72, -93.14, -85.4, -77.55, -70.43, -63.58,
-56.42, -50.39, -44.82, -39.53, -34.54, -30.27, -26.22, -22.4, -19.14, -16.19, -13.24, -10.85, -8.67,
-6.64, -4.77, -3.25, -1.91, -0.79, 0, 0.58, 0.99, 1.2, 1.27, 1.2, 0.96, 0.55, -0.12, -1.15, -2.49, -4.25,
-6.71, -9.35, -12.33, -15.7, -19.41, -23.02, -26.85, -30.88, -34.68]
B_WEIGHTING = [-96.41, -90.6, -84.19, -78.41, -72.64, -66.69, -60.59, -54.95, -49.21, -43.43, -38.24, -33.32, -28.28,
-24.16, -20.48, -17.13, -14.1, -11.63, -9.36, -7.31, -5.65, -4.23, -2.94, -2.04, -1.36, -0.85, -0.5,
-0.28, -0.13, -0.04, 0, 0.01, -0.02, -0.09, -0.21, -0.4, -0.73, -1.18, -1.89, -2.94, -4.3, -6.07, -8.53,
-11.17, -14.16, -17.53, -21.24, -24.85, -28.68, -32.71, -36.51]
C_WEIGHTING = [-52.51, -48.65, -44.38, -40.53, -36.7, -32.76, -28.73, -25.03, -21.3, -17.59, -14.33, -11.34, -8.43,
-6.22, -4.44, -3.03, -1.98, -1.3, -0.82, -0.5, -0.3, -0.17, -0.08, -0.03, 0, 0.02, 0.03, 0.03, 0.03,
0.02, 0, -0.03, -0.09, -0.17, -0.3, -0.5, -0.83, -1.29, -1.99, -3.05, -4.41, -6.18, -8.63, -11.28,
-14.26, -17.64, -21.35, -24.95, -28.79, -32.82, -36.62]
# 组合数据
STANDARD_WEIGHTING = {
"A": list(zip(FREQUENCIES, A_WEIGHTING)),
"B": list(zip(FREQUENCIES, B_WEIGHTING)),
"C": list(zip(FREQUENCIES, C_WEIGHTING))
}
class RealTimeFFT(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("实时声级计(精确计权曲线)")
self.setGeometry(100, 100, 1400, 900)
self.setMinimumWidth(800) # 设置合理的最小宽度,避免界面元素被压缩过度
self.setMinimumHeight(600) # 设置合理的最小高度
self.audio = pyaudio.PyAudio()
self.stream = None
self.is_running = False
self.weighting_type = "A" # 默认 A 计权
# 创建更灵活的画布
self.fig = Figure(figsize=(10, 7), dpi=100, tight_layout=True)
self.ax_spectrum = self.fig.add_subplot(2, 1, 1) # 主频谱
self.ax_standard = self.fig.add_subplot(2, 1, 2) # 标准曲线对比
self.canvas = FigureCanvas(self.fig)
# 控件:选择麦克风
self.mic_label = QLabel("选择麦克风:")
self.mic_combo = QComboBox()
self.populate_mic_devices()
# 控件:选择窗函数
self.window_label = QLabel("选择窗函数:")
self.window_combo = QComboBox()
self.window_combo.addItems(["blackman", "hamming", "hann", "bartlett", "boxcar"])
# 控件:增益补偿
self.gain_label = QLabel("增益补偿 (dB):")
self.gain_combo = QComboBox()
self.gain_combo.addItems(["150", "160", "170", "180", "190", "200"])
self.gain_combo.setCurrentText("150")
# 控件:刷新速度(输入框)
self.refresh_label = QLabel("刷新速度 (ms):")
self.refresh_edit = QLineEdit("50")
self.refresh_edit.setValidator(QIntValidator(10, 1000))
# 新增坐标轴范围输入控件
self.x_start_label = QLabel("X轴起始频点:")
self.x_start_edit = QLineEdit("10")
self.x_start_edit.setValidator(QDoubleValidator(0.1, 100000, 2))
self.x_end_label = QLabel("X轴截止频点:")
self.x_end_edit = QLineEdit("20000")
self.x_end_edit.setValidator(QDoubleValidator(0.1, 100000, 2))
self.y_start_label = QLabel("Y轴起始值:")
self.y_start_edit = QLineEdit("-50")
self.y_start_edit.setValidator(QDoubleValidator(-100, 100, 2))
self.y_end_label = QLabel("Y轴截止值:")
self.y_end_edit = QLineEdit("100")
self.y_end_edit.setValidator(QDoubleValidator(-100, 100, 2))
# 控件:计权选择(A、B、C)
self.weighting_group = QGroupBox("频率计权(精确标准)")
self.weighting_layout = QHBoxLayout()
self.a_weighting_radio = QRadioButton("A计权")
self.b_weighting_radio = QRadioButton("B计权")
self.c_weighting_radio = QRadioButton("C计权")
self.a_weighting_radio.setChecked(True)
self.weighting_layout.addWidget(self.a_weighting_radio)
self.weighting_layout.addWidget(self.b_weighting_radio)
self.weighting_layout.addWidget(self.c_weighting_radio)
self.weighting_group.setLayout(self.weighting_layout)
self.a_weighting_radio.clicked.connect(lambda: self.set_weighting("A"))
self.b_weighting_radio.clicked.connect(lambda: self.set_weighting("B"))
self.c_weighting_radio.clicked.connect(lambda: self.set_weighting("C"))
# 控件:开始/停止按钮
self.start_btn = QPushButton("开始")
self.start_btn.clicked.connect(self.toggle_stream)
self.stop_btn = QPushButton("停止")
self.stop_btn.clicked.connect(self.stop_stream)
self.stop_btn.setEnabled(False)
# 统计信息
self.stats_label = QLabel("当前声级: -- dB 峰值: -- dB")
# 布局设置:新增坐标轴输入框布局
input_layout = QHBoxLayout()
input_layout.addWidget(self.x_start_label)
input_layout.addWidget(self.x_start_edit)
input_layout.addWidget(self.x_end_label)
input_layout.addWidget(self.x_end_edit)
input_layout.addWidget(self.y_start_label)
input_layout.addWidget(self.y_start_edit)
input_layout.addWidget(self.y_end_label)
input_layout.addWidget(self.y_end_edit)
# 优化控制布局,添加伸缩项使控件分布更均匀
control_layout = QHBoxLayout()
control_layout.addWidget(self.mic_label)
control_layout.addWidget(self.mic_combo)
control_layout.addWidget(self.window_label)
control_layout.addWidget(self.window_combo)
control_layout.addWidget(self.gain_label)
control_layout.addWidget(self.gain_combo)
control_layout.addWidget(self.refresh_label)
control_layout.addWidget(self.refresh_edit)
control_layout.addLayout(input_layout)
control_layout.addWidget(self.weighting_group)
control_layout.addWidget(self.start_btn)
control_layout.addWidget(self.stop_btn)
control_layout.addStretch() # 添加伸缩项
stats_layout = QHBoxLayout()
stats_layout.addWidget(self.stats_label)
# 主布局设置
main_layout = QVBoxLayout()
main_layout.addLayout(control_layout)
main_layout.addLayout(stats_layout)
main_layout.addWidget(self.canvas)
# 设置布局伸缩性和边距
main_layout.setContentsMargins(5, 5, 5, 5) # 设置适当边距
main_layout.setSpacing(5)
main_layout.setStretch(2, 1) # 让画布区域可伸缩
central_widget = QWidget()
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
# FFT 参数初始化
self.CHUNK = 1024 * 2
self.FORMAT = pyaudio.paInt16
self.CHANNELS = 1
self.RATE = 44100
self.freq_axis = np.fft.rfftfreq(self.CHUNK, 1.0 / self.RATE)
# 初始化定时器
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_plot)
# 预绘制标准曲线
self.plot_standard_weighting()
def populate_mic_devices(self):
"""获取麦克风设备列表"""
info = self.audio.get_host_api_info_by_index(0)
num_devices = info.get('deviceCount')
for i in range(num_devices):
device = self.audio.get_device_info_by_host_api_device_index(0, i)
if device['maxInputChannels'] > 0:
self.mic_combo.addItem(device['name'], i)
def set_weighting(self, weighting):
"""设置计权类型并更新对比曲线"""
self.weighting_type = weighting
self.plot_standard_weighting()
def start_stream(self):
"""启动音频流"""
device_index = self.mic_combo.currentData()
try:
refresh_ms = int(self.refresh_edit.text())
if not (10 <= refresh_ms <= 1000):
raise ValueError("刷新速度需在10-1000ms之间")
self.stream = self.audio.open(
input_device_index=device_index,
format=self.FORMAT,
channels=self.CHANNELS,
rate=self.RATE,
input=True,
frames_per_buffer=self.CHUNK
)
self.is_running = True
self.timer.start(refresh_ms)
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
print(f"已启动音频流,设备: {self.mic_combo.currentText()}")
except Exception as e:
QMessageBox.critical(self, "错误", f"无法启动音频流: {str(e)}")
def stop_stream(self):
"""停止音频流"""
self.is_running = False
self.timer.stop()
if self.stream:
self.stream.stop_stream()
self.stream.close()
self.stream = None
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.stats_label.setText("当前声级: -- dB 峰值: -- dB")
def toggle_stream(self):
if not self.is_running:
self.start_stream()
def interpolate_weighting(self, freq):
"""根据标准数据插值计算计权修正值"""
std_data = STANDARD_WEIGHTING[self.weighting_type]
freqs = np.array([f for f, _ in std_data])
weights = np.array([w for _, w in std_data])
return np.interp(freq, freqs, weights)
def plot_standard_weighting(self):
"""绘制标准计权曲线"""
self.ax_standard.clear()
std_data = STANDARD_WEIGHTING[self.weighting_type]
freqs = [f for f, _ in std_data]
weights = [w for _, w in std_data]
self.ax_standard.plot(freqs, weights, color='red', label=f"标准 {self.weighting_type}计权")
self.ax_standard.axvline(x=1000, color='gray', linestyle='--', label="1kHz 参考线")
self.ax_standard.axhline(y=0, color='gray', linestyle='--')
self.ax_standard.set_xscale('log')
self.ax_standard.set_xlabel("频率 (Hz)")
self.ax_standard.set_ylabel("计权修正值 (dB)")
self.ax_standard.set_xlim(10, 20000)
self.ax_standard.grid(True, linestyle='--', alpha=0.7)
self.ax_standard.legend()
self.fig.tight_layout() # 确保布局紧凑
self.canvas.draw()
def update_plot(self):
"""更新频谱和统计信息,根据输入框值更新坐标轴范围"""
if not self.is_running or not self.stream:
return
try:
# 读取音频数据
data = self.stream.read(self.CHUNK)
data_np = np.frombuffer(data, dtype=np.int16) / 32768.0
# 应用窗函数
window_name = self.window_combo.currentText()
window = get_window(window_name, self.CHUNK)
windowed_data = data_np * window
# 计算FFT
fft_data = np.fft.rfft(windowed_data)
magnitude = np.abs(fft_data) * 2 / np.sum(window)
magnitude[0] /= 2 # 直流分量修正
# 增益补偿
gain = float(self.gain_combo.currentText())
magnitude *= 10 ** (gain / 20)
# 计算计权修正
weighting = np.array([self.interpolate_weighting(f) for f in self.freq_axis])
db_data = 20 * np.log10(magnitude + 1e-10) + weighting
# 获取坐标轴范围输入值
x_start = float(self.x_start_edit.text())
x_end = float(self.x_end_edit.text())
y_start = float(self.y_start_edit.text())
y_end = float(self.y_end_edit.text())
# 统计信息(在设置的X轴范围内)
valid_indices = (self.freq_axis >= x_start) & (self.freq_axis <= x_end)
valid_db = db_data[valid_indices]
avg_db = np.mean(valid_db) if valid_db.size > 0 else 0
max_db = np.max(valid_db) if valid_db.size > 0 else 0
self.stats_label.setText(f"当前声级: {avg_db:.1f} dB 峰值: {max_db:.1f} dB")
# 绘制频谱
self.ax_spectrum.clear()
self.ax_spectrum.plot(self.freq_axis, db_data, color='blue', label="实时频谱")
self.ax_spectrum.set_title(f"实时FFT频谱({self.weighting_type}计权)")
self.ax_spectrum.set_xlabel("频率 (Hz)")
self.ax_spectrum.set_ylabel("声压级 (dB)")
self.ax_spectrum.set_xscale('log')
self.ax_spectrum.set_xlim(x_start, x_end)
self.ax_spectrum.set_ylim(y_start, y_end)
self.ax_spectrum.grid(True, linestyle='--', alpha=0.7)
self.ax_spectrum.legend()
# 刷新画布
self.fig.tight_layout() # 确保布局紧凑
self.canvas.draw()
except Exception as e:
print(f"更新失败: {e}")
self.stop_stream()
def closeEvent(self, event):
"""关闭时释放资源"""
self.stop_stream()
self.audio.terminate()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
font = app.font()
font.setFamily("SimHei") # 设置中文字体支持
app.setFont(font)
window = RealTimeFFT()
window.show()
sys.exit(app.exec_())这个代码在pycharm上正常运行,希望同步生成手机apk安装包给手机安装成app后使用,教我一步步打包,我是超级新手,所有的步骤都要很详细,比如输入命令时要告诉我是在powershell,还是pycharm的终端输入类似这样的动作要很详细
最新发布