From 3b4f507df93ad31e39cc8e35800ecf9923e4dee9 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Sun, 29 Mar 2026 03:04:12 +0200 Subject: [PATCH] optional additive blending for the spectrogram --- src/audio/AudioEngine.cpp | 2 +- src/ui/Application.cpp | 5 +++ src/ui/SpectrumDisplay.cpp | 91 +++++++++++++++++++++++++++----------- src/ui/SpectrumDisplay.h | 1 + src/ui/UIState.h | 13 +++--- 5 files changed, 78 insertions(+), 34 deletions(-) diff --git a/src/audio/AudioEngine.cpp b/src/audio/AudioEngine.cpp index 0d98e69..332fd2f 100644 --- a/src/audio/AudioEngine.cpp +++ b/src/audio/AudioEngine.cpp @@ -9,7 +9,7 @@ namespace baudmine { namespace { -constexpr float kLinearEpsilon = kLinearEpsilon; // threshold for log10 of linear power +constexpr float kLinearEpsilon = 1e-20f; // threshold for log10 of linear power constexpr float kLogGuard = 1e-30f; // guard against log10(0) in compressed scale } // namespace diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index b9162ac..bf0b9bb 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -467,6 +467,9 @@ void Application::render() { if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Grid", nullptr, &specDisplay_.showGrid); ImGui::MenuItem("Fill Spectrum", nullptr, &specDisplay_.fillSpectrum); + ImGui::MenuItem("Additive Blend", nullptr, &specDisplay_.additiveBlend); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Mix multi-channel spectrum colors additively"); ImGui::Separator(); if (ImGui::MenuItem("VSync", nullptr, &vsync_)) { SDL_GL_SetSwapInterval(vsync_ ? 1 : 0); @@ -669,6 +672,7 @@ void Application::loadConfig() { showSidebar_ = config_.getBool("show_sidebar", showSidebar_); specDisplay_.peakHoldEnable = config_.getBool("peak_hold", specDisplay_.peakHoldEnable); specDisplay_.peakHoldDecay = config_.getFloat("peak_hold_decay", specDisplay_.peakHoldDecay); + specDisplay_.additiveBlend = config_.getBool("additive_blend", specDisplay_.additiveBlend); cursors_.snapToPeaks = config_.getBool("snap_to_peaks", cursors_.snapToPeaks); measurements_.traceMinFreq = config_.getFloat("trace_min_freq", measurements_.traceMinFreq); measurements_.traceMaxFreq = config_.getFloat("trace_max_freq", measurements_.traceMaxFreq); @@ -732,6 +736,7 @@ void Application::saveConfig() const { cfg.setBool("show_sidebar", showSidebar_); cfg.setBool("peak_hold", specDisplay_.peakHoldEnable); cfg.setFloat("peak_hold_decay", specDisplay_.peakHoldDecay); + cfg.setBool("additive_blend", specDisplay_.additiveBlend); cfg.setBool("snap_to_peaks", cursors_.snapToPeaks); cfg.setFloat("trace_min_freq", measurements_.traceMinFreq); cfg.setFloat("trace_max_freq", measurements_.traceMaxFreq); diff --git a/src/ui/SpectrumDisplay.cpp b/src/ui/SpectrumDisplay.cpp index bf67b18..d4a25a5 100644 --- a/src/ui/SpectrumDisplay.cpp +++ b/src/ui/SpectrumDisplay.cpp @@ -2,8 +2,22 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#else +#include +#endif + namespace baudmine { +// ImGui draw-list callbacks to switch GL blend mode for additive color mixing. +static void setAdditiveBlend(const ImDrawList*, const ImDrawCmd*) { + glBlendFunc(GL_SRC_ALPHA, GL_ONE); +} +static void restoreDefaultBlend(const ImDrawList*, const ImDrawCmd*) { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + static float freqToLogFrac(double freq, double minFreq, double maxFreq) { if (freq <= 0 || minFreq <= 0) return 0.0f; double logMin = std::log10(minFreq); @@ -152,52 +166,75 @@ void SpectrumDisplay::draw(const std::vector>& spectra, } } - // Draw each channel's spectrum. - std::vector points; + // Build polylines for all channels. int nCh = static_cast(spectra.size()); + std::vector> allPoints(nCh); for (int ch = 0; ch < nCh; ++ch) { if (spectra[ch].empty()) continue; - const ChannelStyle& st = (ch < static_cast(styles.size())) - ? styles[ch] - : styles.back(); - buildPolyline(spectra[ch], minDB, maxDB, isIQ, freqScale, posX, posY, sizeX, sizeY, - viewLo, viewHi, points); + viewLo, viewHi, allPoints[ch]); + } - // Fill - if (fillSpectrum && points.size() >= 2) { - for (size_t i = 0; i + 1 < points.size(); ++i) { - ImVec2 tl = points[i]; - ImVec2 tr = points[i + 1]; - ImVec2 bl = {tl.x, posY + sizeY}; - ImVec2 br = {tr.x, posY + sizeY}; - dl->AddQuadFilled(tl, tr, br, bl, st.fillColor); + // Draw spectra: all fills first, then all lines on top. + // Multi-channel uses additive GL blending so colors mix (green + purple = white). + { + bool additive = additiveBlend && nCh > 1; + // Use lower alpha under additive blend to avoid oversaturation. + constexpr ImU8 kLineAlpha = 180; + constexpr ImU8 kFillAlpha = 30; + + if (additive) + dl->AddCallback(setAdditiveBlend, nullptr); + + // Pass 1: fills (drawn first so lines sit on top of all fills). + if (fillSpectrum) { + for (int ch = 0; ch < nCh; ++ch) { + const auto& pts = allPoints[ch]; + if (pts.size() < 2) continue; + const ChannelStyle& st = (ch < static_cast(styles.size())) + ? styles[ch] : styles.back(); + ImU32 col = additive + ? ((st.fillColor & 0x00FFFFFF) | (static_cast(kFillAlpha) << 24)) + : st.fillColor; + for (size_t i = 0; i + 1 < pts.size(); ++i) { + dl->AddQuadFilled(pts[i], pts[i + 1], + {pts[i + 1].x, posY + sizeY}, + {pts[i].x, posY + sizeY}, col); + } } } - // Line - if (points.size() >= 2) - dl->AddPolyline(points.data(), static_cast(points.size()), - st.lineColor, ImDrawFlags_None, 1.5f); + // Pass 2: lines. + for (int ch = 0; ch < nCh; ++ch) { + const auto& pts = allPoints[ch]; + if (pts.size() < 2) continue; + const ChannelStyle& st = (ch < static_cast(styles.size())) + ? styles[ch] : styles.back(); + ImU32 col = additive + ? ((st.lineColor & 0x00FFFFFF) | (static_cast(kLineAlpha) << 24)) + : st.lineColor; + dl->AddPolyline(pts.data(), static_cast(pts.size()), + col, ImDrawFlags_None, 1.5f); + } + + if (additive) + dl->AddCallback(restoreDefaultBlend, nullptr); } - // Peak hold traces (drawn as dashed-style thin lines above the live spectrum). + // Peak hold traces. if (peakHoldEnable && !peakHold_.empty()) { + std::vector phPoints; for (int ch = 0; ch < nCh && ch < static_cast(peakHold_.size()); ++ch) { if (peakHold_[ch].empty()) continue; const ChannelStyle& st = (ch < static_cast(styles.size())) ? styles[ch] : styles.back(); - - // Use the same line color but dimmer. ImU32 col = (st.lineColor & 0x00FFFFFF) | 0x90000000; - buildPolyline(peakHold_[ch], minDB, maxDB, isIQ, freqScale, posX, posY, sizeX, sizeY, - viewLo, viewHi, points); - - if (points.size() >= 2) - dl->AddPolyline(points.data(), static_cast(points.size()), + viewLo, viewHi, phPoints); + if (phPoints.size() >= 2) + dl->AddPolyline(phPoints.data(), static_cast(phPoints.size()), col, ImDrawFlags_None, 1.0f); } } diff --git a/src/ui/SpectrumDisplay.h b/src/ui/SpectrumDisplay.h index 1a7540b..7e9ebce 100644 --- a/src/ui/SpectrumDisplay.h +++ b/src/ui/SpectrumDisplay.h @@ -46,6 +46,7 @@ public: bool showGrid = true; bool fillSpectrum = false; + bool additiveBlend = true; // additive color mixing for multi-channel bool peakHoldEnable = false; float peakHoldDecay = 20.0f; // dB/second decay rate diff --git a/src/ui/UIState.h b/src/ui/UIState.h index df02b5b..f656860 100644 --- a/src/ui/UIState.h +++ b/src/ui/UIState.h @@ -18,15 +18,16 @@ struct UIState { int waterfallChannel = 0; bool waterfallMultiCh = true; std::array channelEnabled = {true,true,true,true,true,true,true,true}; + // Complementary pairs: colors at indices 0+1, 2+3, 4+5, 6+7 sum to white. std::array channelColors = {{ - {0.20f, 0.90f, 0.30f, 1.0f}, // green - {0.70f, 0.30f, 1.00f, 1.0f}, // purple + {0.20f, 0.90f, 0.20f, 1.0f}, // green + {0.80f, 0.10f, 0.80f, 1.0f}, // purple {1.00f, 0.55f, 0.00f, 1.0f}, // orange - {0.00f, 0.75f, 1.00f, 1.0f}, // cyan + {0.00f, 0.45f, 1.00f, 1.0f}, // cyan {1.00f, 0.25f, 0.25f, 1.0f}, // red - {1.00f, 1.00f, 0.30f, 1.0f}, // yellow - {0.50f, 0.80f, 0.50f, 1.0f}, // light green - {0.80f, 0.50f, 0.80f, 1.0f}, // pink + {0.00f, 0.75f, 0.75f, 1.0f}, // teal + {1.00f, 1.00f, 0.20f, 1.0f}, // yellow + {0.00f, 0.00f, 0.80f, 1.0f}, // blue }}; };