C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > QT6音频可视化

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音频可视化的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文