在 Qt5 中实现扬声器(或麦克风)的音频频谱图,通常涉及以下几个核心技术点:
1. 使用 `QAudioInput` 或 `QAudioOutput` 获取音频数据(本例中我们以采集麦克风输入为例,用于分析频谱)。
2. 对采集到的时域音频信号进行 **快速傅里叶变换(FFT)**,转换为频域数据,得到频谱。
3. 使用 `QPainter`、`QWidget` 或 `QGraphicsView` 绘制频谱图,推荐使用 `QCustomPlot` 或自定义 `QWidget` 实现高效绘图。
下面是一个完整的 C++ 示例代码,展示如何在 Qt5 中实现一个实时音频频谱图(基于麦克风输入):
---
### ✅ 功能说明:
- 使用默认音频输入设备采集 PCM 数据。
- 使用 FFT(通过 KissFFT 库)将时域数据转为频域。
- 在自定义 `SpectrumWidget` 中实时绘制频谱柱状图。
> 注意:KissFFT 是一个轻量级的 FFT 库,需提前集成。也可以用 FFTW,但 KissFFT 更适合嵌入式和 Qt 项目。
---
### 步骤 1:添加依赖(KissFFT)
请下载 [KissFFT](https://github.com/mborgerding/kissfft) 并将其加入项目,或使用以下简化版本的复数结构与 FFT 调用方式(此处使用静态链接方式示意)。
---
### 主要代码如下(C++ / Qt5):
```cpp
// spectrumwidget.h
#ifndef SPECTRUMWIDGET_H
#define SPECTRUMWIDGET_H
#include <QWidget>
#include <QVector>
class SpectrumWidget : public QWidget
{
Q_OBJECT
public:
explicit SpectrumWidget(QWidget *parent = nullptr);
void setSpectrumData(const QVector<double> &spectrum);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QVector<double> m_spectrum;
};
#endif // SPECTRUMWIDGET_H
```
```cpp
// spectrumwidget.cpp
#include "spectrumwidget.h"
#include <QPainter>
#include <QColor>
SpectrumWidget::SpectrumWidget(QWidget *parent)
: QWidget(parent), m_spectrum(256, 0.0)
{
setMinimumHeight(200);
setAutoFillBackground(false);
}
void SpectrumWidget::setSpectrumData(const QVector<double> &spectrum)
{
m_spectrum = spectrum;
update(); // 触发重绘
}
void SpectrumWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(), Qt::black);
if (m_spectrum.isEmpty()) return;
const int nBars = m_spectrum.size();
const int barWidth = width() / nBars + 1;
const double maxLevel = 100.0; // 假设能量最大值归一化为100
painter.setPen(Qt::NoPen);
for (int i = 0; i < nBars; ++i) {
double value = m_spectrum[i];
int height = qBound(0, static_cast<int>(height() * value / maxLevel), height());
QColor color(0, 255 * (value / maxLevel), 255); // 颜色随强度变化
painter.fillRect(i * barWidth, height() - height(), barWidth - 1, height, color);
}
}
```
```cpp
// audioinputhandler.h
#ifndef AUDIOINPUTHANDLER_H
#define AUDIOINPUTHANDLER_H
#include <QAudioInput>
#include <QByteArray>
#include <QIODevice>
#include <QObject>
#include <QTimer>
#include "kiss_fft.h"
class AudioInputHandler : public QObject, public QIODevice
{
Q_OBJECT
public:
explicit AudioInputHandler(QObject *parent = nullptr);
void start();
QVector<double> getSpectrum() const { return m_spectrum; }
signals:
void spectrumReady();
private slots:
void processAudio();
protected:
qint64 readData(char *data, qint64 maxlen) override;
qint64 writeData(const char *data, qint64 len) override;
private:
QAudioInput *m_audioInput;
QByteArray m_buffer;
QVector<double> m_spectrum;
kiss_fft_cfg m_fftCfg;
int m_sampleRate;
int m_bufferSize;
void computeFFT(const QVector<qint16> &samples);
};
```
```cpp
// audioinputhandler.cpp
#include "audioinputhandler.h"
#include <QAudioFormat>
#include <QAudioDeviceInfo>
#include <QDebug>
#include <algorithm>
#include <cmath>
// 简化版 KissFFT 接口封装(确保已链接 kissfft)
extern "C" {
#include "kiss_fft.h"
}
AudioInputHandler::AudioInputHandler(QObject *parent)
: QObject(parent), QIODevice(parent),
m_sampleRate(44100), m_bufferSize(1024)
{
QAudioFormat format;
format.setSampleRate(m_sampleRate);
format.setChannelCount(1);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
QAudioDeviceInfo info = QAudioDeviceInfo::defaultInputDevice();
if (!info.isFormatSupported(format)) {
qWarning() << "Warning: Desired format not supported, using nearest.";
format = info.nearestFormat(format);
}
m_audioInput = new QAudioInput(format, this);
open(QIODevice::WriteOnly);
m_fftCfg = kiss_fft_alloc(m_bufferSize, 0, nullptr, nullptr); // 0=forward FFT
m_spectrum.resize(m_bufferSize / 2); // 只取正频率部分
}
void AudioInputHandler::start()
{
m_buffer.clear();
m_audioInput->start(this);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &AudioInputHandler::processAudio);
timer->start(30); // 每30ms处理一次频谱
}
void AudioInputHandler::processAudio()
{
if (m_buffer.size() < m_bufferSize * 2) // 16位 = 2字节每样本
return;
QVector<qint16> samples(m_bufferSize);
const qint16 *rawData = reinterpret_cast<const qint16*>(m_buffer.constData());
for (int i = 0; i < m_bufferSize; ++i) {
samples[i] = rawData[i]; // 单通道直接读取
}
computeFFT(samples);
m_buffer.remove(0, m_bufferSize * 2); // 移动窗口
emit spectrumReady();
}
qint64 AudioInputHandler::readData(char *data, qint64 maxlen)
{
Q_UNUSED(data)
Q_UNUSED(maxlen)
return 0; // 我们不从中读取,只写入
}
qint64 AudioInputHandler::writeData(const char *data, qint64 len)
{
m_buffer.append(data, len);
return len;
}
void AudioInputHandler::computeFFT(const QVector<qint16> &samples)
{
// 创建输入输出缓冲区
kiss_fft_cpx *in = new kiss_fft_cpx[m_bufferSize];
kiss_fft_cpx *out = new kiss_fft_cpx[m_bufferSize];
for (int i = 0; i < m_bufferSize; ++i) {
in[i].r = static_cast<qint16>(samples[i]) / 32768.0f; // 归一化到 [-1,1]
in[i].i = 0.0f;
}
// 执行 FFT
kiss_fft(m_fftCfg, in, out);
// 计算幅值并填充频谱(仅前 N/2 项有意义)
for (int i = 0; i < m_bufferSize / 2; ++i) {
float re = out[i].r;
float im = out[i].i;
float mag = std::sqrt(re * re + im * im);
// 对数缩放增强视觉效果
m_spectrum[i] = 20.0f * std::log10(mag + 1e-5f) + 100; // 偏移显示
if (m_spectrum[i] < 0) m_spectrum[i] = 0;
}
delete[] in;
delete[] out;
}
```
```cpp
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "spectrumwidget.h"
#include "audioinputhandler.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
private slots:
void updateSpectrum();
private:
SpectrumWidget *m_spectrumWidget;
AudioInputHandler *m_audioInputHandler;
};
```
```cpp
// mainwindow.cpp
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QWidget>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
m_spectrumWidget = new SpectrumWidget(this);
setCentralWidget(m_spectrumWidget);
m_audioInputHandler = new AudioInputHandler(this);
connect(m_audioInputHandler, &AudioInputHandler::spectrumReady,
this, &MainWindow::updateSpectrum);
m_audioInputHandler->start();
}
void MainWindow::updateSpectrum()
{
m_spectrumWidget->setSpectrumData(m_audioInputHandler->getSpectrum());
}
```
```cpp
// main.cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
```
---
### 🔧 编译说明:
1. 将 `kissfft` 源码加入项目(`kiss_fft.c`, `kiss_fft.h` 等),并在 `.pro` 文件中包含:
```pro
SOURCES += \
main.cpp \
spectrumwidget.cpp \
audioinputhandler.cpp \
mainwindow.cpp \
kiss_fft.c
HEADERS += \
spectrumwidget.h \
audioinputhandler.h \
mainwindow.h \
kiss_fft.h
```
2. 设置编译器支持 C++11:
```pro
CONFIG += c++11
```
---
### 📌 解释:
- **QAudioInput**:从麦克风捕获原始 PCM 数据。
- **QIODevice 子类**:作为音频数据的目标设备,接收实时音频流。
- **FFT 分析**:将时域波形转换为频率成分,体现各频率的能量强度。
- **SpectrumWidget**:自定义绘图部件,动态显示柱状频谱图。
- **定时刷新**:每 30ms 更新一次画面,保证流畅性。
---
### ⚠️ 注意事项:
- 若想用于“扬声器播放内容”的频谱(即回放监控),需使用 `QAudioOutput` 的反馈机制或 WASAPI Loopback(Windows 特有),Qt 原生不支持系统声音捕捉,需平台特定 API。
- 当前示例是 **麦克风输入频谱**,若要监听扬声器输出,必须使用操作系统提供的 loopback 录音功能(如 Windows 的 `eRender` 设备)。
- 性能优化可考虑双缓冲、降采样、使用 OpenGL 绘图(Qt Quick + Shader)等。
---
###