diff --git a/src/audio/AudioEngine.cpp b/src/audio/AudioEngine.cpp index f5f79ab..0d98e69 100644 --- a/src/audio/AudioEngine.cpp +++ b/src/audio/AudioEngine.cpp @@ -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(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(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; } diff --git a/src/audio/AudioEngine.h b/src/audio/AudioEngine.h index a70c299..388e991 100644 --- a/src/audio/AudioEngine.h +++ b/src/audio/AudioEngine.h @@ -50,7 +50,6 @@ public: // ── Math channels ── std::vector& mathChannels() { return mathChannels_; } const std::vector& mathChannels() const { return mathChannels_; } - std::vector>& mathSpectra() { return mathSpectra_; } const std::vector>& mathSpectra() const { return mathSpectra_; } void computeMathChannels(); diff --git a/src/core/Types.h b/src/core/Types.h index 2f2374c..f288f62 100644 --- a/src/core/Types.h +++ b/src/core/Types.h @@ -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. diff --git a/src/dsp/WindowFunctions.cpp b/src/dsp/WindowFunctions.cpp index f7cb61f..45b91fc 100644 --- a/src/dsp/WindowFunctions.cpp +++ b/src/dsp/WindowFunctions.cpp @@ -4,8 +4,6 @@ namespace baudmine { -static constexpr double kPi = 3.14159265358979323846; - void WindowFunctions::generate(WindowType type, int size, std::vector& out, float kaiserBeta) { out.resize(size); diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 530aa3a..b9162ac 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -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(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(); diff --git a/src/ui/Application.h b/src/ui/Application.h index b25f683..2936b30 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -66,6 +66,7 @@ private: float lastDpr_ = 0.0f; void applyUIScale(float scale); void requestUIScale(float scale); + float systemDpiScale() const; void syncCanvasSize(); // UI visibility diff --git a/src/ui/ControlPanel.cpp b/src/ui/ControlPanel.cpp index d57442e..6a54d2c 100644 --- a/src/ui/ControlPanel.cpp +++ b/src/ui/ControlPanel.cpp @@ -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)); diff --git a/src/ui/ControlPanel.h b/src/ui/ControlPanel.h index 4d691f9..e9c4848 100644 --- a/src/ui/ControlPanel.h +++ b/src/ui/ControlPanel.h @@ -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}; diff --git a/src/ui/DisplayPanel.cpp b/src/ui/DisplayPanel.cpp index e040731..65a3484 100644 --- a/src/ui/DisplayPanel.cpp +++ b/src/ui/DisplayPanel.cpp @@ -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(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((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(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((freq - freqMin) / (freqMax - freqMin) * (bins - 1)); + double fMin = freqMin(settings.sampleRate, settings.isIQ); + double fMax = freqMax(settings.sampleRate, settings.isIQ); + int bin = static_cast((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); diff --git a/src/ui/DisplayPanel.h b/src/ui/DisplayPanel.h index 0b6943a..092ce0f 100644 --- a/src/ui/DisplayPanel.h +++ b/src/ui/DisplayPanel.h @@ -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> wfSpectraScratch_; std::vector wfChInfoScratch_; diff --git a/src/ui/UIState.h b/src/ui/UIState.h index 2f14eea..df02b5b 100644 --- a/src/ui/UIState.h +++ b/src/ui/UIState.h @@ -2,6 +2,7 @@ #include "core/Types.h" #include +#include 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 channelEnabled = {true,true,true,true,true,true,true,true}; + std::array 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