You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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. 如何平滑频谱图边缘的尖峰?
    1. 是否因麦克风读取被计算和绘图中断导致丢包,进而产生尖峰?
    1. 为何单块音频与1秒音频的z轴范围差异巨大?
    1. 为何麦克风返回的块大小是请求的2倍(请求1024样本,实际2048字节)?
    1. 为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 09:35:09