基于Python plot_surface实现实时麦克风3D瀑布频谱图的问题
Python音频可视化瀑布频谱图问题及解决方案
测试代码
import numpy as np import matplotlib.pyplot as plt from scipy import signal # spectrogram function from matplotlib import cm # colour map import pyaudio as pa import time import struct CHUNK = 1024 FORMAT = pa.paInt16 CHANNELS = 1 RATE = 44100 # in Hz LENGTH = 2 # in seconds p = pa.PyAudio() stream = p.open( format = FORMAT, channels = CHANNELS, rate = RATE, input=True, output=True, frames_per_buffer=CHUNK ) # 3d plot fig = plt.figure() ax = plt.axes(projection='3d') ax.set_xlim(RATE/2,0) # ---- OPTION QUESTION COMING UP BELOW # this works better for option 1. ax.set_zlim(-40,40) # this works better for option 2. Why? #ax.set_zlim(0,100) # ---- END OF OPTION QUESTION totaldata = bytearray(RATE*LENGTH) # this zeros out the entire array sf = None while 1: data = stream.read(CHUNK) # ---- 2 OPTIONS COMING UP BELOW (UNCOMMENT ONE OR THE OTHER, AND MATCH IT WITH THE ONE ABOVE) # OPTION 1 (CHUNK of data only--this works fine) # prepare just this chunk for display numpydata = np.frombuffer(data, dtype=np.int16) ## # OPTION 2 (1 second of data--this has huge walls against the edges of each chunk?) ## # pop first CHUNK-sized data ## totaldata = totaldata[((CHUNK*2)-1):] ## # append the new data ## totaldata[((RATE*LENGTH)-(CHUNK*2)):] = data ## # load the data (option 1 ,one CHUNK) ## #numpydata = np.frombuffer(onechunkdata, dtype=np.int16) ## # (option 2, 1 second) ## numpydata = np.frombuffer(totaldata, dtype=np.int16) # ---- END OF TWO OPTIONS # analyze data freq_bins, timestamps, spec = signal.spectrogram(numpydata, RATE) # redraw (have if statement since the first time sf is not instantiated until below) if (sf): sf.remove() sf = ax.plot_surface(freq_bins[:, None], timestamps[None, :], 10.0*np.log10(spec), cmap='inferno') plt.show(block=False) plt.pause(0.01)
疑问列表
- 如何平滑频谱图边缘的尖峰?
- 是否因麦克风读取被计算和绘图中断导致丢包,进而产生尖峰?
- 为何单块音频与1秒音频的z轴范围差异巨大?
- 为何麦克风返回的块大小是请求的2倍(请求1024样本,实际2048字节)?
- 为何y轴(时间)的绘图长度仅为totaldata设定长度的一半?
解决方案与答疑
1. 平滑频谱图边缘尖峰(核心问题)
尖峰源于方案2中totaldata的滚动更新逻辑错误,导致新旧数据拼接时出现信号断裂,引发频谱泄漏。修复步骤如下:
步骤1:修正totaldata滚动更新逻辑
按样本对应的字节数正确裁剪和拼接数据,保证数组长度始终一致:
# 替换方案2中的更新代码 chunk_byte_size = CHUNK * 2 # paInt16每个样本占2字节 # 移除最前面的CHUNK样本对应的字节 totaldata = totaldata[chunk_byte_size:] # 追加新读取的音频数据 totaldata.extend(data) # 转换为numpy数组 numpydata = np.frombuffer(totaldata, dtype=np.int16)
步骤2:优化频谱分析参数
使用窗口函数和重叠率抑制频谱泄漏,进一步平滑过渡:
# 修改spectrogram调用,添加窗口和重叠参数 freq_bins, timestamps, spec = signal.spectrogram( numpydata, RATE, window='hann', # 汉宁窗减少频谱泄漏 nperseg=1024, # 每个分析段的样本数 noverlap=512 # 段之间重叠50%,平滑时间轴过渡 )
步骤3:频谱数据平滑(可选)
对频谱矩阵做轻微高斯滤波,压制剩余尖峰:
from scipy.ndimage import gaussian_filter # 平滑频谱数据,sigma值按需调整 spec_smoothed = gaussian_filter(spec, sigma=0.5) # 绘制平滑后的频谱 sf = ax.plot_surface(freq_bins[:, None], timestamps[None, :], 10.0*np.log10(spec_smoothed), cmap='inferno')
2. 麦克风读取是否会丢包导致尖峰?
不会。stream.read(CHUNK)是阻塞式读取,PyAudio会确保返回请求的样本数(除非音频设备硬件出错)。当前尖峰完全由数据拼接逻辑错误导致,信号断裂引发的突变在频谱上表现为宽频尖峰。
3. 单块与1秒音频z轴范围差异巨大的原因
z轴是频谱的分贝值(10*log10(spec)),差异源于:
- 方案1的单块数据时间短,能量集中在少量时间点,分贝值波动范围窄(-40到40dB);
- 方案2因之前的拼接错误,出现了异常高能量的信号突变,导致分贝值被异常拉高到0-100dB范围。修正拼接逻辑后,两者的z轴范围会趋于一致,可统一设置
ax.set_zlim(-60, 30)适配正常场景。
4. 麦克风返回字节数是请求样本数2倍的原因
因为使用了pa.paInt16格式,每个音频样本占2字节。你请求的CHUNK=1024是样本数,所以返回的字节数为1024*2=2048,这是正常的格式换算,并非块大小翻倍。可以用stream.get_sample_size(FORMAT)自动获取样本字节数,避免手动计算错误。
5. y轴时间长度为设定值一半的原因
totaldata初始化时按RATE*LENGTH字节分配,但paInt16每个样本占2字节,实际样本数为(RATE*LENGTH)/2,对应时间长度为(RATE*LENGTH/2)/RATE = LENGTH/2。正确初始化应按样本数对应的字节数计算:
# 初始化totaldata为LENGTH秒音频对应的字节数:样本数=RATE*LENGTH,每个样本2字节 totaldata = bytearray(RATE * LENGTH * 2)
修正后,转换为numpy数组的时间长度会是完整的2秒,y轴显示也会正常。
内容的提问来源于stack exchange,提问作者Pd-L2Ork
相关产品推荐
相关产品推荐

