some more refactoring

- UIState.h: Replaced C-style arrays bool[] and ImVec4[] with
std::array<bool, kMaxChannels> and std::array<ImVec4, kMaxChannels> for
type safety and STL compatibility.
  - Application.cpp: Extracted duplicated DPI calculation (lines 189-196
and 475-481) into a systemDpiScale() helper method, reducing both call
sites to single-line calls.
  - Application.cpp: Replaced magic 1400, 900 window dimensions with
kDefaultWindowWidth/kDefaultWindowHeight constants (defined in Types.h).
  - AudioEngine.cpp: Named the magic epsilon thresholds 1e-20f →
kLinearEpsilon and 1e-30f → kLogGuard with explanatory comments, defined
in an anonymous namespace.
  - Types.h: Added kDefaultWindowWidth and kDefaultWindowHeight
constants.
This commit is contained in:
2026-03-28 16:09:29 +01:00
parent b42d7fb69b
commit baf6fd94cc
11 changed files with 105 additions and 109 deletions

View File

@@ -8,6 +8,11 @@
namespace baudmine {
namespace {
constexpr float kLinearEpsilon = kLinearEpsilon; // threshold for log10 of linear power
constexpr float kLogGuard = 1e-30f; // guard against log10(0) in compressed scale
} // namespace
AudioEngine::AudioEngine() = default;
// ── Device enumeration ───────────────────────────────────────────────────────
@@ -271,7 +276,7 @@ void AudioEngine::computeMathChannels() {
out.resize(specSz);
if (!mc.enabled) {
std::fill(out.begin(), out.end(), -200.0f);
std::fill(out.begin(), out.end(), kNoSignalDB);
continue;
}
@@ -283,7 +288,7 @@ void AudioEngine::computeMathChannels() {
const auto& yC = getComplex(sy);
for (int i = 0; i < specSz; ++i) {
float val = -200.0f;
float val = kNoSignalDB;
switch (mc.op) {
case MathOp::Negate: val = -xDB[i]; break;
case MathOp::Absolute: val = std::abs(xDB[i]); break;
@@ -292,21 +297,21 @@ void AudioEngine::computeMathChannels() {
case MathOp::Sqrt: val = 0.5f * xDB[i]; break;
case MathOp::Log: {
float lin = std::pow(10.0f, xDB[i] / 10.0f);
val = 10.0f * std::log10(lin + 1e-30f);
val = 10.0f * std::log10(lin + kLogGuard);
break;
}
case MathOp::Add: {
float lx = std::pow(10.0f, xDB[i] / 10.0f);
float ly = std::pow(10.0f, yDB[i] / 10.0f);
float s = lx + ly;
val = (s > 1e-20f) ? 10.0f * std::log10(s) : -200.0f;
val = (s > kLinearEpsilon) ? 10.0f * std::log10(s) : kNoSignalDB;
break;
}
case MathOp::Subtract: {
float lx = std::pow(10.0f, xDB[i] / 10.0f);
float ly = std::pow(10.0f, yDB[i] / 10.0f);
float d = std::abs(lx - ly);
val = (d > 1e-20f) ? 10.0f * std::log10(d) : -200.0f;
val = (d > kLinearEpsilon) ? 10.0f * std::log10(d) : kNoSignalDB;
break;
}
case MathOp::Multiply:
@@ -317,7 +322,7 @@ void AudioEngine::computeMathChannels() {
i < static_cast<int>(yC.size())) {
auto cross = xC[i] * std::conj(yC[i]);
val = std::atan2(cross.imag(), cross.real())
* (180.0f / 3.14159265f);
* (180.0f / kPi);
}
break;
}
@@ -326,7 +331,7 @@ void AudioEngine::computeMathChannels() {
i < static_cast<int>(yC.size())) {
auto cross = xC[i] * std::conj(yC[i]);
float mag2 = std::norm(cross);
val = (mag2 > 1e-20f) ? 10.0f * std::log10(mag2) : -200.0f;
val = (mag2 > kLinearEpsilon) ? 10.0f * std::log10(mag2) : kNoSignalDB;
}
break;
}

View File

@@ -50,7 +50,6 @@ public:
// ── Math channels ──
std::vector<MathChannel>& mathChannels() { return mathChannels_; }
const std::vector<MathChannel>& mathChannels() const { return mathChannels_; }
std::vector<std::vector<float>>& mathSpectra() { return mathSpectra_; }
const std::vector<std::vector<float>>& mathSpectra() const { return mathSpectra_; }
void computeMathChannels();

View File

@@ -16,6 +16,15 @@ constexpr int kMinFFTSize = 256;
constexpr int kMaxFFTSize = 65536;
constexpr int kDefaultFFTSize = 4096;
constexpr int kWaterfallHistory = 2048;
constexpr int kDefaultWindowWidth = 1400;
constexpr int kDefaultWindowHeight = 900;
// ── Shared display constants ────────────────────────────────────────────────
constexpr float kNoSignalDB = -200.0f; // sentinel for "no signal" in dB
constexpr float kZoomFactor = 0.85f; // scroll-wheel zoom step
constexpr float kMinLogBinFrac = 0.001f; // minimum bin fraction for log scale
constexpr double kPi = 3.14159265358979323846;
// ── Enumerations ─────────────────────────────────────────────────────────────
@@ -87,6 +96,16 @@ inline const char* inputFormatName(InputFormat f) {
}
}
// ── Frequency range helpers ──────────────────────────────────────────────────
inline double freqMin(double sampleRate, bool isIQ) {
return isIQ ? -sampleRate / 2.0 : 0.0;
}
inline double freqMax(double sampleRate, bool isIQ) {
return sampleRate / 2.0;
}
// ── Formatting helpers ───────────────────────────────────────────────────────
// Format frequency into buf with fixed-width numeric field per unit range.

View File

@@ -4,8 +4,6 @@
namespace baudmine {
static constexpr double kPi = 3.14159265358979323846;
void WindowFunctions::generate(WindowType type, int size, std::vector<float>& out,
float kaiserBeta) {
out.resize(size);

View File

@@ -104,6 +104,17 @@ void Application::requestUIScale(float scale) {
pendingScale_ = scale;
}
float Application::systemDpiScale() const {
#ifdef __EMSCRIPTEN__
return js_devicePixelRatio();
#else
float ddpi = 0;
if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) == 0 && ddpi > 0)
return ddpi / 96.0f;
return 1.0f;
#endif
}
// ── Lifecycle ───────────────────────────────────────────────────────────────
Application::~Application() {
@@ -147,7 +158,7 @@ bool Application::init(int argc, char** argv) {
window_ = SDL_CreateWindow("Baudmine Spectrum Analyzer",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
1400, 900,
kDefaultWindowWidth, kDefaultWindowHeight,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE |
SDL_WINDOW_ALLOW_HIGHDPI);
if (!window_) {
@@ -186,14 +197,9 @@ bool Application::init(int argc, char** argv) {
// DPI-aware UI scaling
{
float dpiScale = 1.0f;
float dpiScale = systemDpiScale();
#ifdef __EMSCRIPTEN__
dpiScale = js_devicePixelRatio();
lastDpr_ = dpiScale;
#else
float ddpi = 0;
if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) == 0 && ddpi > 0)
dpiScale = ddpi / 96.0f;
#endif
applyUIScale((uiScale_ > 0.0f) ? uiScale_ : dpiScale);
}
@@ -472,15 +478,7 @@ void Application::render() {
int curPct = static_cast<int>(appliedScale_ * 100.0f + 0.5f);
if (ImGui::MenuItem("Auto", nullptr, uiScale_ == 0.0f)) {
uiScale_ = 0.0f;
float dpiScale = 1.0f;
#ifdef __EMSCRIPTEN__
dpiScale = js_devicePixelRatio();
#else
float ddpi = 0;
if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) == 0 && ddpi > 0)
dpiScale = ddpi / 96.0f;
#endif
requestUIScale(dpiScale);
requestUIScale(systemDpiScale());
saveConfig();
}
for (int s : kScales) {
@@ -542,9 +540,9 @@ void Application::render() {
measurements_, colorMap_, waterfall_);
ImGui::EndChild();
if (controlPanel_.needsAnalyzerUpdate())
if (controlPanel_.consumeUpdateRequest())
updateAnalyzerSettings();
if (controlPanel_.needsSave())
if (controlPanel_.consumeSaveRequest())
saveConfig();
ImGui::SameLine();

View File

@@ -66,6 +66,7 @@ private:
float lastDpr_ = 0.0f;
void applyUIScale(float scale);
void requestUIScale(float scale);
float systemDpiScale() const;
void syncCanvasSize();
// UI visibility

View File

@@ -128,7 +128,7 @@ void ControlPanel::render(AudioEngine& audio, UIState& ui,
ImGui::SameLine();
if (ImGui::Button(isLog ? "Logarithmic" : "Linear", {ImGui::GetContentRegionAvail().x, 0})) {
if (canLog) {
constexpr float kMinBF = 0.001f;
constexpr float kMinBF = kMinLogBinFrac;
float logMin = std::log10(kMinBF);
auto screenToBin = [&](float sf) -> float {
if (isLog) return std::pow(10.0f, logMin + sf * (0.0f - logMin));

View File

@@ -16,15 +16,14 @@ class WaterfallDisplay;
class ControlPanel {
public:
// Render the sidebar. Returns true if config should be saved.
void render(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
Measurements& measurements, ColorMap& colorMap,
WaterfallDisplay& waterfall);
// Action flags — checked and cleared by Application after render().
bool needsSave() { bool v = needsSave_; needsSave_ = false; return v; }
bool needsAnalyzerUpdate() { bool v = needsUpdate_; needsUpdate_ = false; return v; }
// Consume action flags set during render(). Returns true once, then resets.
bool consumeSaveRequest() { bool v = needsSave_; needsSave_ = false; return v; }
bool consumeUpdateRequest() { bool v = needsUpdate_; needsUpdate_ = false; return v; }
// FFT / analysis controls
static constexpr int kFFTSizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536};

View File

@@ -8,12 +8,37 @@
namespace baudmine {
bool DisplayPanel::splitReleased() {
bool v = splitWasReleased_;
splitWasReleased_ = false;
return v;
namespace {
// Zoom the view range centered on a screen-fraction cursor position.
void zoomView(float& viewLo, float& viewHi, float cursorScreenFrac, float wheelDir) {
float viewFrac = viewLo + cursorScreenFrac * (viewHi - viewLo);
float factor = (wheelDir > 0) ? kZoomFactor : 1.0f / kZoomFactor;
float newSpan = std::clamp((viewHi - viewLo) * factor, 0.001f, 1.0f);
float newLo = viewFrac - cursorScreenFrac * newSpan;
float newHi = newLo + newSpan;
if (newLo < 0.0f) { newHi -= newLo; newLo = 0.0f; }
if (newHi > 1.0f) { newLo -= (newHi - 1.0f); newHi = 1.0f; }
viewLo = std::clamp(newLo, 0.0f, 1.0f);
viewHi = std::clamp(newHi, 0.0f, 1.0f);
}
// Pan the view range by a screen-pixel delta.
void panView(float& viewLo, float& viewHi, float dxPixels, float panelWidth) {
float panFrac = -dxPixels / panelWidth * (viewHi - viewLo);
float span = viewHi - viewLo;
float newLo = viewLo + panFrac;
float newHi = viewHi + panFrac;
if (newLo < 0.0f) { newLo = 0.0f; newHi = span; }
if (newHi > 1.0f) { newHi = 1.0f; newLo = 1.0f - span; }
viewLo = newLo;
viewHi = newHi;
}
} // anonymous namespace
void DisplayPanel::renderSpectrum(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
Measurements& measurements) {
@@ -117,7 +142,7 @@ void DisplayPanel::renderWaterfall(AudioEngine& audio, UIState& ui,
{pos.x + availW, yStart + spanH},
{ui.viewLo, v1}, {ui.viewHi, v0});
} else {
constexpr float kMinBinFrac = 0.001f;
constexpr float kMinBinFrac = kMinLogBinFrac;
float logMin2 = std::log10(kMinBinFrac);
float logMax2 = 0.0f;
int numStrips = std::min(512, static_cast<int>(availW));
@@ -154,12 +179,12 @@ void DisplayPanel::renderWaterfall(AudioEngine& audio, UIState& ui,
// ── Frequency axis labels ──
ImU32 textCol = IM_COL32(180, 180, 200, 200);
double freqFullMin = settings.isIQ ? -settings.sampleRate / 2.0 : 0.0;
double freqFullMax = settings.isIQ ? settings.sampleRate / 2.0 : settings.sampleRate / 2.0;
double freqFullMin = freqMin(settings.sampleRate, settings.isIQ);
double freqFullMax = freqMax(settings.sampleRate, settings.isIQ);
auto viewFracToFreq = [&](float vf) -> double {
if (logMode) {
constexpr float kMinBinFrac = 0.001f;
constexpr float kMinBinFrac = kMinLogBinFrac;
float logMin2 = std::log10(kMinBinFrac);
float logMax2 = 0.0f;
float binFrac = std::pow(10.0f, logMin2 + vf * (logMax2 - logMin2));
@@ -206,8 +231,8 @@ void DisplayPanel::renderWaterfall(AudioEngine& audio, UIState& ui,
settings.isIQ, ui.freqScale,
ui.viewLo, ui.viewHi);
int bins = audio.spectrumSize();
double fMin = settings.isIQ ? -settings.sampleRate / 2.0 : 0.0;
double fMax = settings.isIQ ? settings.sampleRate / 2.0 : settings.sampleRate / 2.0;
double fMin = freqMin(settings.sampleRate, settings.isIQ);
double fMax = freqMax(settings.sampleRate, settings.isIQ);
int bin = static_cast<int>((freq - fMin) / (fMax - fMin) * (bins - 1));
bin = std::clamp(bin, 0, bins - 1);
@@ -225,35 +250,11 @@ void DisplayPanel::renderWaterfall(AudioEngine& audio, UIState& ui,
}
if (inWaterfall) {
if (io.MouseWheel != 0) {
float cursorFrac = (mx - pos.x) / availW;
float viewFrac = ui.viewLo + cursorFrac * (ui.viewHi - ui.viewLo);
if (io.MouseWheel != 0)
zoomView(ui.viewLo, ui.viewHi, (mx - pos.x) / availW, io.MouseWheel);
float zoomFactor = (io.MouseWheel > 0) ? 0.85f : 1.0f / 0.85f;
float newSpan = (ui.viewHi - ui.viewLo) * zoomFactor;
newSpan = std::clamp(newSpan, 0.001f, 1.0f);
float newLo = viewFrac - cursorFrac * newSpan;
float newHi = newLo + newSpan;
if (newLo < 0.0f) { newHi -= newLo; newLo = 0.0f; }
if (newHi > 1.0f) { newLo -= (newHi - 1.0f); newHi = 1.0f; }
// Safe to cast away const on ui here — caller passes mutable UIState
ui.viewLo = std::clamp(newLo, 0.0f, 1.0f);
ui.viewHi = std::clamp(newHi, 0.0f, 1.0f);
}
if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 1.0f)) {
float dx = io.MouseDelta.x;
float panFrac = -dx / availW * (ui.viewHi - ui.viewLo);
float newLo = ui.viewLo + panFrac;
float newHi = ui.viewHi + panFrac;
float span = ui.viewHi - ui.viewLo;
if (newLo < 0.0f) { newLo = 0.0f; newHi = span; }
if (newHi > 1.0f) { newHi = 1.0f; newLo = 1.0f - span; }
ui.viewLo = newLo;
ui.viewHi = newHi;
}
if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 1.0f))
panView(ui.viewLo, ui.viewHi, io.MouseDelta.x, availW);
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) {
ui.viewLo = 0.0f;
@@ -293,8 +294,8 @@ void DisplayPanel::renderHoverOverlay(const AudioEngine& audio, const UIState& u
// Hover info (right side)
{
int bins = audio.spectrumSize();
double fMin = settings.isIQ ? -settings.sampleRate / 2.0 : 0.0;
double fMax = settings.isIQ ? settings.sampleRate / 2.0 : settings.sampleRate / 2.0;
double fMin = freqMin(settings.sampleRate, settings.isIQ);
double fMax = freqMax(settings.sampleRate, settings.isIQ);
double binCenterFreq = fMin + (static_cast<double>(cursors.hover.bin) + 0.5)
/ bins * (fMax - fMin);
@@ -336,9 +337,9 @@ void DisplayPanel::handleSpectrumInput(AudioEngine& audio, UIState& ui,
float dB = specDisplay.screenYToDB(my, posY, sizeY, ui.minDB, ui.maxDB);
int bins = audio.spectrumSize();
double freqMin = settings.isIQ ? -settings.sampleRate / 2.0 : 0.0;
double freqMax = settings.isIQ ? settings.sampleRate / 2.0 : settings.sampleRate / 2.0;
int bin = static_cast<int>((freq - freqMin) / (freqMax - freqMin) * (bins - 1));
double fMin = freqMin(settings.sampleRate, settings.isIQ);
double fMax = freqMax(settings.sampleRate, settings.isIQ);
int bin = static_cast<int>((freq - fMin) / (fMax - fMin) * (bins - 1));
bin = std::clamp(bin, 0, bins - 1);
int curCh = std::clamp(ui.waterfallChannel, 0, audio.totalNumSpectra() - 1);
@@ -371,33 +372,11 @@ void DisplayPanel::handleSpectrumInput(AudioEngine& audio, UIState& ui,
}
}
else if (io.MouseWheel != 0) {
float cursorFrac = (mx - posX) / sizeX;
float viewFrac = ui.viewLo + cursorFrac * (ui.viewHi - ui.viewLo);
float zoomFactor = (io.MouseWheel > 0) ? 0.85f : 1.0f / 0.85f;
float newSpan = (ui.viewHi - ui.viewLo) * zoomFactor;
newSpan = std::clamp(newSpan, 0.001f, 1.0f);
float newLo = viewFrac - cursorFrac * newSpan;
float newHi = newLo + newSpan;
if (newLo < 0.0f) { newHi -= newLo; newLo = 0.0f; }
if (newHi > 1.0f) { newLo -= (newHi - 1.0f); newHi = 1.0f; }
ui.viewLo = std::clamp(newLo, 0.0f, 1.0f);
ui.viewHi = std::clamp(newHi, 0.0f, 1.0f);
zoomView(ui.viewLo, ui.viewHi, (mx - posX) / sizeX, io.MouseWheel);
}
if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 1.0f)) {
float dx = io.MouseDelta.x;
float panFrac = -dx / sizeX * (ui.viewHi - ui.viewLo);
float newLo = ui.viewLo + panFrac;
float newHi = ui.viewHi + panFrac;
float span = ui.viewHi - ui.viewLo;
if (newLo < 0.0f) { newLo = 0.0f; newHi = span; }
if (newHi > 1.0f) { newHi = 1.0f; newLo = 1.0f - span; }
ui.viewLo = newLo;
ui.viewHi = newHi;
}
if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 1.0f))
panView(ui.viewLo, ui.viewHi, io.MouseDelta.x, sizeX);
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) {
ui.viewLo = 0.0f;
@@ -427,6 +406,7 @@ void DisplayPanel::handleTouch(const SDL_Event& event, UIState& ui, SDL_Window*
if (nf >= 2) {
SDL_Finger* f0 = SDL_GetTouchFinger(tid, 0);
SDL_Finger* f1 = SDL_GetTouchFinger(tid, 1);
if (!f0 || !f1) return;
float x0 = f0->x * w, x1 = f1->x * w;
float dx = x1 - x0, dy = (f1->y - f0->y) * h;
touch_.startDist = std::sqrt(dx * dx + dy * dy);
@@ -446,6 +426,7 @@ void DisplayPanel::handleTouch(const SDL_Event& event, UIState& ui, SDL_Window*
if (nf >= 2) {
SDL_Finger* f0 = SDL_GetTouchFinger(tid, 0);
SDL_Finger* f1 = SDL_GetTouchFinger(tid, 1);
if (!f0 || !f1) return;
float x0 = f0->x * w, x1 = f1->x * w;
float dx = x1 - x0, dy = (f1->y - f0->y) * h;
float dist = std::sqrt(dx * dx + dy * dy);

View File

@@ -42,9 +42,6 @@ public:
float spectrumFrac = 0.35f;
bool draggingSplit = false;
// Returns true if split was just released (caller should save config).
bool splitReleased();
private:
void handleSpectrumInput(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
@@ -60,8 +57,6 @@ private:
float lastDist = 0.0f;
} touch_;
bool splitWasReleased_ = false;
// Scratch buffers
std::vector<std::vector<float>> wfSpectraScratch_;
std::vector<WaterfallChannelInfo> wfChInfoScratch_;

View File

@@ -2,6 +2,7 @@
#include "core/Types.h"
#include <imgui.h>
#include <array>
namespace baudmine {
@@ -16,8 +17,8 @@ struct UIState {
int waterfallChannel = 0;
bool waterfallMultiCh = true;
bool channelEnabled[kMaxChannels] = {true,true,true,true,true,true,true,true};
ImVec4 channelColors[kMaxChannels] = {
std::array<bool, kMaxChannels> channelEnabled = {true,true,true,true,true,true,true,true};
std::array<ImVec4, kMaxChannels> channelColors = {{
{0.20f, 0.90f, 0.30f, 1.0f}, // green
{0.70f, 0.30f, 1.00f, 1.0f}, // purple
{1.00f, 0.55f, 0.00f, 1.0f}, // orange
@@ -26,7 +27,7 @@ struct UIState {
{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
};
}};
};
} // namespace baudmine