求助:Qt实现非实时Audacity风格音频波形渲染
实现非实时音频波形渲染(类似Audacity)
我来帮你搞定这个非实时音频波形渲染的问题——你想要像Audacity那样一次性渲染完整的音频波形,不用播放文件,而不是现在的实时渲染逻辑。先看看你的核心问题,再一步步修正代码:
问题分析
你当前的代码有几个关键问题:
- 直接把
char类型的原始音频数据当作波形高度,完全没考虑音频的采样格式(位深、通道数),QAudioBuffer的原始数据需要转换才能用; - 波形渲染逻辑不对:你现在是取相邻点的距离画线段,而Audacity的逻辑是每个像素对应一段采样区间,取这段的最大值和最小值,画竖线连接这两个点;
- 没有对采样值做归一化,导致波形显示异常;
- 没有处理多通道音频(比如立体声)的情况。
先贴出你现有的代码方便对比:
现有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
相关产品推荐
相关产品推荐

