You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:Qt实现非实时Audacity风格音频波形渲染

实现非实时音频波形渲染(类似Audacity)

我来帮你搞定这个非实时音频波形渲染的问题——你想要像Audacity那样一次性渲染完整的音频波形,不用播放文件,而不是现在的实时渲染逻辑。先看看你的核心问题,再一步步修正代码:

问题分析

你当前的代码有几个关键问题:

  1. 直接把char类型的原始音频数据当作波形高度,完全没考虑音频的采样格式(位深、通道数),QAudioBuffer的原始数据需要转换才能用;
  2. 波形渲染逻辑不对:你现在是取相邻点的距离画线段,而Audacity的逻辑是每个像素对应一段采样区间,取这段的最大值和最小值,画竖线连接这两个点;
  3. 没有对采样值做归一化,导致波形显示异常;
  4. 没有处理多通道音频(比如立体声)的情况。

先贴出你现有的代码方便对比:

现有track.h代码

#ifndef TRACK_H
#define TRACK_H
#include <QWidget>
#include <QAudioBuffer>
class QAudioDecoder;

class Track : public QWidget {
    Q_OBJECT
public:
    Track(QWidget *parent = Q_NULLPTR);
    ~Track();
    void setSource(const QString &fileName);

public slots:
    void setBuffer();

protected:
    void paintEvent(QPaintEvent *e) override;

private:
    int pointDistance(const QPoint& a, const QPoint& b);
    QAudioDecoder *decoder;
    QAudioBuffer buffer;
    QByteArray byteArr;
};

#endif // TRACK_H

现有track.cpp代码

#include "track.h"
#include <QPaintEvent>
#include <QPainter>
#include <QAudioDecoder>
#include <cmath>

Track::Track(QWidget *parent) : QWidget(parent), decoder(new QAudioDecoder(this)) {
    setMinimumHeight(50);
    connect(decoder, SIGNAL(bufferReady()), this, SLOT(setBuffer()));
    connect(decoder, SIGNAL(finished()), this, SLOT(update()));
}

Track::~Track() {
    delete decoder;
}

void Track::setSource(const QString &fileName) {
    byteArr.clear();
    decoder->setSourceFilename(fileName);
    decoder->start();
}

void Track::setBuffer() {
    buffer = decoder->read();
    byteArr.append(buffer.constData<char>(), buffer.byteCount());
}

void Track::paintEvent(QPaintEvent *e) {
    QWidget::paintEvent(e);
    int w = width(), h = height();
    QBrush backgroundBrush(Qt::white);
    QPainter painter(this);
    painter.fillRect(0, 0, w, h, backgroundBrush);
    painter.drawLine(0, h/2, w, h/2);

    if (!byteArr.isEmpty()){
        QPen pen(QColor(Qt::blue));
        painter.setPen(pen);
        int length = byteArr.size();
        int samplesPerPixel = length/w;
        int idx=0;
        for (int i=0; i<w; i++){
            QLine line;
            int higher = 0;
            for (int j=0; j<samplesPerPixel && idx+1<length; j++){
                const QPoint a(i, byteArr.at(idx)+(h/2));
                const QPoint b(i, byteArr.at(idx+1)+(h/2));
                if (higher < pointDistance(a, b)) line = QLine(a, b);
                idx++;
            }
            painter.drawLine(line);
        }
    }
}

int Track::pointDistance(const QPoint &a, const QPoint &b) {
    int ret = 0;
    ret = sqrt(pow(b.x()-a.x(), 2) + pow(b.y()-a.y(), 2));
    return ret;
}

修正方案

1. 先修改Track类的成员变量

首先,我们需要存储归一化后的所有音频样本,而不是原始的字节数据。更新track.h:

#ifndef TRACK_H
#define TRACK_H
#include <QWidget>
#include <QAudioBuffer>
#include <QVector>
class QAudioDecoder;

class Track : public QWidget {
    Q_OBJECT
public:
    Track(QWidget *parent = Q_NULLPTR);
    ~Track();
    void setSource(const QString &fileName);

public slots:
    void setBuffer();
    void onDecoderFinished(); // 新增:解码完成后统一处理

protected:
    void paintEvent(QPaintEvent *e) override;

private:
    QVector<qreal> getAllSamplesFromBuffer(const QAudioBuffer &buffer); // 新增:提取并归一化样本
    QAudioDecoder *decoder;
    QVector<qreal> allSamples; // 替换byteArr:存储所有归一化后的样本
};

#endif // TRACK_H

2. 实现样本提取与归一化

在track.cpp中添加getAllSamplesFromBuffer方法,这个方法会根据音频格式把原始数据转换成[-1.0, 1.0]范围的归一化样本,同时处理多通道:

#include "track.h"
#include <QPaintEvent>
#include <QPainter>
#include <QAudioDecoder>
#include <cmath>

// ... 其他构造函数、析构函数等

