目录
一、前言
实现的功能:
1、捕获Windows系统音频,从扬声器播放的声音(网页、应用)。
2、FFT傅里叶将频域转为分贝。
3、在QChart中用QBarSet生成20段实时频谱图,0Hz-20kHz。
(主线程只负责更新UI,子线程处理音频数据,做FFT变换)
(代码本身并不复杂,只是涉及的东西比较多,多梳理几遍就能理解)
准备工作:
1、您需要在Qt Maintenance Tool中安装Qt Multimedia模块,并且在VS项目属性中引用该模块


2、您需要正确安装 #include <fftw3.h> // FFT库
FFTW3在VS环境下的安装(亲测)_vs2022 fftw-CSDN博客
实现图片效果如下:

实现视频效果如下:
QT windows频谱分析器
二、代码详解
程序执行流程图:
1、用到的头文件:
#include <QAudioInput>
#include <QAudioFormat>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioSource>
#include <QIODevice>
#include <QAudioBuffer> //音频帧
#include <cmath>
#include <QTimer>
#include <QThread>
#include <QHBoxLayout>
#include <QChart>
#include <QChartView>
#include <QBarSeries>
#include <QBarSet>
#include <QBarCategoryAxis>
#include <QValueAxis>
#include <fftw3.h> // FFT库
2、用到的两个类:QtWidgetsApplication4 主线程类 、 AudioQThread 子线程类
class QtWidgetsApplication4 : public QWidget
{
Q_OBJECT
public:
QtWidgetsApplication4(QWidget* parent = nullptr);
~QtWidgetsApplication4();
QBarSeries* m_series; // 频谱柱状图数据集
QChart* m_chart; // 频谱图表
QStringList bandCategories; // 存储20个频段的X轴标签
QBarCategoryAxis* m_axisX; // X轴
QValueAxis* m_axisY; // Y轴
QChartView* m_chartView; // 频谱图表视图
QBarSet* m_barSet; // 频谱柱状图
QString getBandLabel(int); // 获取20个频段的标签
void paintEvent_init(); // 频谱图初始化
public slots:
void update_chart(QVector<double>); // 更新图表 30ms/次
private:
Ui::QtWidgetsApplication4Class ui;
};
class AudioQThread : public QObject
{
Q_OBJECT
public:
AudioQThread();
~AudioQThread();
void work(); // 线程处理函数
void readAll(); // 读取音频数据
QIODevice* audioIODevice = nullptr; // 录音源
QAudioFormat formatAudio; // 音频格式
void updateFFT(); // 计算FFT,将频域转为分贝
signals:
void updatePlot(QVector<double>); // 更新频谱图
private:
// FFT相关变量
// FFT核心参数(确保覆盖20kHz:采样率≥44.1kHz,FFT点数≥2048)
static const int FFT_SIZE = 2048; // FFT点数(2^11,44.1kHz采样率下频率分辨率≈21.5Hz)
fftw_complex* fftIn; // FFT输入(复数数组)
fftw_complex* fftOut; // FFT输出(复数数组)
fftw_plan fftPlan; // FFT计划
QVector<double> fftFreqs; // FFT对应的实际频率(0Hz~奈奎斯特频率)
QVector<double> fftDb; // FFT结果(分贝值,对应fftFreqs)
// 20频段配置(对数刻度,覆盖20Hz-20kHz)
static const int BAND_COUNT = 20; // 频段数量
QVector<QPair<double, double>> bandRanges; // 每个频段的[起始频率, 结束频率]
QVector<double> bandEnergy; // 每个频段的平均能量(分贝值)
// 辅助变量
QTimer* updateTimer; // 刷新定时器(30ms/次,≈33fps)
QVector<qint16> audioBuffer;// 音频缓冲区(存储待FFT的时域数据)
double nyquistFreq=NULL; // 奈奎斯特频率(采样率/2,确保≥20kHz)
};
3、在主类构造中创建子线程,并绑定updatePlot信号和update_chart槽来更新频谱图
paintEvent_init(); //频谱图初始化
QThread* thread = new QThread(); //创建线程
AudioQThread* st = new AudioQThread(); //创建对象
st->moveToThread(thread); //将对象移动到线程中
connect(thread, &QThread::started, st, &AudioQThread::work);
connect(st, &AudioQThread::updatePlot, this, &QtWidgetsApplication4::update_chart);
connect(ui.pushButton, &QPushButton::clicked, this, [=]() {
thread->start(); //启动线程
});
4、在子线程构造中初始化定时器、FFT资源、音频缓冲区
// 子线程的构造中初始化音频处理线程
AudioQThread::AudioQThread()
{
// 1、 初始化20频段范围(对数刻度,覆盖20Hz-20kHz,符合人耳听觉)
const double bandStarts[] = { 20, 28, 40, 56, 80, 112, 160, 224, 320, 448,
640, 896, 1280, 1792, 2560, 3584, 5120, 7168, 10240, 14336 };
const double bandEnds[] = { 28, 40, 56, 80, 112, 160, 224, 320, 448, 640,
896, 1280, 1792, 2560, 3584, 5120, 7168, 10240, 14336, 20000 };
for (int i = 0; i < BAND_COUNT; ++i) {
bandRanges.append({ bandStarts[i], bandEnds[i] });
}
bandEnergy.resize(BAND_COUNT, -60.0); // 初始化能量为-60dB(噪声阈值)
// 2、 初始化FFT资源(FFT_SIZE=2048,确保频率分辨率足够)
fftIn = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * FFT_SIZE);
fftOut = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * FFT_SIZE);
fftPlan = fftw_plan_dft_1d(FFT_SIZE, fftIn, fftOut, FFTW_FORWARD, FFTW_ESTIMATE);
// 3、 初始化刷新定时器(控制可视化帧率)
updateTimer = new QTimer(this);
updateTimer->setInterval(30);
connect(updateTimer, &QTimer::timeout, this, &AudioQThread::updateFFT);
// 4、 初始化音频缓冲区
audioBuffer.reserve(FFT_SIZE);
}
5、在子线程中设置音频格式、音频输入设备、创建音源,绑定readAll函数,获取设备的音频数据
// 子线程,获取音频数据并执行FFT
void AudioQThread::work()
{
// 1、使用默认音频格式
formatAudio.setSampleRate(44100);
formatAudio.setChannelCount(2);
formatAudio.setSampleFormat(QAudioFormat::Int16);
nyquistFreq = formatAudio.sampleRate() / 2.0;
qInfo() << "采样率:" << formatAudio.sampleRate() << "Hz,奈奎斯特频率:" << nyquistFreq << "Hz";
// 2、初始化FFT频率数组(映射FFT索引到实际频率)
fftFreqs.resize(FFT_SIZE / 2);
double freqStep = formatAudio.sampleRate() / (double)FFT_SIZE; // FFT频率分辨率
for (int i = 0; i < FFT_SIZE / 2; ++i) {
fftFreqs[i] = i * freqStep; // 第i个FFT点对应的实际频率
}
// 3、获取默认音频输入设备
QAudioDevice inputDevice = QMediaDevices::defaultAudioInput();
// 4、创建音频输入
QAudioInput* audioInput = new QAudioInput(inputDevice, this);
// 5、创建音频源
QAudioSource* audioSource = new QAudioSource(inputDevice, formatAudio, this);
// 6、开始录音
audioIODevice = audioSource->start();
// 7、连接readyRead信号
connect(audioIODevice, &QIODevice::readyRead, this, &AudioQThread::readAll);
// 8、启动定时器(FFT和频段计算)
updateTimer->start();
}
6、readAll缓存数据处理,将数据存到audioBuffer缓冲区中
// 转换为16位整数采样点,存入缓冲区(仅保留最新FFT_SIZE个点)
void AudioQThread::readAll()
{
QByteArray data = audioIODevice->readAll();
if (!data.isEmpty()) {
const qint16* samples = reinterpret_cast<const qint16*>(data.constData());
int sampleCount = data.size() / sizeof(qint16);
for (int i = 0; i < sampleCount; ++i) {
audioBuffer.append(samples[i]);
if (audioBuffer.size() > FFT_SIZE) {
audioBuffer.remove(0, audioBuffer.size() - FFT_SIZE);
}
}
}
}
7、FFT将时域->频域->分贝值->存到bandEnergy中->通过updatePlot(bandEnergy);发送到主线程
// 频谱处理、频段计算、通过updatePlot信号在UI中绘制频谱图
void AudioQThread::updateFFT()
{
// 0、缓冲区数据不足,不执行FFT
if (audioBuffer.size() < FFT_SIZE) return;
// 1、 时域数据加汉宁窗(减少频谱泄漏)
for (int i = 0; i < FFT_SIZE; ++i) {
double hanning = 0.5 * (1 - cos(2 * M_PI * i / (FFT_SIZE - 1)));
fftIn[i][0] = audioBuffer[i] * hanning; // 实部(时域数据)
fftIn[i][1] = 0.0; // 虚部(无)
}
// 2、 执行FFT,转换为频域数据
fftw_execute(fftPlan);
// 3、 FFT结果转换为分贝值(仅保留前半段,对应0Hz~奈奎斯特频率)
fftDb.resize(FFT_SIZE / 2);
for (int i = 0; i < FFT_SIZE / 2; ++i) {
double magnitude = sqrt(fftOut[i][0] * fftOut[i][0] + fftOut[i][1] * fftOut[i][1]);
fftDb[i] = (magnitude > 1e-8) ? 20 * log10(magnitude) : -120.0; // 避免log(0)
}
// 4、 计算20个频段的平均能量(核心:按频段范围分组FFT数据)
bandEnergy.fill(-60.0); // 重置能量
for (int bandIdx = 0; bandIdx < BAND_COUNT; ++bandIdx) {
double bandStart = bandRanges[bandIdx].first;
double bandEnd = bandRanges[bandIdx].second;
double energySum = 0.0;
int count = 0;
// 遍历FFT频率点,找到当前频段内的所有点
for (int fftIdx = 0; fftIdx < fftFreqs.size(); ++fftIdx) {
double freq = fftFreqs[fftIdx];
if (freq >= bandStart && freq < bandEnd) {
energySum += fftDb[fftIdx]; // 累加该频段内的分贝值
count++;
}
}
// 计算该频段的平均能量(若有有效点)
if (count > 0) {
bandEnergy[bandIdx] = energySum / count;
}
}
// 5、更新频谱图
updatePlot(bandEnergy);
8、主线程中初始化频谱图,UI文件中只有一个按钮和widget。注意:paintEvent_init一开始就放在构造里面了,程序执行时频谱图就已经初始化了。
// 频谱图初始化
void QtWidgetsApplication4::paintEvent_init()
{
// 1、创建图表布局
QWidget* Widget = ui.widget;
QVBoxLayout* layout = new QVBoxLayout(Widget);
// 2、初始化柱状图数据集
m_barSet = new QBarSet("FFT 分贝值");
m_series = new QBarSeries();
m_series->append(m_barSet);
m_series->setBarWidth(1.0); // 相对宽度,0.0-1.0之间
// 3、创建图表
m_chart = new QChart();
m_chart->addSeries(m_series);
m_chart->setTitle("20频段音频频谱分析(20Hz-20kHz)");
m_chart->setAnimationOptions(QChart::SeriesAnimations); // 添加动画效果
m_chart->legend()->hide(); // 隐藏图例
// 4、创建X轴标签
for (int i = 1; i <= 20; ++i) {
QString bandLabel = getBandLabel(i); //获取x轴标签
bandCategories << bandLabel;
*m_barSet << 0.0; //设置20个柱状图的默认值
}
// 5、创建X轴(频率点)
m_axisX = new QBarCategoryAxis();
m_chart->addAxis(m_axisX, Qt::AlignBottom);
m_axisX->append(bandCategories); // 添加X轴标签
m_series->attachAxis(m_axisX);
m_axisX->setLabelsFont(QFont("Arial", 5)); // 设置字体和字号
// 6、创建Y轴(分贝值)
m_axisY = new QValueAxis();
m_axisY->setTitleText("分贝 (dB)");
m_axisY->setRange(0, 200); // 根据数据范围设置
m_chart->addAxis(m_axisY, Qt::AlignLeft);
m_series->attachAxis(m_axisY);
// 7、创建图表视图
m_chartView = new QChartView(m_chart);
m_chartView->setRenderHint(QPainter::Antialiasing); // 抗锯齿
// 8、将图表视图添加到布局中
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_chartView);
}

