QT6實(shí)現(xiàn)音頻可視化的示例代碼
QT6音頻可視化
需要做一個簡易的音頻可視化,網(wǎng)上找的案例都是基于QT5的。很多API和類都棄用了,分享一個基于QT6的音頻可視化Demo。
1 獲取QAudioBuffer
QAudioBuffer 存儲一系列音頻幀,每個幀包含一個時間點(diǎn)上所有通道的采樣值。采樣格式通過 QAudioFormat 指定,包括采樣率、通道數(shù)、樣本大小等。在多通道(例如立體聲)音頻中,采樣數(shù)據(jù)通常是交錯存儲的,即每個幀包含各個通道的采樣值依次排列。
QT5 通過 QAudioProbe 來獲取幀數(shù)據(jù),QT6 提供了 QAudioBufferOutput 來獲取幀數(shù)據(jù)。
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 時域轉(zhuǎn)頻域
Qt 自身未集成 FFT 計(jì)算,其自帶的 Demo 里使用的 FFTReal。FFTW 、FFTReal、KISSFFT 都試了下,最后用的 KISSFFT。
int N = audioData.size(); kiss_fft_cfg cfg = kiss_fft_alloc(N, 0, NULL, NULL); // 準(zhǔn)備輸入和輸出 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); // 計(jì)算 FFT 結(jié)果的幅值 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); // 計(jì)算動態(tài)范圍的有效最大值 float effectiveMaxMagnitude = qMax(frameMaxMagnitude, GlobalMaxMagnitude * DynamicThreshold); // 歸一化 for (int i = 0; i < N / 2; ++i) { magnitude[i] = (magnitude[i] / effectiveMaxMagnitude) * 100.0f; }
3 頻域結(jié)果分組
簡單區(qū)分下高中低頻率,不同頻率占比不同
// 分組頻段范圍 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 數(shù)量 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) { // 更新頻域數(shù)據(jù) if (m_FreqData.size() == FreqDomainSum) { for (int i = 0; i < m_FreqData.size(); ++i) { m_FreqBarSet->replace(i, m_FreqData.at(i)); } } // 更新時域數(shù)據(jù) 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; // 頻率數(shù)據(jù)最大值 // 色相范圍 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; // 繪制頻域數(shù)據(jù)相關(guān)的格子 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; // 網(wǎng)格間隙(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; } // 計(jì)算總的網(wǎng)格寬度和高度 int totalWidth = (Cols * gridSize) + (Cols + 1) * gap; int totalHeight = (Rows * gridSize) + (Rows + 1) * gap; // 計(jì)算繪制起始位置,使網(wǎng)格居中 startX = (width() - totalWidth) / 2; startY = (height() - totalHeight) / 2; }
以上就是QT6實(shí)現(xiàn)音頻可視化的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于QT6音頻可視化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言中實(shí)現(xiàn)itoa函數(shù)的實(shí)例
這篇文章主要介紹了C語言中實(shí)現(xiàn)itoa函數(shù)的實(shí)例的相關(guān)資料,希望通過本文能幫助到大家,讓大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-10-10C++ accumulate函數(shù)詳細(xì)介紹和具體案例
這篇文章主要介紹了C++ accumulate函數(shù)詳細(xì)介紹和具體案例,accumulate是numeric庫中的一個函數(shù),主要用來對指定范圍內(nèi)元素求和,但也自行指定一些其他操作,如范圍內(nèi)所有元素相乘、相除等2022-08-08