QVector<qreal> Track::getAllSamplesFromBuffer(const QAudioBuffer &buffer) {
    QVector<qreal> samples;
    const QAudioFormat format = buffer.format();
    const int channelCount = format.channelCount();
    const qint64 sampleCount = buffer.sampleCount();

    if (format.sampleType() == QAudioFormat::SignedInt) {
        switch (format.sampleSize()) {
        case 8: {
            const qint8 *data = buffer.constData<qint8>();
            for (qint64 i = 0; i < sampleCount * channelCount; i += channelCount) {
                // 多通道取平均值(立体声合并为单声道波形)
                qreal val = 0;
                for (int c = 0; c < channelCount; ++c) {
                    val += data[i + c] / qreal(128); // 8位有符号范围是-128到127,归一化到-1~1
                }
                val /= channelCount;
                samples.append(val);
            }
            break;
        }
        case 16: {
            const qint16 *data = buffer.constData<qint16>();
            for (qint64 i = 0; i < sampleCount * channelCount; i += channelCount) {
                qreal val = 0;
                for (int c = 0; c < channelCount; ++c) {
                    val += data[i + c] / qreal(32768); // 16位有符号范围-32768到32767
                }
                val /= channelCount;
                samples.append(val);
            }
            break;
        }
        // 可以扩展支持24/32位整数或浮点格式
        }
    } else if (format.sampleType() == QAudioFormat::Float) {
        const float *data = buffer.constData<float>();
        for (qint64 i = 0; i < sampleCount * channelCount; i += channelCount) {
            qreal val = 0;
            for (int c = 0; c < channelCount; ++c) {
                val += data[i + c]; // 浮点格式本身就是-1~1范围
            }
            val /= channelCount;
            samples.append(val);
        }
    }
    return samples;
}

3. 更新解码与渲染逻辑

修改setBuffer、onDecoderFinished和paintEvent,确保等所有音频数据解码完成后,一次性渲染完整波形:

void Track::setSource(const QString &fileName) {
    allSamples.clear(); // 清空之前的样本
    decoder->setSourceFilename(fileName);
    decoder->start();
}

void Track::setBuffer() {
    QAudioBuffer buffer = decoder->read();
    // 提取当前解码块的样本,追加到总样本列表
    allSamples.append(getAllSamplesFromBuffer(buffer));
}

void Track::onDecoderFinished() {
    // 所有数据解码完成,触发重绘
    update();
}

void Track::paintEvent(QPaintEvent *e) {
    QWidget::paintEvent(e);
    int w = width(), h = height();
    int halfHeight = h / 2;

    QPainter painter(this);
    painter.fillRect(rect(), Qt::white);
    painter.setPen(Qt::blue);

    // 画中心基线
    painter.drawLine(0, halfHeight, w, halfHeight);

    if (allSamples.isEmpty()) return;

    int totalSamples = allSamples.size();
    int samplesPerPixel = totalSamples / w;
    // 如果样本数少于控件宽度,每个像素对应一个样本
    if (samplesPerPixel == 0) samplesPerPixel = 1;

    int currentSample = 0;
    for (int x = 0; x < w && currentSample < totalSamples; ++x) {
        qreal minVal = 1.0;
        qreal maxVal = -1.0;

        // 遍历当前像素对应的采样区间,找出最大和最小值
        int endSample = qMin(currentSample + samplesPerPixel, totalSamples);
        for (; currentSample < endSample; ++currentSample) {
            qreal val = allSamples[currentSample];
            if (val < minVal) minVal = val;
            if (val > maxVal) maxVal = val;
        }

        // 把归一化值转换为控件坐标(y轴向下,所以需要翻转)
        int yMin = halfHeight - qRound(minVal * halfHeight);
        int yMax = halfHeight - qRound(maxVal * halfHeight);

        // 画竖线,对应这段采样的波形范围
        painter.drawLine(x, yMin, x, yMax);
    }
}

// 更新构造函数的信号连接,用新式信号槽语法更安全
Track::Track(QWidget *parent) : QWidget(parent), decoder(new QAudioDecoder(this)) {
    setMinimumHeight(50);
    connect(decoder, &QAudioDecoder::bufferReady, this, &Track::setBuffer);
    connect(decoder, &QAudioDecoder::finished, this, &Track::onDecoderFinished);
}

核心逻辑说明

  • 样本归一化:把不同位深的音频数据统一转换到[-1.0, 1.0],这样不管是8位、16位还是浮点音频,都能正确映射到控件高度;
  • 区间极值渲染:每个像素对应一段采样,取这段的最大和最小值画竖线,这就是Audacity那样的波形效果;
  • 非实时渲染:等QAudioDecoder把所有音频数据解码完成后再触发重绘,确保渲染的是完整波形,而不是边解码边画;
  • 多通道处理:这里把立体声的左右通道样本取平均值合并为单声道波形,你也可以改成上下分别渲染左右通道的波形。

内容的提问来源于stack exchange,提问作者thibsc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:03:36