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 {
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

View File

@@ -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);

View File

@@ -2,8 +2,22 @@
#include <cmath>
#include <algorithm>
#ifdef __EMSCRIPTEN__
#include <GLES2/gl2.h>
#else
#include <GL/gl.h>
#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<std::vector<float>>& spectra,
}
}
// Draw each channel's spectrum.
std::vector<ImVec2> points;
// Build polylines for all channels.
int nCh = static_cast<int>(spectra.size());
std::vector<std::vector<ImVec2>> allPoints(nCh);
for (int ch = 0; ch < nCh; ++ch) {
if (spectra[ch].empty()) continue;
const ChannelStyle& st = (ch < static_cast<int>(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<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
if (points.size() >= 2)
dl->AddPolyline(points.data(), static_cast<int>(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<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()) {
std::vector<ImVec2> phPoints;
for (int ch = 0; ch < nCh && ch < static_cast<int>(peakHold_.size()); ++ch) {
if (peakHold_[ch].empty()) continue;
const ChannelStyle& st = (ch < static_cast<int>(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<int>(points.size()),
viewLo, viewHi, phPoints);
if (phPoints.size() >= 2)
dl->AddPolyline(phPoints.data(), static_cast<int>(phPoints.size()),
col, ImDrawFlags_None, 1.0f);
}
}

View File

@@ -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

View File

@@ -18,15 +18,16 @@ struct UIState {
int waterfallChannel = 0;
bool waterfallMultiCh = 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 = {{
{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
}};
};