optional additive blending for the spectrogram

This commit is contained in:
2026-03-29 03:04:12 +02:00
parent baf6fd94cc
commit 3b4f507df9
5 changed files with 78 additions and 34 deletions

View File

@@ -9,7 +9,7 @@
namespace baudmine { namespace baudmine {
namespace { 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 constexpr float kLogGuard = 1e-30f; // guard against log10(0) in compressed scale
} // namespace } // namespace

View File

@@ -467,6 +467,9 @@ void Application::render() {
if (ImGui::BeginMenu("View")) { if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Grid", nullptr, &specDisplay_.showGrid); ImGui::MenuItem("Grid", nullptr, &specDisplay_.showGrid);
ImGui::MenuItem("Fill Spectrum", nullptr, &specDisplay_.fillSpectrum); 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(); ImGui::Separator();
if (ImGui::MenuItem("VSync", nullptr, &vsync_)) { if (ImGui::MenuItem("VSync", nullptr, &vsync_)) {
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0); SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
@@ -669,6 +672,7 @@ void Application::loadConfig() {
showSidebar_ = config_.getBool("show_sidebar", showSidebar_); showSidebar_ = config_.getBool("show_sidebar", showSidebar_);
specDisplay_.peakHoldEnable = config_.getBool("peak_hold", specDisplay_.peakHoldEnable); specDisplay_.peakHoldEnable = config_.getBool("peak_hold", specDisplay_.peakHoldEnable);
specDisplay_.peakHoldDecay = config_.getFloat("peak_hold_decay", specDisplay_.peakHoldDecay); 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); cursors_.snapToPeaks = config_.getBool("snap_to_peaks", cursors_.snapToPeaks);
measurements_.traceMinFreq = config_.getFloat("trace_min_freq", measurements_.traceMinFreq); measurements_.traceMinFreq = config_.getFloat("trace_min_freq", measurements_.traceMinFreq);
measurements_.traceMaxFreq = config_.getFloat("trace_max_freq", measurements_.traceMaxFreq); measurements_.traceMaxFreq = config_.getFloat("trace_max_freq", measurements_.traceMaxFreq);
@@ -732,6 +736,7 @@ void Application::saveConfig() const {
cfg.setBool("show_sidebar", showSidebar_); cfg.setBool("show_sidebar", showSidebar_);
cfg.setBool("peak_hold", specDisplay_.peakHoldEnable); cfg.setBool("peak_hold", specDisplay_.peakHoldEnable);
cfg.setFloat("peak_hold_decay", specDisplay_.peakHoldDecay); cfg.setFloat("peak_hold_decay", specDisplay_.peakHoldDecay);
cfg.setBool("additive_blend", specDisplay_.additiveBlend);
cfg.setBool("snap_to_peaks", cursors_.snapToPeaks); cfg.setBool("snap_to_peaks", cursors_.snapToPeaks);
cfg.setFloat("trace_min_freq", measurements_.traceMinFreq); cfg.setFloat("trace_min_freq", measurements_.traceMinFreq);
cfg.setFloat("trace_max_freq", measurements_.traceMaxFreq); cfg.setFloat("trace_max_freq", measurements_.traceMaxFreq);

View File

@@ -2,8 +2,22 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#ifdef __EMSCRIPTEN__
#include <GLES2/gl2.h>
#else
#include <GL/gl.h>
#endif
namespace baudmine { 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) { static float freqToLogFrac(double freq, double minFreq, double maxFreq) {
if (freq <= 0 || minFreq <= 0) return 0.0f; if (freq <= 0 || minFreq <= 0) return 0.0f;
double logMin = std::log10(minFreq); double logMin = std::log10(minFreq);
@@ -152,52 +166,75 @@ void SpectrumDisplay::draw(const std::vector<std::vector<float>>& spectra,
} }
} }
// Draw each channel's spectrum. // Build polylines for all channels.
std::vector<ImVec2> points;
int nCh = static_cast<int>(spectra.size()); int nCh = static_cast<int>(spectra.size());
std::vector<std::vector<ImVec2>> allPoints(nCh);
for (int ch = 0; ch < nCh; ++ch) { for (int ch = 0; ch < nCh; ++ch) {
if (spectra[ch].empty()) continue; if (spectra[ch].empty()) continue;
const ChannelStyle& st = (ch < static_cast<int>(styles.size()))
? styles[ch]
: styles.back();
buildPolyline(spectra[ch], minDB, maxDB, buildPolyline(spectra[ch], minDB, maxDB,
isIQ, freqScale, posX, posY, sizeX, sizeY, isIQ, freqScale, posX, posY, sizeX, sizeY,
viewLo, viewHi, points); viewLo, viewHi, allPoints[ch]);
}
// Fill // Draw spectra: all fills first, then all lines on top.
if (fillSpectrum && points.size() >= 2) { // Multi-channel uses additive GL blending so colors mix (green + purple = white).
for (size_t i = 0; i + 1 < points.size(); ++i) { {
ImVec2 tl = points[i]; bool additive = additiveBlend && nCh > 1;
ImVec2 tr = points[i + 1]; // Use lower alpha under additive blend to avoid oversaturation.
ImVec2 bl = {tl.x, posY + sizeY}; constexpr ImU8 kLineAlpha = 180;
ImVec2 br = {tr.x, posY + sizeY}; constexpr ImU8 kFillAlpha = 30;
dl->AddQuadFilled(tl, tr, br, bl, st.fillColor);
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<int>(styles.size()))
? styles[ch] : styles.back();
ImU32 col = additive
? ((st.fillColor & 0x00FFFFFF) | (static_cast<ImU32>(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 // Pass 2: lines.
if (points.size() >= 2) for (int ch = 0; ch < nCh; ++ch) {
dl->AddPolyline(points.data(), static_cast<int>(points.size()), const auto& pts = allPoints[ch];
st.lineColor, ImDrawFlags_None, 1.5f); if (pts.size() < 2) continue;
const ChannelStyle& st = (ch < static_cast<int>(styles.size()))
? styles[ch] : styles.back();
ImU32 col = additive
? ((st.lineColor & 0x00FFFFFF) | (static_cast<ImU32>(kLineAlpha) << 24))
: st.lineColor;
dl->AddPolyline(pts.data(), static_cast<int>(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()) { if (peakHoldEnable && !peakHold_.empty()) {
std::vector<ImVec2> phPoints;
for (int ch = 0; ch < nCh && ch < static_cast<int>(peakHold_.size()); ++ch) { for (int ch = 0; ch < nCh && ch < static_cast<int>(peakHold_.size()); ++ch) {
if (peakHold_[ch].empty()) continue; if (peakHold_[ch].empty()) continue;
const ChannelStyle& st = (ch < static_cast<int>(styles.size())) const ChannelStyle& st = (ch < static_cast<int>(styles.size()))
? styles[ch] : styles.back(); ? styles[ch] : styles.back();
// Use the same line color but dimmer.
ImU32 col = (st.lineColor & 0x00FFFFFF) | 0x90000000; ImU32 col = (st.lineColor & 0x00FFFFFF) | 0x90000000;
buildPolyline(peakHold_[ch], minDB, maxDB, buildPolyline(peakHold_[ch], minDB, maxDB,
isIQ, freqScale, posX, posY, sizeX, sizeY, isIQ, freqScale, posX, posY, sizeX, sizeY,
viewLo, viewHi, points); viewLo, viewHi, phPoints);
if (phPoints.size() >= 2)
if (points.size() >= 2) dl->AddPolyline(phPoints.data(), static_cast<int>(phPoints.size()),
dl->AddPolyline(points.data(), static_cast<int>(points.size()),
col, ImDrawFlags_None, 1.0f); col, ImDrawFlags_None, 1.0f);
} }
} }

View File

@@ -46,6 +46,7 @@ public:
bool showGrid = true; bool showGrid = true;
bool fillSpectrum = false; bool fillSpectrum = false;
bool additiveBlend = true; // additive color mixing for multi-channel
bool peakHoldEnable = false; bool peakHoldEnable = false;
float peakHoldDecay = 20.0f; // dB/second decay rate float peakHoldDecay = 20.0f; // dB/second decay rate

View File

@@ -18,15 +18,16 @@ struct UIState {
int waterfallChannel = 0; int waterfallChannel = 0;
bool waterfallMultiCh = true; bool waterfallMultiCh = true;
std::array<bool, kMaxChannels> channelEnabled = {true,true,true,true,true,true,true,true}; std::array<bool, kMaxChannels> 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<ImVec4, kMaxChannels> channelColors = {{ std::array<ImVec4, kMaxChannels> channelColors = {{
{0.20f, 0.90f, 0.30f, 1.0f}, // green {0.20f, 0.90f, 0.20f, 1.0f}, // green
{0.70f, 0.30f, 1.00f, 1.0f}, // purple {0.80f, 0.10f, 0.80f, 1.0f}, // purple
{1.00f, 0.55f, 0.00f, 1.0f}, // orange {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, 0.25f, 0.25f, 1.0f}, // red
{1.00f, 1.00f, 0.30f, 1.0f}, // yellow {0.00f, 0.75f, 0.75f, 1.0f}, // teal
{0.50f, 0.80f, 0.50f, 1.0f}, // light green {1.00f, 1.00f, 0.20f, 1.0f}, // yellow
{0.80f, 0.50f, 0.80f, 1.0f}, // pink {0.00f, 0.00f, 0.80f, 1.0f}, // blue
}}; }};
}; };