diff --git a/src/dsp/FFTProcessor.cpp b/src/dsp/FFTProcessor.cpp index 7ede4b5..65b55f6 100644 --- a/src/dsp/FFTProcessor.cpp +++ b/src/dsp/FFTProcessor.cpp @@ -85,15 +85,13 @@ void FFTProcessor::processComplex(const float* inputIQ, } } -// Convenience overloads (no complex output). +// Convenience overloads (no complex output) — reuse scratch buffer. void FFTProcessor::processReal(const float* input, std::vector& outputDB) { - std::vector> dummy; - processReal(input, outputDB, dummy); + processReal(input, outputDB, scratchCplx_); } void FFTProcessor::processComplex(const float* inputIQ, std::vector& outputDB) { - std::vector> dummy; - processComplex(inputIQ, outputDB, dummy); + processComplex(inputIQ, outputDB, scratchCplx_); } } // namespace baudline diff --git a/src/dsp/FFTProcessor.h b/src/dsp/FFTProcessor.h index 2267d8b..fb787d3 100644 --- a/src/dsp/FFTProcessor.h +++ b/src/dsp/FFTProcessor.h @@ -50,6 +50,9 @@ private: fftwf_plan cplxPlan_ = nullptr; void destroyPlans(); + + // Scratch buffer for convenience overloads (avoids per-call allocation). + std::vector> scratchCplx_; }; } // namespace baudline diff --git a/src/dsp/SpectrumAnalyzer.cpp b/src/dsp/SpectrumAnalyzer.cpp index 5ae6be7..5941810 100644 --- a/src/dsp/SpectrumAnalyzer.cpp +++ b/src/dsp/SpectrumAnalyzer.cpp @@ -26,6 +26,9 @@ void SpectrumAnalyzer::configure(const AnalyzerSettings& settings) { hopSize_ = static_cast(settings_.fftSize * (1.0f - settings_.overlap)); if (hopSize_ < 1) hopSize_ = 1; + // Cache window gain correction (avoid recomputing per block). + windowCorrection_ = -20.0f * std::log10(windowGain_ > 0 ? windowGain_ : 1.0f); + if (sizeChanged) { accumBuf_.assign(settings_.fftSize * inCh, 0.0f); accumPos_ = 0; @@ -37,6 +40,13 @@ void SpectrumAnalyzer::configure(const AnalyzerSettings& settings) { channelComplex_.assign(nSpec, std::vector>(specSz, {0,0})); channelWaterfalls_.assign(nSpec, {}); + // Pre-allocate scratch buffers. + if (settings_.isIQ) { + windowedBuf_.resize(settings_.fftSize * 2); + } else { + chanBuf_.resize(settings_.fftSize); + } + newSpectrumReady_ = false; } } @@ -74,38 +84,27 @@ void SpectrumAnalyzer::processBlock() { int N = settings_.fftSize; int inCh = settings_.inputChannels(); int nSpec = static_cast(channelSpectra_.size()); - int specSz = fft_.spectrumSize(); - - std::vector> tempDBs(nSpec); - std::vector>> tempCplx(nSpec); if (settings_.isIQ) { - std::vector windowed(N * 2); for (int i = 0; i < N; ++i) { - windowed[2 * i] = accumBuf_[2 * i] * window_[i]; - windowed[2 * i + 1] = accumBuf_[2 * i + 1] * window_[i]; + windowedBuf_[2 * i] = accumBuf_[2 * i] * window_[i]; + windowedBuf_[2 * i + 1] = accumBuf_[2 * i + 1] * window_[i]; } - fft_.processComplex(windowed.data(), tempDBs[0], tempCplx[0]); + fft_.processComplex(windowedBuf_.data(), channelSpectra_[0], channelComplex_[0]); } else { - std::vector chanBuf(N); for (int ch = 0; ch < nSpec; ++ch) { for (int i = 0; i < N; ++i) - chanBuf[i] = accumBuf_[i * inCh + ch]; - WindowFunctions::apply(window_, chanBuf.data(), N); - fft_.processReal(chanBuf.data(), tempDBs[ch], tempCplx[ch]); + chanBuf_[i] = accumBuf_[i * inCh + ch]; + WindowFunctions::apply(window_, chanBuf_.data(), N); + fft_.processReal(chanBuf_.data(), channelSpectra_[ch], channelComplex_[ch]); } } - // Window gain correction. - float correction = -20.0f * std::log10(windowGain_ > 0 ? windowGain_ : 1.0f); - for (auto& db : tempDBs) - for (float& v : db) - v += correction; - + // Apply cached window gain correction. for (int ch = 0; ch < nSpec; ++ch) { - channelSpectra_[ch] = tempDBs[ch]; - channelComplex_[ch] = tempCplx[ch]; - channelWaterfalls_[ch].push_back(tempDBs[ch]); + for (float& v : channelSpectra_[ch]) + v += windowCorrection_; + channelWaterfalls_[ch].push_back(channelSpectra_[ch]); if (channelWaterfalls_[ch].size() > kWaterfallHistory) channelWaterfalls_[ch].pop_front(); } diff --git a/src/dsp/SpectrumAnalyzer.h b/src/dsp/SpectrumAnalyzer.h index e1e3a58..1bfae58 100644 --- a/src/dsp/SpectrumAnalyzer.h +++ b/src/dsp/SpectrumAnalyzer.h @@ -63,6 +63,11 @@ private: std::vector>> channelComplex_; std::vector>> channelWaterfalls_; bool newSpectrumReady_ = false; + + // Pre-allocated scratch buffers for processBlock (avoid per-frame heap alloc) + std::vector windowedBuf_; // IQ mode: windowed interleaved samples + std::vector chanBuf_; // real mode: single-channel scratch + float windowCorrection_ = 0.0f; }; } // namespace baudline diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 558d4a8..a9e4bf9 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -183,24 +183,24 @@ void Application::processAudio() { int nSpec = analyzer_.numSpectra(); if (waterfallMultiCh_ && nSpec > 1) { // Multi-channel overlay waterfall: physical + math channels. - std::vector> wfSpectra; - std::vector wfChInfo; + wfSpectraScratch_.clear(); + wfChInfoScratch_.clear(); for (int ch = 0; ch < nSpec; ++ch) { const auto& c = channelColors_[ch % kMaxChannels]; - wfSpectra.push_back(analyzer_.channelSpectrum(ch)); - wfChInfo.push_back({c.x, c.y, c.z, + wfSpectraScratch_.push_back(analyzer_.channelSpectrum(ch)); + wfChInfoScratch_.push_back({c.x, c.y, c.z, channelEnabled_[ch % kMaxChannels]}); } for (size_t mi = 0; mi < mathChannels_.size(); ++mi) { if (mathChannels_[mi].enabled && mathChannels_[mi].waterfall && mi < mathSpectra_.size()) { const auto& c = mathChannels_[mi].color; - wfSpectra.push_back(mathSpectra_[mi]); - wfChInfo.push_back({c.x, c.y, c.z, true}); + wfSpectraScratch_.push_back(mathSpectra_[mi]); + wfChInfoScratch_.push_back({c.x, c.y, c.z, true}); } } - waterfall_.pushLineMulti(wfSpectra, wfChInfo, minDB_, maxDB_); + waterfall_.pushLineMulti(wfSpectraScratch_, wfChInfoScratch_, minDB_, maxDB_); } else { int wfCh = std::clamp(waterfallChannel_, 0, nSpec - 1); waterfall_.pushLine(analyzer_.channelSpectrum(wfCh), @@ -601,37 +601,34 @@ void Application::renderSpectrumPanel() { // Build per-channel styles and combine physical + math spectra. int nPhys = analyzer_.numSpectra(); int nMath = static_cast(mathSpectra_.size()); - int nTotal = nPhys + nMath; - std::vector> allSpectra; - std::vector styles; - allSpectra.reserve(nTotal); - styles.reserve(nTotal); + allSpectraScratch_.clear(); + stylesScratch_.clear(); // Physical channels. for (int ch = 0; ch < nPhys; ++ch) { - allSpectra.push_back(analyzer_.channelSpectrum(ch)); + allSpectraScratch_.push_back(analyzer_.channelSpectrum(ch)); const auto& c = channelColors_[ch % kMaxChannels]; uint8_t r = static_cast(c.x * 255); uint8_t g = static_cast(c.y * 255); uint8_t b = static_cast(c.z * 255); - styles.push_back({IM_COL32(r, g, b, 220), IM_COL32(r, g, b, 35)}); + stylesScratch_.push_back({IM_COL32(r, g, b, 220), IM_COL32(r, g, b, 35)}); } // Math channels. for (int mi = 0; mi < nMath; ++mi) { if (mi < static_cast(mathChannels_.size()) && mathChannels_[mi].enabled) { - allSpectra.push_back(mathSpectra_[mi]); + allSpectraScratch_.push_back(mathSpectra_[mi]); const auto& c = mathChannels_[mi].color; uint8_t r = static_cast(c.x * 255); uint8_t g = static_cast(c.y * 255); uint8_t b = static_cast(c.z * 255); - styles.push_back({IM_COL32(r, g, b, 220), IM_COL32(r, g, b, 35)}); + stylesScratch_.push_back({IM_COL32(r, g, b, 220), IM_COL32(r, g, b, 35)}); } } - specDisplay_.updatePeakHold(allSpectra); - specDisplay_.draw(allSpectra, styles, minDB_, maxDB_, + specDisplay_.updatePeakHold(allSpectraScratch_); + specDisplay_.draw(allSpectraScratch_, stylesScratch_, minDB_, maxDB_, settings_.sampleRate, settings_.isIQ, freqScale_, specPosX_, specPosY_, specSizeX_, specSizeY_, viewLo_, viewHi_); diff --git a/src/ui/Application.h b/src/ui/Application.h index d2bea2a..e878fb4 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -170,6 +170,12 @@ private: // Panel geometry (stored for cursor interaction) float specPosX_ = 0, specPosY_ = 0, specSizeX_ = 0, specSizeY_ = 0; float wfPosX_ = 0, wfPosY_ = 0, wfSizeX_ = 0, wfSizeY_ = 0; + + // Pre-allocated scratch buffers (avoid per-frame heap allocations) + std::vector> wfSpectraScratch_; + std::vector wfChInfoScratch_; + std::vector> allSpectraScratch_; + std::vector stylesScratch_; }; } // namespace baudline diff --git a/src/ui/WaterfallDisplay.cpp b/src/ui/WaterfallDisplay.cpp index 152b1d6..dfc352e 100644 --- a/src/ui/WaterfallDisplay.cpp +++ b/src/ui/WaterfallDisplay.cpp @@ -71,28 +71,35 @@ void WaterfallDisplay::pushLineMulti( float minDB, float maxDB) { if (width_ == 0 || height_ == 0) return; - int nCh = static_cast(channelSpectra.size()); int row = currentRow_; int rowOffset = row * width_ * 3; float range = maxDB - minDB; if (range < 1.0f) range = 1.0f; + float invRange = 1.0f / range; + + // Pre-filter enabled channels to avoid per-texel branching. + struct ActiveCh { const float* data; int bins; float r, g, b; }; + activeChBuf_.clear(); + int nCh = std::min(static_cast(channelSpectra.size()), + static_cast(channels.size())); + for (int ch = 0; ch < nCh; ++ch) { + if (!channels[ch].enabled || channelSpectra[ch].empty()) continue; + activeChBuf_.push_back({channelSpectra[ch].data(), + static_cast(channelSpectra[ch].size()), + channels[ch].r, channels[ch].g, channels[ch].b}); + } // One texel per bin — direct 1:1 mapping. for (int x = 0; x < width_; ++x) { float accR = 0.0f, accG = 0.0f, accB = 0.0f; - for (int ch = 0; ch < nCh; ++ch) { - if (ch >= static_cast(channels.size()) || !channels[ch].enabled) - continue; - if (channelSpectra[ch].empty()) continue; + for (const auto& ac : activeChBuf_) { + float dB = (x < ac.bins) ? ac.data[x] : -200.0f; + float intensity = std::clamp((dB - minDB) * invRange, 0.0f, 1.0f); - int bins = static_cast(channelSpectra[ch].size()); - float dB = (x < bins) ? channelSpectra[ch][x] : -200.0f; - float intensity = std::clamp((dB - minDB) / range, 0.0f, 1.0f); - - accR += channels[ch].r * intensity; - accG += channels[ch].g * intensity; - accB += channels[ch].b * intensity; + accR += ac.r * intensity; + accG += ac.g * intensity; + accB += ac.b * intensity; } pixelBuf_[rowOffset + x * 3 + 0] = @@ -112,7 +119,7 @@ void WaterfallDisplay::uploadRow(int row) { glTexSubImage2D(GL_TEXTURE_2D, 0, 0, row, width_, 1, GL_RGB, GL_UNSIGNED_BYTE, pixelBuf_.data() + row * width_ * 3); - glBindTexture(GL_TEXTURE_2D, 0); + // Note: no unbind — ImGui will bind its own textures before drawing. } } // namespace baudline diff --git a/src/ui/WaterfallDisplay.h b/src/ui/WaterfallDisplay.h index fff9a74..6793eb6 100644 --- a/src/ui/WaterfallDisplay.h +++ b/src/ui/WaterfallDisplay.h @@ -49,6 +49,10 @@ private: ColorMap colorMap_; std::vector pixelBuf_; + + // Scratch buffer for pushLineMulti (pre-filtered enabled channels). + struct ActiveCh { const float* data; int bins; float r, g, b; }; + std::vector activeChBuf_; }; } // namespace baudline