当前位置: 首页 > ai >正文

VS2022+QT6.7+Multimedia(捕获Windows音频数据,生成实时频谱)

目录

一、前言

二、代码详解

三、gitee完整项目下载


一、前言

实现的功能:

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_OBJECTpublic: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_OBJECTpublic: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、缓冲区数据不足,不执行FFTif (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完整项目下载

https://gitee.com/zjq11223344/qt-widgets-application4https://gitee.com/zjq11223344/qt-widgets-application4

http://www.xdnf.cn/news/18895.html

相关文章:

  • 浅谈JMeter Listener
  • 安宝特方案丨AR异地专家远程支持平台,适合:机电运维、应急处置、监造验收
  • esp32c2 at 请问通过HTTPS进行OTA升级的AT命令流程有吗?
  • ERNIE 学习
  • Linux中Java后端调用外部进程 未处理后台输出流 导致io阻塞问题解决方法
  • Mac训练大模型:MLX-LM框架LoRA训练Qwen3并集成SwanLab进行可视化
  • VMware + Ubuntu 桥接模式不能联网 的常见原因、排查思路和解决步骤
  • leetcode 3446. 按对角线进行矩阵排序 中等
  • 与trae携手,构建owtb一体化物流平台之--需求文档V0.3
  • 第五章:Go运行时、内存管理与性能优化之Go垃圾回收机制 (GC) 深入
  • UDS NRC24
  • AI智能农业监测系统深度解读:从大田作物管理到病虫害预警,破解传统农业增产难题
  • 终极实战 - 全链路排查一次“502 Bad Gateway”
  • 从用户视角出发:如何提升B端产品的操作效率?
  • 【第四章】BS 架构测试全解析:从功能验证到问题定位​
  • 使用 logging 模块生成 .log 文件
  • SMU算法与人工智能创新实践班SMU2025 Summer 7th 参考题解
  • npm install 安装离线包的方法
  • 光谱相机在雾霾监测中有何优势?
  • ABeam中国 | 中国汽车市场(5)——软件定义汽车(SDV)的智能化应用场景
  • MATLAB中的蛙跳算法实现
  • Android Glide插件化开发实战:模块化加载与自定义扩展
  • 从0开始搭建一个前端项目(vue + vite + typescript)
  • AI驱动企业数字化转型:解码未来三年的智能化变革密码
  • 深度学习④【经典卷积神经网络演进:从LeNet到ResNet(重要意义)的架构革命】
  • 【目标检测】论文阅读6
  • nvme ,文件系统、namespace、LBA,文件名的浅浅理解
  • 解决Visual Studio中UWP设计器无法显示的问题:需升级至Windows 11 24H2
  • SynClub-百度在海外推出的AI社交产品
  • Elasticsearch 启动反复重启排查实录:从“内存不足”到“vm.max\_map\_count 过小”