From c46caf753345619e419aa02be0fd394922a9560c Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Thu, 11 Jun 2026 18:45:03 +0200 Subject: [PATCH] try to make code performant, lets see if this will work. --- CMakeLists.txt | 11 ++ build.sh | 14 ++ src/engine/AudioEngine.h | 2 +- src/engine/EqProcessor.cpp | 95 +++++++----- src/engine/EqProcessor.h | 32 ++--- src/engine/SpectrumAnalyzer.cpp | 47 +++--- src/engine/SpectrumAnalyzer.h | 20 +-- src/gui/SpectrogramWidget.cpp | 246 ++++++++++++++++---------------- src/gui/SpectrogramWidget.h | 69 +++++---- 9 files changed, 306 insertions(+), 230 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9e9fa2..9fd784a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,17 @@ set(RESOURCES # target add_executable(SubWave ${SOURCES} ${HEADERS} ${RESOURCES}) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.") + set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Choose the type of build." FORCE) +endif() + +if(MSVC) + target_compile_options(SubWave PRIVATE /O2 /fp:fast) +else() + target_compile_options(SubWave PRIVATE -O3 -ffast-math -march=native) +endif() + # headers target_include_directories(SubWave PRIVATE src diff --git a/build.sh b/build.sh index 6310843..f371ed0 100755 --- a/build.sh +++ b/build.sh @@ -21,6 +21,17 @@ clean_build() { make -j"$(nproc)" } +release_build() { + if [ -d "build" ]; then + rm -rf "build" + fi + mkdir "build" + + cd build + cmake -DCMAKE_BUILD_TYPE=Release .. + make -j"$(nproc)" +} + run_executable() { ./build/SubWave } @@ -38,6 +49,9 @@ case "${1:-build}" in run|exec|r) run_executable ;; + release|rb) + release_build + ;; *) echo "Usage: $0 {build|clean|cleanbuild|cb}" exit 1 diff --git a/src/engine/AudioEngine.h b/src/engine/AudioEngine.h index bc9eb97..ca650d1 100644 --- a/src/engine/AudioEngine.h +++ b/src/engine/AudioEngine.h @@ -75,7 +75,7 @@ private: float m_bqCoeff[EQ_BANDS][5] {}; float m_bqState[EQ_BANDS][2][2] {}; - static constexpr int FFT_SIZE = 4096; + static constexpr int FFT_SIZE = 2048; float m_fftBuf[FFT_SIZE] {}; int m_fftPos { 0 }; }; diff --git a/src/engine/EqProcessor.cpp b/src/engine/EqProcessor.cpp index 54d7fab..b4ddaf6 100644 --- a/src/engine/EqProcessor.cpp +++ b/src/engine/EqProcessor.cpp @@ -1,21 +1,23 @@ #include "EqProcessor.h" #include #include -#include +#include namespace engine { EqProcessor::EqProcessor() { + std::fill(std::begin(m_gainDb), std::end(m_gainDb), 0.f); for (int b = 0; b < BANDS; ++b) { - m_gainDb[b] = 0.f; recomputeBiquad(b); } } void EqProcessor::setSampleRate(int sr) { + if (sr <= 0) return; m_sampleRate = sr; - for (int b = 0; b < BANDS; ++b) + for (int b = 0; b < BANDS; ++b) { recomputeBiquad(b); + } } void EqProcessor::setBand(int band, float dB) { @@ -29,34 +31,56 @@ float EqProcessor::band(int band) const { } void EqProcessor::reset() { - std::memset(m_state, 0, sizeof(m_state)); + for (int b = 0; b < BANDS; ++b) { + for (int c = 0; c < MAX_CH; ++c) { + m_state[b][c] = BiquadState{}; + } + } } - void EqProcessor::process(AudioBuffer &a_buf) { - const int channels = a_buf.channels; - const int count = a_buf.sampleCount(); - float *samples = a_buf.samples.data(); + const int channels = a_buf.channels; + const int count = a_buf.sampleCount(); + float *samples = a_buf.samples.data(); + + if (channels <= 0 || count <= 0 || !samples) return; + + const int activeChannels = std::min(channels, MAX_CH); + + struct ActiveBand { + BiquadCoefficients coeff; + int index; + }; + + ActiveBand activeBands[BANDS]; + int activeCount = 0; for (int band = 0; band < BANDS; ++band) { - if (std::abs(m_gainDb[band]) < 0.01f) {continue;} - - const auto& c = m_coeff[band]; - - for (int idx = 0; idx < count; ++idx) { - const int ch = idx % channels; - - auto &s = m_state[band][ch]; - - const float x = samples[idx]; - const float y = c.b0 * x + s.z1; - - s.z1 = c.b1 * x - c.a1 * y + s.z2; - s.z2 = c.b2 * x - c.a2 * y; - - samples[idx] = y; + if (std::abs(m_gainDb[band]) >= 0.01f) { + activeBands[activeCount++] = { m_coeff[band], band }; + } + } + + if (activeCount == 0) return; + + for (int frame = 0; frame < count; frame += channels) { + for (int ch = 0; ch < activeChannels; ++ch) { + const int idx = frame + ch; + float sample = samples[idx]; + + for (int b = 0; b < activeCount; ++b) { + const auto& c = activeBands[b].coeff; + auto &s = m_state[activeBands[b].index][ch]; + + const float y = c.b0 * sample + s.z1; + s.z1 = c.b1 * sample - c.a1 * y + s.z2; + s.z2 = c.b2 * sample - c.a2 * y; + + sample = y; + } + + samples[idx] = sample; } - } } @@ -66,27 +90,28 @@ void EqProcessor::recomputeBiquad(int band) { const double gainDb = m_gainDb[band]; constexpr double Q = std::numbers::sqrt2; - // RBJ peaking EQ gain factor - const double A = std::pow(10.0, gainDb / 40.0); - const double omega = 2.0 * std::numbers::pi * frequency / sampleRate; - const double sinW = std::sin(omega); - const double cosW = std::cos(omega); + if (frequency >= sampleRate * 0.5) { + auto &c = m_coeff[band]; + c.b0 = 1.0f; c.b1 = 0.0f; c.b2 = 0.0f; c.a1 = 0.0f; c.a2 = 0.0f; + return; + } + const double A = std::pow(10.0, gainDb / 40.0); + const double omega = 2.0 * std::numbers::pi * frequency / sampleRate; + const double sinW = std::sin(omega); + const double cosW = std::cos(omega); const double alpha = sinW / (2.0 * Q); - // rbj audio eq cookbook const double b0 = 1.0 + alpha * A; const double b1 = -2.0 * cosW; const double b2 = 1.0 - alpha * A; - const double a0 = 1.0 + alpha / A; const double a1 = -2.0 * cosW; const double a2 = 1.0 - alpha / A; - + const double invA0 = 1.0 / a0; - auto &c = m_coeff[band]; - + c.b0 = static_cast(b0 * invA0); c.b1 = static_cast(b1 * invA0); c.b2 = static_cast(b2 * invA0); diff --git a/src/engine/EqProcessor.h b/src/engine/EqProcessor.h index dcea34b..9c18976 100644 --- a/src/engine/EqProcessor.h +++ b/src/engine/EqProcessor.h @@ -1,11 +1,13 @@ #pragma once + #include "AudioBuffer.h" +#include namespace engine { class EqProcessor { public: - static constexpr int BANDS = 10; + static constexpr int BANDS = 10; static constexpr float FREQS[BANDS] = { 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 }; @@ -14,12 +16,9 @@ public: void setBand(int band, float dB); float band(int band) const; - - void process(AudioBuffer &buf); - - void reset(); - - void setSampleRate(int sr); + void process(AudioBuffer &buf); + void reset(); + void setSampleRate(int sr); private: void recomputeBiquad(int band); @@ -28,22 +27,21 @@ private: float m_gainDb[BANDS] {}; struct BiquadCoefficients { - float b0 {}; - float b1 {}; - float b2 {}; - float a1 {}; - float a2 {}; + float b0 { 1.0f }; + float b1 { 0.0f }; + float b2 { 0.0f }; + float a1 { 0.0f }; + float a2 { 0.0f }; }; - BiquadCoefficients m_coeff[BANDS]; struct BiquadState { - float z1; - float z2; + float z1 { 0.0f }; + float z2 { 0.0f }; }; - + static constexpr int MAX_CH = 8; - BiquadState m_state[BANDS][MAX_CH] {}; + BiquadState m_state[BANDS][MAX_CH] {}; }; } // namespace engine \ No newline at end of file diff --git a/src/engine/SpectrumAnalyzer.cpp b/src/engine/SpectrumAnalyzer.cpp index 1b0fc13..89feed0 100644 --- a/src/engine/SpectrumAnalyzer.cpp +++ b/src/engine/SpectrumAnalyzer.cpp @@ -18,27 +18,35 @@ SpectrumAnalyzer::SpectrumAnalyzer(QObject *parent) if (!m_plan) throw std::runtime_error("FFTW plan creation failed"); + // Precompute Hann window once — avoid recalculating every FFT frame + const double invN1 = 1.0 / static_cast(FFT_SIZE - 1); + for (int i = 0; i < FFT_SIZE; ++i) + m_hannWindow[i] = 0.5 * (1.0 - std::cos(2.0 * std::numbers::pi * i * invN1)); + reset(); } SpectrumAnalyzer::~SpectrumAnalyzer() { - if (m_plan) fftw_destroy_plan(m_plan); - if (m_fftIn) fftw_free(m_fftIn); + if (m_plan) fftw_destroy_plan(m_plan); + if (m_fftIn) fftw_free(m_fftIn); if (m_fftOut) fftw_free(m_fftOut); } void SpectrumAnalyzer::reset() { + // Fill the whole buffer with silence. + // m_pos starts at FFT_SIZE/2 so the first FFT fires after one + // half-window of real samples, maintaining 50% overlap from the start. std::fill(std::begin(m_buf), std::end(m_buf), 0.f); m_pos = FFT_SIZE / 2; } void SpectrumAnalyzer::feed(const AudioBuffer &buf) { - const int channels = buf.channels; - const int count = buf.sampleCount(); - const float *src = buf.samples.data(); + const int channels = buf.channels; + const int count = buf.sampleCount(); + const float *src = buf.samples.data(); for (int i = 0; i < count; i += channels) { float mono = 0.f; @@ -50,33 +58,28 @@ void SpectrumAnalyzer::feed(const AudioBuffer &buf) if (m_pos == FFT_SIZE) { emit fftReady(computeFFT(m_buf)); - // 50 % overlap + // 50% overlap: shift the second half down to the first std::copy(m_buf + FFT_SIZE / 2, m_buf + FFT_SIZE, m_buf); m_pos = FFT_SIZE / 2; } } } -QVector SpectrumAnalyzer::computeFFT(const float *input) +QVector SpectrumAnalyzer::computeFFT(const float (&input)[FFT_SIZE]) { - // Copy input and apply Hann window - const double invN1 = 1.0 / static_cast(FFT_SIZE - 1); - for (int i = 0; i < FFT_SIZE; ++i) { - const double w = 0.5 * (1.0 - std::cos(2.0 * std::numbers::pi * i * invN1)); - m_fftIn[i] = static_cast(input[i]) * w; - } + // Apply precomputed Hann window + for (int i = 0; i < FFT_SIZE; ++i) + m_fftIn[i] = static_cast(input[i]) * m_hannWindow[i]; - // Execute FFTW plan fftw_execute(m_plan); - // Compute magnitude spectrum (bins 0 .. FFT_SIZE/2 - 1) - const int bins = FFT_SIZE / 2; - QVector mag(bins); - const double invN = 1.0 / static_cast(FFT_SIZE); - for (int i = 0; i < bins; ++i) { - const double re = m_fftOut[i][0]; - const double im = m_fftOut[i][1]; - mag[i] = static_cast(std::sqrt(re * re + im * im) * invN); + // Magnitude spectrum + const float invN = 1.f / static_cast(FFT_SIZE); + QVector mag(BIN_COUNT); + for (int i = 0; i < BIN_COUNT; ++i) { + const float re = static_cast(m_fftOut[i][0]); + const float im = static_cast(m_fftOut[i][1]); + mag[i] = std::sqrt(re * re + im * im) * invN; } return mag; } diff --git a/src/engine/SpectrumAnalyzer.h b/src/engine/SpectrumAnalyzer.h index 2adadca..27b19ba 100644 --- a/src/engine/SpectrumAnalyzer.h +++ b/src/engine/SpectrumAnalyzer.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include "AudioBuffer.h" #include @@ -9,7 +10,8 @@ namespace engine { class SpectrumAnalyzer : public QObject { Q_OBJECT public: - static constexpr int FFT_SIZE = 4096; + static constexpr int FFT_SIZE = 2048; + static constexpr int BIN_COUNT = FFT_SIZE / 2; explicit SpectrumAnalyzer(QObject *parent = nullptr); ~SpectrumAnalyzer() override; @@ -21,15 +23,17 @@ signals: void fftReady(QVector magnitudes); private: - QVector computeFFT(const float *input); + QVector computeFFT(const float (&input)[FFT_SIZE]); - float m_buf[FFT_SIZE] {}; - int m_pos { 0 }; + float m_buf[FFT_SIZE]{}; + int m_pos{FFT_SIZE / 2}; - // FFTW members - fftw_plan m_plan = nullptr; - double *m_fftIn = nullptr; - fftw_complex *m_fftOut = nullptr; + // Precomputed Hann window coefficients + double m_hannWindow[FFT_SIZE]{}; + + fftw_plan m_plan = nullptr; + double *m_fftIn = nullptr; + fftw_complex *m_fftOut = nullptr; }; } // namespace engine \ No newline at end of file diff --git a/src/gui/SpectrogramWidget.cpp b/src/gui/SpectrogramWidget.cpp index 2d83556..75b4cf4 100644 --- a/src/gui/SpectrogramWidget.cpp +++ b/src/gui/SpectrogramWidget.cpp @@ -4,14 +4,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include SpectrogramWidget::SpectrogramWidget(QWidget *parent) : QOpenGLWidget(parent) @@ -28,15 +26,14 @@ SpectrogramWidget::SpectrogramWidget(QWidget *parent) setMinimumSize(400, 200); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - m_barHeights.resize(BAR_COUNT); - m_barPeaksVec.resize(BAR_COUNT); - loadBarColors(); auto *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, QOverload<>::of(&SpectrogramWidget::update)); - timer->start(16); + timer->start(16); + + m_frameTimer.start(); } SpectrogramWidget::~SpectrogramWidget() @@ -49,23 +46,23 @@ SpectrogramWidget::~SpectrogramWidget() void SpectrogramWidget::receiveFft(const QVector &mag) { - QMutexLocker lk(&m_queueMtx); - while (m_fftQueue.size() >= 4) - m_fftQueue.dequeue(); - m_fftQueue.enqueue(mag); - update(); + QMutexLocker lk(&m_latestMtx); + m_latestMag = mag; + m_newMagAvailable = true; } void SpectrogramWidget::reset() { { - QMutexLocker lk(&m_queueMtx); - m_fftQueue.clear(); + QMutexLocker lk(&m_latestMtx); + m_latestMag.clear(); + m_newMagAvailable = false; } - m_currentMag .fill(0.f); + m_currentMag.fill(0.f); m_barSmoothed.fill(0.f); - m_barPeak .fill(0.f); - m_peakTimer .fill(0); + m_barPeak.fill(0.f); + m_peakHoldTimer.fill(0.f); + m_dirty = false; update(); } @@ -73,10 +70,22 @@ void SpectrogramWidget::initializeGL() { initializeOpenGLFunctions(); initShaders(); - buildHeatmapTexture(); + updateHeatmapTexture(); glGenVertexArrays(1, &m_vaoBars); glDisable(GL_BLEND); + + const int bins = engine::SpectrumAnalyzer::BIN_COUNT; + const double logMin = std::log10(20.0); + const double logMax = std::log10(22050.0); + const double range = logMax - logMin; + + for (int b = 0; b < BAR_COUNT; ++b) { + double f1 = std::pow(10.0, logMin + range * b / BAR_COUNT); + double f2 = std::pow(10.0, logMin + range * (b + 1) / BAR_COUNT); + m_binMap[b].lo = std::clamp(int(f1 / 22050.0 * bins), 0, bins - 1); + m_binMap[b].hi = std::clamp(int(f2 / 22050.0 * bins), m_binMap[b].lo, bins - 1); + } } void SpectrogramWidget::initShaders() @@ -90,9 +99,11 @@ void SpectrogramWidget::initShaders() return f.readAll(); }; - if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Vertex, load(":/shaders/bars.vert"))) + if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Vertex, + load(":/shaders/bars.vert"))) qWarning() << "Bars vert error:" << m_barsProg.log(); - if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Fragment, load(":/shaders/bars.frag"))) + if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Fragment, + load(":/shaders/bars.frag"))) qWarning() << "Bars frag error:" << m_barsProg.log(); if (!m_barsProg.link()) qWarning() << "Bars link error:" << m_barsProg.log(); @@ -107,50 +118,43 @@ void SpectrogramWidget::initShaders() m_uHeatmap = m_barsProg.uniformLocation("heatmap"); } -void SpectrogramWidget::buildHeatmapTexture() -{ - updateHeatmapTexture(); // use current m_stops -} - void SpectrogramWidget::updateHeatmapTexture() { + if (m_stops.size() < 2) + m_stops = { QColor(0, 0, 0), QColor(255, 255, 255) }; + + const int nSegs = m_stops.size() - 1; + auto lerp = [](int x, int y, float f) { + return std::clamp(int(x + (y - x) * f), 0, 255); + }; + for (int i = 0; i < 256; ++i) { + float t = float(i) / 255.f * nSegs; + int seg = std::min(int(t), nSegs - 1); + float f = t - seg; + QColor a = m_stops[seg], b = m_stops[seg + 1]; + m_heatmap[i] = qRgb(lerp(a.red(), b.red(), f), + lerp(a.green(), b.green(), f), + lerp(a.blue(), b.blue(), f)); + } + + std::array td; + for (int i = 0; i < 256; ++i) { + td[3 * i + 0] = static_cast(qRed (m_heatmap[i])); + td[3 * i + 1] = static_cast(qGreen(m_heatmap[i])); + td[3 * i + 2] = static_cast(qBlue (m_heatmap[i])); + } + if (m_heatmapTex) { glDeleteTextures(1, &m_heatmapTex); m_heatmapTex = 0; } - - if (m_stops.size() < 2) - m_stops = { QColor(0,0,0), QColor(255,255,255) }; - - const int nSegs = m_stops.size() - 1; - - auto lerp = [](int x, int y, float f) { - return std::clamp(int(x + (y - x) * f), 0, 255); - }; - - for (int i = 0; i < 256; i++) { - float t = float(i) / 255.f * nSegs; - int seg = std::min(int(t), nSegs - 1); - float f = t - seg; - QColor a = m_stops[seg], b = m_stops[seg+1]; - m_heatmap[i] = qRgb(lerp(a.red(), b.red(), f), - lerp(a.green(), b.green(), f), - lerp(a.blue(), b.blue(), f)); - } - glGenTextures(1, &m_heatmapTex); glBindTexture(GL_TEXTURE_1D, m_heatmapTex); - std::vector td(256 * 3); - for (int i = 0; i < 256; i++) { - td[3*i+0] = quint8(qRed (m_heatmap[i])); - td[3*i+1] = quint8(qGreen(m_heatmap[i])); - td[3*i+2] = quint8(qBlue (m_heatmap[i])); - } glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, td.data()); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); } void SpectrogramWidget::setStops(const QVector &stops) @@ -166,26 +170,18 @@ void SpectrogramWidget::setStops(const QVector &stops) void SpectrogramWidget::loadBarColors() { - QString configDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.subwave"; - QDir().mkpath(configDir); - QString path = configDir + "/barcolors.json"; + QString path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + + "/.subwave/barcolors.json"; QFile f(path); - if (!f.open(QIODevice::ReadOnly)) - return; + if (!f.open(QIODevice::ReadOnly)) return; QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); - f.close(); - if (!doc.isArray()) - return; + if (!doc.isArray()) return; - QJsonArray arr = doc.array(); QVector loaded; - for (const auto &v : arr) { + for (const auto &v : doc.array()) { QJsonObject obj = v.toObject(); - int r = obj["r"].toInt(0); - int g = obj["g"].toInt(0); - int b = obj["b"].toInt(0); - loaded.append(QColor(r, g, b)); + loaded.append(QColor(obj["r"].toInt(), obj["g"].toInt(), obj["b"].toInt())); } if (loaded.size() >= 2) m_stops = loaded; @@ -193,12 +189,11 @@ void SpectrogramWidget::loadBarColors() void SpectrogramWidget::saveBarColors() { - QString configDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.subwave"; - QDir().mkpath(configDir); - QString path = configDir + "/barcolors.json"; - QFile f(path); - if (!f.open(QIODevice::WriteOnly)) - return; + QString dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + + "/.subwave"; + QDir().mkpath(dir); + QFile f(dir + "/barcolors.json"); + if (!f.open(QIODevice::WriteOnly)) return; QJsonArray arr; for (const QColor &c : m_stops) { @@ -208,89 +203,94 @@ void SpectrogramWidget::saveBarColors() obj["b"] = c.blue(); arr.append(obj); } - QJsonDocument doc(arr); - f.write(doc.toJson()); - f.close(); + f.write(QJsonDocument(arr).toJson()); } + void SpectrogramWidget::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { GradientDialog dlg(m_stops, this); - if (dlg.exec() == QDialog::Accepted) { + if (dlg.exec() == QDialog::Accepted) setStops(dlg.stops()); - } } QOpenGLWidget::mousePressEvent(e); } -void SpectrogramWidget::computeBars() + +void SpectrogramWidget::computeBars(float dt) { const int fftLen = m_currentMag.size(); - const double logMin = std::log10(20.0), logMax = std::log10(22050.0); - QVector bars(BAR_COUNT, 0.f); - for (int b = 0; b < BAR_COUNT; b++) { - double f1 = std::pow(10.0, logMin + (logMax-logMin)* b /BAR_COUNT); - double f2 = std::pow(10.0, logMin + (logMax-logMin)*(b+1)/BAR_COUNT); - int bin1 = std::clamp(int(f1/22050.0*fftLen), 0, fftLen-1); - int bin2 = std::clamp(int(f2/22050.0*fftLen), bin1, fftLen-1); - float sum = 0.f; int cnt = 0; - for (int k = bin1; k <= bin2; k++) { sum += m_currentMag[k]; cnt++; } - bars[b] = cnt > 0 ? sum/cnt : 0.f; - } - for (int b = 0; b < BAR_COUNT; b++) { - float db = bars[b] > 0 ? 20.f*std::log10(bars[b]) : DB_MIN; - bars[b] = std::clamp((db - DB_MIN) / (DB_MAX - DB_MIN), 0.f, 1.f); - } - for (int b = 0; b < BAR_COUNT; b++) { - m_barSmoothed[b] = m_barSmoothed[b]*0.65f + bars[b]*0.35f; + for (int b = 0; b < BAR_COUNT; ++b) { + const int lo = m_binMap[b].lo; + const int hi = std::min(m_binMap[b].hi, fftLen - 1); + + float sum = 0.f; + int cnt = 0; + for (int k = lo; k <= hi; ++k) { sum += m_currentMag[k]; ++cnt; } + float raw = cnt > 0 ? sum / cnt : 0.f; + + float db = raw > 0.f ? 20.f * std::log10(raw) : DB_MIN; + float bar = std::clamp((db - DB_MIN) / (DB_MAX - DB_MIN), 0.f, 1.f); + + const float coeff = bar > m_barSmoothed[b] ? 0.50f : 0.18f; + m_barSmoothed[b] += (bar - m_barSmoothed[b]) * coeff; + if (m_barSmoothed[b] >= m_barPeak[b]) { - m_barPeak[b] = m_barSmoothed[b]; - m_peakTimer[b] = 25; - } else if (m_peakTimer[b] > 0) { - m_peakTimer[b]--; + m_barPeak[b] = m_barSmoothed[b]; + m_peakHoldTimer[b] = PEAK_HOLD_SEC; + } else if (m_peakHoldTimer[b] > 0.f) { + m_peakHoldTimer[b] -= dt; } else { - m_barPeak[b] = std::max(0.f, m_barPeak[b] - 0.006f); + m_barPeak[b] = std::max(0.f, m_barPeak[b] - PEAK_DECAY_SEC * dt); } + m_barHeights [b] = m_barSmoothed[b]; m_barPeaksVec[b] = m_barPeak[b]; } } + void SpectrogramWidget::paintGL() { + const float dt = static_cast(m_frameTimer.restart()) / 1000.f; + { - QMutexLocker lk(&m_queueMtx); - while (!m_fftQueue.isEmpty()) - m_currentMag = m_fftQueue.dequeue(); + QMutexLocker lk(&m_latestMtx); + if (m_newMagAvailable) { + m_currentMag = std::move(m_latestMag); + m_latestMag = {}; + m_newMagAvailable = false; + m_dirty = true; + } } - computeBars(); + computeBars(dt); QColor bg = palette().color(QPalette::Window); glClearColor(bg.redF(), bg.greenF(), bg.blueF(), 1.0f); glClear(GL_COLOR_BUFFER_BIT); - if (m_barsProg.isLinked()) { - const float barWidth = 2.0f / BAR_COUNT; - const float gap = barWidth * 0.2f; - const float drawWidth = barWidth - gap; + if (!m_barsProg.isLinked()) return; - m_barsProg.bind(); - glUniform1fv(m_uBarHeights, BAR_COUNT, m_barHeights .constData()); - glUniform1fv(m_uBarPeaks, BAR_COUNT, m_barPeaksVec.constData()); - glUniform1i (m_uBarCount, BAR_COUNT); - glUniform1f (m_uBarWidth, drawWidth); - glUniform1f (m_uGap, gap); - glUniform1f (m_uBottomY, -1.0f); - glUniform1f (m_uTopClip, 1.0f); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_1D, m_heatmapTex); - glUniform1i(m_uHeatmap, 0); - glBindVertexArray(m_vaoBars); - glDrawArraysInstanced(GL_TRIANGLES, 0, 6, BAR_COUNT); - glBindVertexArray(0); - m_barsProg.release(); - } + const float barWidth = 2.0f / BAR_COUNT; + const float gap = barWidth * 0.2f; + const float drawWidth = barWidth - gap; + + m_barsProg.bind(); + glUniform1fv(m_uBarHeights, BAR_COUNT, m_barHeights .data()); + glUniform1fv(m_uBarPeaks, BAR_COUNT, m_barPeaksVec.data()); + glUniform1i (m_uBarCount, BAR_COUNT); + glUniform1f (m_uBarWidth, drawWidth); + glUniform1f (m_uGap, gap); + glUniform1f (m_uBottomY, -1.0f); + glUniform1f (m_uTopClip, 1.0f); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_1D, m_heatmapTex); + glUniform1i(m_uHeatmap, 0); + glBindVertexArray(m_vaoBars); + glDrawArraysInstanced(GL_TRIANGLES, 0, 6, BAR_COUNT); + glBindVertexArray(0); + m_barsProg.release(); } \ No newline at end of file diff --git a/src/gui/SpectrogramWidget.h b/src/gui/SpectrogramWidget.h index f128c02..b0d76ac 100644 --- a/src/gui/SpectrogramWidget.h +++ b/src/gui/SpectrogramWidget.h @@ -2,60 +2,81 @@ #include #include #include +#include #include -#include #include +#include #include #include +#include "SpectrumAnalyzer.h" class SpectrogramWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core { Q_OBJECT + static constexpr int BAR_COUNT = 96; - static constexpr float DB_MIN = -80.f, DB_MAX = 0.f; + static constexpr float DB_MIN = -80.f; + static constexpr float DB_MAX = 0.f; + + static constexpr float PEAK_HOLD_SEC = 0.4f; + static constexpr float PEAK_DECAY_SEC = 0.5f; public: explicit SpectrogramWidget(QWidget *parent = nullptr); - ~SpectrogramWidget(); + ~SpectrogramWidget() override; public slots: void receiveFft(const QVector &mag); void reset(); protected: - void initializeGL() override; - void paintGL() override; + void initializeGL() override; + void paintGL() override; void mousePressEvent(QMouseEvent *e) override; private: + // GL setup void initShaders(); - void buildHeatmapTexture(); - void updateHeatmapTexture(); + void updateHeatmapTexture(); + + // Data void setStops(const QVector &stops); void loadBarColors(); void saveBarColors(); - void computeBars(); + void computeBars(float dt); - QMutex m_queueMtx; - QQueue> m_fftQueue; - QVector m_currentMag = QVector(2048, 0.f); + QMutex m_latestMtx; + QVector m_latestMag; // written by audio thread + bool m_newMagAvailable{false}; - QVector m_barSmoothed = QVector(BAR_COUNT, 0.f); - QVector m_barPeak = QVector(BAR_COUNT, 0.f); - QVector m_peakTimer = QVector (BAR_COUNT, 0); - QVector m_barHeights; - QVector m_barPeaksVec; + QVector m_currentMag = + QVector(engine::SpectrumAnalyzer::BIN_COUNT, 0.f); - QVector m_stops = { QColor("#078D70"), - QColor("#26CEAA"), - QColor("#98E8C1"), - QColor("#FFFFFF") }; + // --- bar state --- + std::array m_barSmoothed{}; + std::array m_barPeak{}; + std::array m_peakHoldTimer{}; // seconds remaining in hold - std::array m_heatmap{}; + // Uploaded to GPU each frame + std::array m_barHeights{}; + std::array m_barPeaksVec{}; - QOpenGLShaderProgram m_barsProg; - GLuint m_heatmapTex = 0; - GLuint m_vaoBars = 0; + struct BinRange { int lo, hi; }; + std::array m_binMap{}; + + QVector m_stops = { QColor("#078D70"), + QColor("#26CEAA"), + QColor("#98E8C1"), + QColor("#FFFFFF") }; + + std::array m_heatmap{}; + + QElapsedTimer m_frameTimer; + bool m_dirty{false}; // true when new data arrived since last paint + + QOpenGLShaderProgram m_barsProg; + GLuint m_heatmapTex = 0; + GLuint m_vaoBars = 0; int m_uBarHeights = -1; int m_uBarPeaks = -1;