QT6实现音频可视化的示例代码
作者:Beyond欣
这篇文章主要为大家详细介绍了一个基于QT6的音频可视化Demo,文中的示例代码,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下
QT6音频可视化
需要做一个简易的音频可视化,网上找的案例都是基于QT5的。很多API和类都弃用了,分享一个基于QT6的音频可视化Demo。
1 获取QAudioBuffer
QAudioBuffer 存储一系列音频帧,每个帧包含一个时间点上所有通道的采样值。采样格式通过 QAudioFormat 指定,包括采样率、通道数、样本大小等。在多通道(例如立体声)音频中,采样数据通常是交错存储的,即每个帧包含各个通道的采样值依次排列。
QT5 通过 QAudioProbe 来获取帧数据,QT6 提供了 QAudioBufferOutput 来获取帧数据。
QMediaPlayer * player = new QMediaPlayer(this); QAudioOutput * audioOutput = new QAudioOutput; player->setAudioOutput(audioOutput); player->setSource(QUrl::fromLocalFile("./111.flac")); QAudioFormat formatAudio; formatAudio.setSampleRate(44100); formatAudio.setChannelCount(1); formatAudio.setSampleFormat(QAudioFormat::UInt8); QAudioBufferOutput * buffer = new QAudioBufferOutput(formatAudio, this); player->setAudioBufferOutput(buffer); player->play(); connect(buffer, &QAudioBufferOutput::audioBufferReceived, this, &Widget::OnAudioBufferReceived);
2 时域转频域
Qt 自身未集成 FFT 计算,其自带的 Demo 里使用的 FFTReal。FFTW 、FFTReal、KISSFFT 都试了下,最后用的 KISSFFT。
int N = audioData.size(); kiss_fft_cfg cfg = kiss_fft_alloc(N, 0, NULL, NULL); // 准备输入和输出 std::vector<kiss_fft_cpx> input(N); std::vector<kiss_fft_cpx> output(N); for (int i = 0; i < N; ++i) { input[i].r = audioData[i]; input[i].i = 0.0f; } // FFT kiss_fft(cfg, input.data(), output.data()); free(cfg); // 计算 FFT 结果的幅值 float frameMaxMagnitude = 0.0f; std::vector<float> magnitude(N / 2); for (int i = 0; i < N / 2; ++i) { magnitude[i] = std::sqrt(output[i].r * output[i].r + output[i].i * output[i].i); frameMaxMagnitude = qMax(frameMaxMagnitude, magnitude[i]); } // 更新全局最大值(带平滑) GlobalMaxMagnitude = qMax(GlobalMaxMagnitude * 0.9f, frameMaxMagnitude); // 计算动态范围的有效最大值 float effectiveMaxMagnitude = qMax(frameMaxMagnitude, GlobalMaxMagnitude * DynamicThreshold); // 归一化 for (int i = 0; i < N / 2; ++i) { magnitude[i] = (magnitude[i] / effectiveMaxMagnitude) * 100.0f; }
3 频域结果分组
简单区分下高中低频率,不同频率占比不同
// 分组频段范围 int sampleRate = 44100; int lowEnd = 400; int midEnd = 4000; int highEnd = 20000; int lowBins = lowEnd * N / sampleRate; int midBins = midEnd * N / sampleRate; int highBins = highEnd * N / sampleRate; auto addBands = [&](int startBin, int endBin, int bands) { int binRange = (endBin - startBin) / bands; // 每个小频段包含的 bin 数量 for (int i = 0; i < bands; ++i) { float bandSum = 0; int bandStart = startBin + i * binRange; int bandEnd = bandStart + binRange; for (int j = bandStart; j < bandEnd && j < magnitude.size(); ++j) { bandSum += magnitude[j]; } freqData << bandSum; } }; addBands(0, lowBins, 5); addBands(lowBins, midBins, 10); addBands(midBins, highBins, 5);
4 QChart可视化
QChart 可以同时绘制 柱状图和曲线图,使用 QStackedBarSeries
和 QSplineSeries
constexpr int FreqAxisRange = 512; constexpr int TimeAxisRange = 1024; constexpr int FreqDomainSum = 20; constexpr int FPS = 30; startTimer(FPS); void TimeWaveformWidget::SetSeriesData(const QList<int> & freqData, const QList<int> & timeData) { m_FreqData = std::move(freqData); m_TimeData = std::move(timeData); } void TimeWaveformWidget::timerEvent(QTimerEvent * event) { // 更新频域数据 if (m_FreqData.size() == FreqDomainSum) { for (int i = 0; i < m_FreqData.size(); ++i) { m_FreqBarSet->replace(i, m_FreqData.at(i)); } } // 更新时域数据 QVector<QPointF> points; int sampleCount = m_TimeData.size(); QVector<QPointF> originalPoints; for (int i = 0; i < sampleCount; ++i) { originalPoints.emplace_back(i, m_TimeData.at(i)); } InterpolateData(originalPoints, points, TimeAxisRange); m_TimeSeries->replace(points); m_Chart->update(); } void TimeWaveformWidget::InitFreqSeries() { for (int i = 0; i < FreqDomainSum; i++) { *m_FreqBarSet << 0; } m_FreqSeries->append(m_FreqBarSet); m_Chart->addSeries(m_FreqSeries); m_Chart->addAxis(m_FreqAxisX, Qt::AlignBottom); m_FreqAxisY->setRange(0, FreqAxisRange); m_Chart->addAxis(m_FreqAxisY, Qt::AlignLeft); m_FreqSeries->attachAxis(m_FreqAxisX); m_FreqSeries->attachAxis(m_FreqAxisY); m_FreqAxisX->setLineVisible(false); m_FreqAxisX->setLabelsVisible(false); m_FreqAxisX->setGridLineVisible(false); m_FreqAxisY->setLineVisible(false); m_FreqAxisY->setLabelsVisible(false); m_FreqAxisY->setGridLineVisible(false); } void TimeWaveformWidget::InitTimeSeries() { m_Chart->addSeries(m_TimeSeries); auto axisX = new QValueAxis; axisX->setRange(0, TimeAxisRange); auto axisY = new QValueAxis; axisY->setRange(0, 256); m_Chart->addAxis(axisX, Qt::AlignBottom); m_TimeSeries->attachAxis(axisX); m_Chart->addAxis(axisY, Qt::AlignLeft); m_TimeSeries->attachAxis(axisY); axisX->setLineVisible(false); axisX->setLabelsVisible(false); axisX->setGridLineVisible(false); axisY->setLineVisible(false); axisY->setLabelsVisible(false); axisY->setGridLineVisible(false); } float TimeWaveformWidget::CatmullRomInterpolate( float p0, float p1, float p2, float p3, float t) { float t2 = t * t; float t3 = t2 * t; float result = 0.5f * ((2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3); return result; } void TimeWaveformWidget::InterpolateData( const QVector<QPointF> & originalPoints, QVector<QPointF> & targetPoints, int targetCount) { int n = originalPoints.size(); if (n < 2) { return; } targetPoints.reserve(targetCount); float step = static_cast<float>(n - 1) / (targetCount - 1); for (int i = 0; i < targetCount; ++i) { float t = i * step; int index = static_cast<int>(t); float frac = t - index; int p0 = (index > 0) ? (index - 1) : 0; int p1 = index; int p2 = (index + 1 < n) ? (index + 1) : (n - 1); int p3 = (index + 2 < n) ? (index + 2) : (n - 1); float value = CatmullRomInterpolate( originalPoints[p0].y(), originalPoints[p1].y(), originalPoints[p2].y(), originalPoints[p3].y(), frac); targetPoints.append(QPointF(i, value)); } }
5 QPaint可视化
照着别人的效果,用 QPaint 画一下。
constexpr int Rows = 12; constexpr int Cols = 20; constexpr int GridGap = 4; constexpr int Falling = 50; constexpr bool EnableAntialiasing = true; constexpr int MaxFrequency = 512; // 频率数据最大值 // 色相范围 constexpr int MaxHue = 120; constexpr int MaxSaturation = 180; constexpr int MaxValue = 180; void FrequencySpectrumWidget::resizeEvent(QResizeEvent * event) { QWidget::resizeEvent(event); if (m_BackgroundPixmap.size() != QSize(width(), height())) { GetGridSize(m_GridCache.gridSize, m_GridCache.gap, m_GridCache.startX, m_GridCache.startY); PreDrawBackground(); } } void FrequencySpectrumWidget::timerEvent(QTimerEvent * event) { UpdateFallingColors(); update(); } void FrequencySpectrumWidget::paintEvent(QPaintEvent * event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, EnableAntialiasing); // 绘制缓存的背景图像 painter.drawPixmap(0, 0, m_BackgroundPixmap); const int gridSize = m_GridCache.gridSize; const int gap = m_GridCache.gap; const int startX = m_GridCache.startX; const int startY = m_GridCache.startY; // 绘制频域数据相关的格子 for (int col = 0; col < Cols; ++col) { int volumeLevel = m_FrequencyData[col]; for (int i = 0; i < volumeLevel; ++i) { int row = Rows - 1 - i; int hue = MaxHue - (i * MaxHue / Rows); int saturation = MaxSaturation; int value = MaxValue; QColor color = QColor::fromHsv(hue, saturation, value); QRect rect(startX + col * (gridSize + gap), startY + row * (gridSize + gap), gridSize, gridSize); painter.setBrush(QBrush(color)); painter.drawRect(rect); } int heightRow = Rows - 1 - m_FallingColorHeight[col]; QColor color(200, 200, 200); QRect rect( startX + col * (gridSize + gap), startY + heightRow * (gridSize + gap) + gridSize / 2, gridSize, gridSize / 2); painter.setBrush(QBrush(color)); painter.drawRect(rect); } } void FrequencySpectrumWidget::PreDrawBackground() { m_BackgroundPixmap = QPixmap(width(), height()); m_BackgroundPixmap.fill(Qt::black); // 填充背景为黑色 QPainter painter(&m_BackgroundPixmap); painter.setRenderHint(QPainter::Antialiasing, EnableAntialiasing); const int gridSize = m_GridCache.gridSize; const int gap = m_GridCache.gap; const int startX = m_GridCache.startX; const int startY = m_GridCache.startY; QVector<QRect> gridRects; for (int col = 0; col < Cols; ++col) { for (int row = 0; row < Rows; ++row) { gridRects.append(QRect( startX + col * (gridSize + gap), startY + row * (gridSize + gap), gridSize, gridSize)); } } QColor color(22, 23, 21); painter.setBrush(QBrush(color)); painter.drawRects(gridRects.data(), gridRects.size()); } void FrequencySpectrumWidget::UpdateFallingColors() { for (int col = 0; col < Cols; ++col) { // m_FallingColorHeight[col] = qMax(m_FrequencyData[col], m_FallingColorHeight[col] - 1); m_FallingColorHeight[col] = qMax( m_FrequencyData[col], m_FallingColorHeight[col] - qCeil((m_FallingColorHeight[col] - m_FrequencyData[col]) * 0.1)); } } void FrequencySpectrumWidget::GetGridSize(int & gridSize, int & gap, int & startX, int & startY) { gap = GridGap; // 网格间隙(1:1比例) int availableWidth = width() - (Cols + 1) * gap; // 可用宽度(减去左右间隙) int availableHeight = height() - (Rows + 1) * gap; // 可用高度(减去上下间隙) gridSize = std::min(availableWidth / Cols, availableHeight / Rows); // 格子边长 if (gridSize < gap * 2) { // 尺寸太小,保证至少有两个格子宽度的间隙 gridSize = gap * 2; } // 计算总的网格宽度和高度 int totalWidth = (Cols * gridSize) + (Cols + 1) * gap; int totalHeight = (Rows * gridSize) + (Rows + 1) * gap; // 计算绘制起始位置,使网格居中 startX = (width() - totalWidth) / 2; startY = (height() - totalHeight) / 2; }
以上就是QT6实现音频可视化的示例代码的详细内容,更多关于QT6音频可视化的资料请关注脚本之家其它相关文章!