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

PyQtgraph叠加折线散点图时仅折线更新问题及峰值标注咨询

音频频率分析仪峰值标注问题修复

问题背景

我正在基于参考视频中的脚本开发音频频率分析仪,参考视频链接:https://www.youtube.com/watch?v=RHmTgapLu4s&t=84s

程序基础逻辑:

  • 实时采集麦克风输入数据绘制时域波形
  • 对波形执行傅里叶变换后,在第二个绘图区持续动态更新频谱结果

目前遇到两个待解决问题:

  1. 尝试在FFT频谱图上叠加散点图实现峰值高亮标注,但仅首次渲染能生成散点,后续散点始终无法动态更新。经排查峰值坐标数据可正常更新,但调用self.set_plotdata()方法时仅折线图会刷新,散点图无变化。
  2. 需要为散点添加对应频率值的数据标签,未找到PyQtgraph中实现该功能的相关文档。

完整原始代码

# -*- coding: utf-8 -*-
"""
Created on Mon Jun 27 16:35:59 2022

@author: corn-
"""

import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg

import struct
import pyaudio
from scipy.fftpack import fft
from scipy.signal import find_peaks

import sys
import time


class AudioStream(object):
    def __init__(self):

        # pyqtgraph stuff
        pg.setConfigOptions(antialias=True)
        self.traces = dict()
        self.app = QtGui.QApplication(sys.argv)
        self.win = pg.GraphicsWindow(title='Spectrum Analyzer')
        self.win.setWindowTitle('Spectrum Analyzer')
        self.win.setGeometry(5, 115, 1910, 1070)

        #X labels for time domain graph
        wf_xlabels = [(0, '0'), (2048, '2048'), (4096, '4096')]
        wf_xlabel2 = 'time'
        wf_xaxis = pg.AxisItem(orientation='bottom')
        wf_xaxis.setTicks([wf_xlabels])
        wf_xaxis.setLabel(text = wf_xlabel2)
        #Y labels for time domain graph
        wf_ylabels = [(0, '0'), (127, '128'), (255, '255')]
        wf_yaxis = pg.AxisItem(orientation='left')
        wf_yaxis.setTicks([wf_ylabels])
        
        #X labels for frequency domain graph
        sp_xlabels = [
            (np.log10(10), '10'), (np.log10(100), '100'),
            (np.log10(1000), '1000'), (np.log10(22050), '22050')
        ]
        sp_xlabel2 = 'log frequency'
        sp_xaxis = pg.AxisItem(orientation='bottom')
        sp_xaxis.setTicks([sp_xlabels])
        sp_xaxis.setLabel(sp_xlabel2)

        self.waveform = self.win.addPlot(
            title='WAVEFORM', row=1, col=1, axisItems={'bottom': wf_xaxis, 'left': wf_yaxis},
        )
        self.spectrum = self.win.addPlot(
            title='SPECTRUM', row=2, col=1, axisItems={'bottom': sp_xaxis},
        )
        
        
        

        # pyaudio stuff
        self.FORMAT = pyaudio.paInt16
        self.CHANNELS = 1
        self.RATE = 44100
        self.CHUNK = 1042 * 4

        self.p = pyaudio.PyAudio()
        self.stream = self.p.open(
            format=self.FORMAT,
            channels=self.CHANNELS,
            rate=self.RATE,
            input=True,
            output=True,
            frames_per_buffer=self.CHUNK,
        )
        # waveform and spectrum x points
        self.x = np.arange(0, 2 * self.CHUNK, 2)
        self.f = np.linspace(0, self.RATE / 2, self.CHUNK // 2)

    def start(self):
        if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
            QtGui.QApplication.instance().exec_()

    def set_plotdata(self, name, data_x, data_y, peak_x, peak_y):
        if name in self.traces:
            self.traces[name].setData(data_x, data_y)
            
        else:
            if name == 'waveform':
                self.traces[name] = self.waveform.plot(pen='c', width=3)
                self.waveform.setYRange(0, 255, padding=0)
                self.waveform.setXRange(0, 2 * self.CHUNK, padding=0.005)
            if name == 'spectrum':
                self.traces[name] = self.spectrum.plot(pen='m', width=50)
                self.spectrum.setLogMode(x=True, y=True)
                self.spectrum.setYRange(-4, 0, padding=0)
                self.spectrum.setXRange(
                    np.log10(20), np.log10(self.RATE / 2), padding=0.005)
                #plot the peak tips
                self.peakTops =  self.spectrum.plot()
                self.peakTops.setData(peak_x, peak_y, pen=None, symbol='o')
             
              
                
    def get_peaks(self, name, data_x, data_y):
        
        if name == 'spectrum':
            peak_idx, _ = find_peaks(data_y, height=(0.1))
            peak_height = data_y[peak_idx] 
            return (peak_idx, peak_height)
                
    def update(self):
        wf_data = self.stream.read(self.CHUNK)
        wf_data = struct.unpack(str(2 * self.CHUNK) + 'B', wf_data)
        wf_data = np.array(wf_data, dtype='b')[::2] + 128
        temp_x = 0 
        temp_y = 0 
        self.set_plotdata(name='waveform', data_x=self.x, data_y=wf_data, peak_x = temp_x, peak_y = temp_y)

        sp_data = fft(np.array(wf_data, dtype='int8') - 128)
        sp_data = np.abs(sp_data[0:int(self.CHUNK / 2)]
                         ) * 2 / (128 * self.CHUNK)
        [peak_idx, peak_height] = self.get_peaks(name = 'spectrum', data_x = self.f, data_y = sp_data)
        self.set_plotdata(name='spectrum', data_x=self.f, data_y=sp_data, peak_x = peak_idx, peak_y = peak_height)

    def animation(self):
        timer = QtCore.QTimer()
        timer.timeout.connect(self.update)
        timer.start(20)
        self.start()


if __name__ == '__main__':

    audio_app = AudioStream()
    audio_app.animation()

问题原因

  1. 散点不更新:self.peakTops的初始化和数据赋值写在set_plotdata()的else分支中,该分支仅在第一次创建频谱曲线时执行。后续更新时走if name in self.traces分支,仅更新了频谱折线的数据,没有调用self.peakTops.setData()传入新的峰值坐标,所以散点一直停留在首次渲染的状态。
  2. 坐标不匹配:传给散点的x值是峰值在数组中的索引,不是实际频率值,在开启对数x轴的频谱图上坐标完全错位。
  3. 频率标签:PyQtgraph没有内置的散点标签接口,需要用pg.TextItem手动创建、管理文本标注。

修复方案

1. 初始化逻辑调整

在__init__方法中创建完频谱绘图区后,直接初始化峰值散点对象和标签存储列表,不要放在set_plotdata的首次创建分支里:

self.spectrum = self.win.addPlot(
    title='SPECTRUM', row=2, col=1, axisItems={'bottom': sp_xaxis},
)
# 初始化峰值散点,设置为红色圆点
self.peakTops = self.spectrum.plot(pen=None, symbol='o', symbolBrush='r', symbolSize=8)
# 存储峰值标签,方便后续更新时清空旧标签
self.peak_labels = []

2. 同步更新散点数据

修改set_plotdata()方法,在更新频谱折线数据时同步更新峰值散点,同时处理标签的清空和重绘:

def set_plotdata(self, name, data_x, data_y, peak_x, peak_y):
    if name in self.traces:
        self.traces[name].setData(data_x, data_y)
        if name == 'spectrum':
            # 更新峰值散点数据
            self.peakTops.setData(peak_x, peak_y)
            # 清空上一帧的所有标签
            for label in self.peak_labels:
                self.spectrum.removeItem(label)
            self.peak_labels.clear()
            # 为每个新峰值添加频率标签
            for freq, amp in zip(peak_x, peak_y):
                # 对数轴下坐标要取log10后赋值
                text_label = pg.TextItem(f"{freq:.0f} Hz", color=(255,255,255), anchor=(0.5, 1.2))
                text_label.setPos(np.log10(freq), np.log10(amp))
                self.spectrum.addItem(text_label)
                self.peak_labels.append(text_label)
            
    else:
        if name == 'waveform':
            self.traces[name] = self.waveform.plot(pen='c', width=3)
            self.waveform.setYRange(0, 255, padding=0)
            self.waveform.setXRange(0, 2 * self.CHUNK, padding=0.005)
        if name == 'spectrum':
            # 原代码pen宽度设为50会完全挡住峰值点,改为3
            self.traces[name] = self.spectrum.plot(pen='m', width=3)
            self.spectrum.setLogMode(x=True, y=True)
            self.spectrum.setYRange(-4, 0, padding=0)
            self.spectrum.setXRange(
                np.log10(20), np.log10(self.RATE / 2), padding=0.005)

3. 修正峰值坐标传值

修改update()方法中峰值传参逻辑,把数组索引转为实际频率值再传入:

[peak_idx, peak_height] = self.get_peaks(name = 'spectrum', data_x = self.f, data_y = sp_data)
# 索引转实际频率值
peak_freq = self.f[peak_idx]
self.set_plotdata(name='spectrum', data_x=self.f, data_y=sp_data, peak_x = peak_freq, peak_y = peak_height)

修改后运行即可看到峰值点随音频实时更新,每个峰值上方会显示对应频率值。

内容的提问来源于stack exchange,提问作者user18814028

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 19:09:18