从默认输入设备实时绘制频谱图失败:画面未随数据更新求助
实时滚动频谱图绘制异常排查与修复
问题描述
尝试使用matplotlib的FuncAnimation模块,基于默认音频输入设备的数据实时绘制从右向左滚动的频谱图,但图表不随缓冲区新数据更新,画面呈现异常。
原代码
from matplotlib.animation import FuncAnimation import matplotlib.pyplot as plt import numpy as np import rtmixer import math import sounddevice as sd FRAMES_PER_BUFFER = 512 NFFT = 4096 NOVERLAP = 128 window = 100000 downsample = 10 channels = 1 def create_specgram(frame): global plotdata spec, freqs, t = plt.mlab.specgram(plotdata[:,-1], Fs=samplerate) xmin, xmax = np.min(t) - pad_xextent, np.max(t) + pad_xextent extent = xmin, xmax, freqs[0], freqs[-1] arr = np.flipud(10. * np.log10(spec)) return arr, extent def update_plot(frame): global plotdata while ringBuffer.read_available >= FRAMES_PER_BUFFER: read, buf1, buf2 = ringBuffer.get_read_buffers(FRAMES_PER_BUFFER) assert read == FRAMES_PER_BUFFER buffer = np.frombuffer(buf1, dtype='float32') buffer.shape = -1, channels buffer = buffer[::downsample] assert buffer.base.base == buf1 shift = len(buffer) plotdata = np.roll(plotdata, -shift, axis=0) plotdata[-shift:, :] = buffer ringBuffer.advance_read_index(FRAMES_PER_BUFFER) arr, _ = create_specgram(frame) image.set_array(arr) return image, device_info = sd.query_devices(device=None, kind='input') samplerate = device_info['default_samplerate'] pad_xextent = (NFFT - NOVERLAP) / samplerate / 2 length = int(window * samplerate / (1000 * downsample)) plotdata = np.zeros((length, channels)) stream = rtmixer.Recorder(device=None, channels=channels, blocksize=FRAMES_PER_BUFFER, latency='low', samplerate=samplerate) ringbufferSize = 2**int(math.log2(3 * samplerate)) ringBuffer = rtmixer.RingBuffer(channels * stream.samplesize, ringbufferSize) fig, ax = plt.subplots(figsize=(10, 5)) arr, extent = create_specgram(0) image = plt.imshow(arr, animated=True, extent=extent, aspect='auto') fig.colorbar(image) ani = FuncAnimation(fig, update_plot, interval=1, blit=True, cache_frame_data=False) with stream: ringBuffer = rtmixer.RingBuffer(channels * stream.samplesize, ringbufferSize) action = stream.record_ringbuffer(ringBuffer) plt.show()
错误分析
- RingBuffer实例不匹配:在
with stream:代码块内重新初始化了ringBuffer,导致音频流写入的是新缓冲区,而update_plot读取的是之前创建的旧缓冲区,两者完全脱节,没有数据流通。 - 频谱计算的数据源错误:
plotdata[:,-1]索引错误,因为plotdata的形状是(length, channels),当channels=1时应该用plotdata[:,0],否则会引发索引越界或取到空数据。 - 未实现滚动的时间轴逻辑:每次调用
create_specgram都基于全部plotdata计算频谱,时间轴范围固定在初始状态,且update_plot仅更新图像数据,未调整extent或轴范围,无法呈现滚动效果。 plt.mlab.specgram的使用方式不符合滚动需求:该函数默认对整个输入数组做全局频谱分析,而非滑动窗口的实时片段分析,导致画面无法随新数据更新滚动。
修复后的代码
from matplotlib.animation import FuncAnimation import matplotlib.pyplot as plt import numpy as np import rtmixer import math import sounddevice as sd FRAMES_PER_BUFFER = 512 NFFT = 4096 NOVERLAP = 128 # 调整窗口长度为频谱分析所需的合适大小,避免过大的历史数据 WINDOW_DURATION = 2 # 秒 downsample = 10 channels = 1 def create_specgram(audio_data, samplerate, time_offset): # 使用scipy.signal.spectrogram替代已弃用的plt.mlab.specgram from scipy.signal import spectrogram freqs, t, spec = spectrogram( audio_data, fs=samplerate, nperseg=NFFT, noverlap=NOVERLAP, mode='magnitude' ) # 计算当前时间轴偏移,实现滚动 t += time_offset # 转换为dB,加小值避免log(0)报错 spec_db = 10 * np.log10(spec + 1e-10) arr = np.flipud(spec_db) # 固定x轴显示范围(最近WINDOW_DURATION秒) x_min = time_offset - WINDOW_DURATION x_max = time_offset extent = [x_min, x_max, freqs[0], freqs[-1]] return arr, extent, freqs def update_plot(frame): global plotdata, current_time # 读取音频缓冲区数据 while ringBuffer.read_available >= FRAMES_PER_BUFFER: read, buf1, buf2 = ringBuffer.get_read_buffers(FRAMES_PER_BUFFER) assert read == FRAMES_PER_BUFFER buffer = np.frombuffer(buf1, dtype='float32') buffer.shape = (-1, channels) buffer = buffer[::downsample] shift = len(buffer) # 更新滚动缓冲区 plotdata = np.roll(plotdata, -shift, axis=0) plotdata[-shift:, :] = buffer ringBuffer.advance_read_index(FRAMES_PER_BUFFER) # 更新当前时间(按降采样后的速率计算) current_time += shift / (samplerate / downsample) # 取最新的音频数据段用于频谱分析,确保长度足够 data_len = len(plotdata[:,0]) analyze_data = plotdata[max(0, data_len - NFFT*2):, 0] if data_len > NFFT else plotdata[:,0] arr, extent, freqs = create_specgram(analyze_data, samplerate/downsample, current_time) image.set_array(arr) image.set_extent(extent) # 固定y轴范围(可选,根据需求调整) ax.set_ylim(freqs[0], freqs[-1]) return image, # 初始化音频设备 device_info = sd.query_devices(device=None, kind='input') samplerate = device_info['default_samplerate'] # 计算缓冲区长度(降采样后的) length = int(WINDOW_DURATION * samplerate / downsample) + NFFT plotdata = np.zeros((length, channels)) current_time = 0 # 初始化音频流和RingBuffer stream = rtmixer.Recorder( device=None, channels=channels, blocksize=FRAMES_PER_BUFFER, latency='low', samplerate=samplerate ) ringbufferSize = 2**int(math.log2(3 * samplerate)) ringBuffer = rtmixer.RingBuffer(channels * stream.samplesize, ringbufferSize) # 初始化图表 fig, ax = plt.subplots(figsize=(10, 5)) # 初始频谱计算 init_data = plotdata[:,0] arr, extent, freqs = create_specgram(init_data, samplerate/downsample, current_time) image = plt.imshow(arr, animated=True, extent=extent, aspect='auto', origin='lower') fig.colorbar(image, label='dB') ax.set_xlabel('Time (s)') ax.set_ylabel('Frequency (Hz)') # 创建动画 ani = FuncAnimation( fig, update_plot, interval=30, # 调整为更合理的刷新间隔,避免过度占用资源 blit=True, cache_frame_data=False ) # 启动音频流并显示图表 with stream: action = stream.record_ringbuffer(ringBuffer) plt.show()
修复关键点说明
- 统一RingBuffer实例:移除
with stream:内的重复初始化,确保音频流和绘图逻辑使用同一个缓冲区。 - 替换频谱计算函数:用
scipy.signal.spectrogram替代已弃用的plt.mlab.specgram,更稳定且功能更完整。 - 实现滚动时间轴:通过
time_offset参数跟踪当前时间,每次更新时调整extent的x轴范围,固定显示最近一段时间的数据,模拟滚动效果。 - 优化缓冲区和数据读取:调整
plotdata长度为适合实时分析的大小,仅取最新的片段计算频谱,避免冗余计算。 - 调整动画间隔:将
interval从1ms改为30ms,平衡刷新速度和资源占用。
内容的提问来源于stack exchange,提问作者DeltronPro
相关产品推荐
相关产品推荐