9、主线程中直接更新柱状图
void QtWidgetsApplication4::update_chart(QVector<double> bandEnergy)
{
qDebug() << bandEnergy;
// 批量更新数据
for (int i = 0; i < bandEnergy.size(); ++i) {
// 更新柱状图数据
m_barSet->replace(i, bandEnergy[i]);
}
}
10、X轴的标签在这个函数中创建,使用m_axisX->append(bandCategories); // 添加X轴标签
// 自定义辅助函数:获取第i个频段的频率范围标签(1~20对应20Hz-20kHz的对数划分)
QString QtWidgetsApplication4::getBandLabel(int bandIndex)
{
// 使用与AudioQThread中一致的频段范围定义
const QVector<QString> labels = {
"20-28Hz", "28-40Hz", "40-56Hz", "56-80Hz", "80-112Hz",
"112-160Hz", "160-224Hz", "224-320Hz", "320-448Hz", "448-640Hz",
"640-896Hz", "896-1.28kHz", "1.28-1.79kHz", "1.79-2.56kHz", "2.56-3.58kHz",
"3.58-5.12kHz", "5.12-7.17kHz", "7.17-10.24kHz", "10.24-14.3kHz", "14.3-20kHz"
};
if (bandIndex >= 1 && bandIndex <= 20) {
return labels[bandIndex - 1];
}
return QString("柱 %1").arg(bandIndex);
}
三、gitee完整项目下载
4587

被折叠的 条评论
为什么被折叠?



