QT6音频可视化¶
需要做一个简易的音频可视化,网上找的案例都是基于QT5的。很多API和类都弃用了,分享一个基于QT6的音频可视化Demo。
开源免费,获取项目: 关注微信公众号 【三十儿艺】,回复数字 “001”即可获得。
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;
}