split off more stuff

This commit is contained in:
2026-03-28 15:40:16 +01:00
parent b5906d2ab1
commit b42d7fb69b
8 changed files with 1196 additions and 1089 deletions

View File

@@ -53,6 +53,8 @@ set(SOURCES
src/ui/SpectrumDisplay.cpp
src/ui/Cursors.cpp
src/ui/Measurements.cpp
src/ui/ControlPanel.cpp
src/ui/DisplayPanel.cpp
src/ui/Application.cpp
)

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,9 @@
#include "core/Types.h"
#include "core/Config.h"
#include "audio/AudioEngine.h"
#include "ui/UIState.h"
#include "ui/ControlPanel.h"
#include "ui/DisplayPanel.h"
#include "ui/ColorMap.h"
#include "ui/WaterfallDisplay.h"
#include "ui/SpectrumDisplay.h"
@@ -11,7 +14,6 @@
#include <SDL.h>
#include <string>
#include <vector>
namespace baudmine {
@@ -28,16 +30,11 @@ public:
private:
void processAudio();
void render();
void renderControlPanel();
void renderSpectrumPanel();
void renderWaterfallPanel();
void handleSpectrumInput(float posX, float posY, float sizeX, float sizeY);
void openDevice();
void openMultiDevice();
void openFile(const std::string& path, InputFormat format, double sampleRate);
void updateAnalyzerSettings();
void renderMathPanel();
void loadConfig();
void saveConfig() const;
@@ -47,114 +44,42 @@ private:
SDL_GLContext glContext_ = nullptr;
bool running_ = false;
// Audio engine (owns sources, analyzers, math channels)
AudioEngine audio_;
// Core subsystems
AudioEngine audio_;
UIState ui_;
ControlPanel controlPanel_;
DisplayPanel displayPanel_;
// UI state
// Shared UI components
ColorMap colorMap_;
WaterfallDisplay waterfall_;
SpectrumDisplay specDisplay_;
Cursors cursors_;
Measurements measurements_;
// Display settings
float minDB_ = -120.0f;
float maxDB_ = 0.0f;
FreqScale freqScale_ = FreqScale::Linear;
bool paused_ = false;
bool vsync_ = true;
float uiScale_ = 0.0f; // 0 = auto (use DPI), >0 = manual override
float appliedScale_ = 0.0f; // currently applied user-facing scale
float pendingScale_ = 0.0f; // deferred scale (applied before next frame)
float logicalScale_ = 1.0f; // scale after compensating for framebuffer DPI
float lastDpr_ = 0.0f; // last devicePixelRatio (to detect changes)
// UI scaling
bool vsync_ = true;
float uiScale_ = 0.0f;
float appliedScale_ = 0.0f;
float pendingScale_ = 0.0f;
float logicalScale_ = 1.0f;
float lastDpr_ = 0.0f;
void applyUIScale(float scale);
void requestUIScale(float scale); // safe to call mid-frame
void requestUIScale(float scale);
void syncCanvasSize();
// FFT size options
static constexpr int kFFTSizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536};
static constexpr int kNumFFTSizes = 9;
int fftSizeIdx_ = 4; // default 4096
// Overlap (continuous 095%)
float overlapPct_ = 50.0f;
// Window
int windowIdx_ = static_cast<int>(WindowType::BlackmanHarris);
// Color map
int colorMapIdx_ = static_cast<int>(ColorMapType::Magma);
// File playback
std::string filePath_;
int fileFormatIdx_ = 0;
float fileSampleRate_ = 48000.0f;
bool fileLoop_ = true;
// Channel colors (up to kMaxChannels). Defaults: L=green, R=purple.
ImVec4 channelColors_[kMaxChannels] = {
{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
{0.00f, 0.75f, 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
};
int waterfallChannel_ = 0; // which channel drives the waterfall (single mode)
bool waterfallMultiCh_ = true; // true = multi-channel overlay mode
bool channelEnabled_[kMaxChannels] = {true,true,true,true,true,true,true,true};
// Frequency zoom/pan (normalized 01 over full bandwidth)
float viewLo_ = 0.0f; // left edge
float viewHi_ = 0.5f; // right edge (default 2x zoom from left)
// Spectrum/waterfall split ratio (fraction of content height for spectrum)
float spectrumFrac_ = 0.35f;
bool draggingSplit_ = false;
// Panel geometry (stored for cursor interaction)
float specPosX_ = 0, specPosY_ = 0, specSizeX_ = 0, specSizeY_ = 0;
float wfPosX_ = 0, wfPosY_ = 0, wfSizeX_ = 0, wfSizeY_ = 0;
// Hover state: which panel is being hovered
enum class HoverPanel { None, Spectrum, Waterfall };
HoverPanel hoverPanel_ = HoverPanel::None;
float hoverWfTimeOffset_ = 0.0f; // seconds from newest line
// Touch gesture state (pinch-zoom / two-finger pan)
struct TouchState {
int count = 0; // active finger count
float startDist = 0.0f; // initial distance between two fingers
float startLo = 0.0f; // viewLo_ at gesture start
float startHi = 0.0f; // viewHi_ at gesture start
float startCenterX = 0.0f; // midpoint screen-X at gesture start
float lastCenterX = 0.0f; // last midpoint screen-X
float lastDist = 0.0f; // last distance between fingers
} touch_;
void handleTouchEvent(const SDL_Event& event);
// Config persistence
Config config_;
// UI visibility
bool showSidebar_ = true;
bool showSidebar_ = true;
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
// ImGui debug windows
bool showDemoWindow_ = false;
bool showMetricsWindow_ = false;
bool showDebugLog_ = false;
bool showStackTool_ = false;
#endif
// Pre-allocated scratch buffers (avoid per-frame heap allocations)
std::vector<std::vector<float>> wfSpectraScratch_;
std::vector<WaterfallChannelInfo> wfChInfoScratch_;
std::vector<std::vector<float>> allSpectraScratch_;
std::vector<ChannelStyle> stylesScratch_;
// Config persistence
Config config_;
};
} // namespace baudmine

396
src/ui/ControlPanel.cpp Normal file
View File

@@ -0,0 +1,396 @@
#include "ui/ControlPanel.h"
#include "audio/AudioEngine.h"
#include "ui/SpectrumDisplay.h"
#include "ui/Cursors.h"
#include "ui/Measurements.h"
#include "ui/ColorMap.h"
#include "ui/WaterfallDisplay.h"
#include <imgui.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
namespace baudmine {
void ControlPanel::render(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
Measurements& measurements, ColorMap& colorMap,
WaterfallDisplay& waterfall) {
const auto& settings = audio.settings();
// ── Playback ──
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3.0f;
if (ImGui::Button(ui.paused ? "Resume" : "Pause", {btnW, 0}))
ui.paused = !ui.paused;
ImGui::SameLine();
if (ImGui::Button("Clear", {btnW, 0})) {
audio.clearHistory();
}
ImGui::SameLine();
if (ImGui::Button("Peak", {btnW, 0})) {
int pkCh = std::clamp(ui.waterfallChannel, 0, audio.totalNumSpectra() - 1);
cursors.snapToPeak(audio.getSpectrum(pkCh),
settings.sampleRate, settings.isIQ,
settings.fftSize);
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Snap cursor A to peak");
// ── FFT ──
ImGui::Spacing();
if (ImGui::CollapsingHeader("FFT", ImGuiTreeNodeFlags_DefaultOpen)) {
const char* sizeNames[] = {"256", "512", "1024", "2048", "4096",
"8192", "16384", "32768", "65536"};
float availSpace = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x);
ImGui::SetNextItemWidth(availSpace * 0.35f);
if (ImGui::Combo("##fftsize", &fftSizeIdx, sizeNames, kNumFFTSizes)) {
audio.settings().fftSize = kFFTSizes[fftSizeIdx];
flagUpdate();
flagSave();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("FFT Size");
ImGui::SameLine();
const char* winNames[] = {"Rectangular", "Hann", "Hamming", "Blackman",
"Blackman-Harris", "Kaiser", "Flat Top"};
ImGui::SetNextItemWidth(availSpace * 0.65f);
if (ImGui::Combo("##window", &windowIdx, winNames,
static_cast<int>(WindowType::Count))) {
audio.settings().window = static_cast<WindowType>(windowIdx);
flagUpdate();
flagSave();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Window Function");
if (settings.window == WindowType::Kaiser) {
ImGui::SetNextItemWidth(-1);
if (ImGui::SliderFloat("##kaiser", &audio.settings().kaiserBeta, 0.0f, 20.0f, "Kaiser: %.1f"))
flagUpdate();
}
// Overlap
{
int hopSamples = static_cast<int>(settings.fftSize * (1.0f - settings.overlap));
if (hopSamples < 1) hopSamples = 1;
int overlapSamples = settings.fftSize - hopSamples;
ImGui::SetNextItemWidth(-1);
float sliderVal = 1.0f - std::pow(1.0f - overlapPct / 99.0f, 0.25f);
if (ImGui::SliderFloat("##overlap", &sliderVal, 0.0f, 1.0f, "")) {
float inv = 1.0f - sliderVal;
float inv2 = inv * inv;
overlapPct = 99.0f * (1.0f - inv2 * inv2);
audio.settings().overlap = overlapPct / 100.0f;
flagUpdate();
flagSave();
}
char overlayText[64];
std::snprintf(overlayText, sizeof(overlayText), "%.1f%% (%d samples)", overlapPct, overlapSamples);
ImVec2 textSize = ImGui::CalcTextSize(overlayText);
ImVec2 rMin = ImGui::GetItemRectMin();
ImVec2 rMax = ImGui::GetItemRectMax();
float tx = rMin.x + ((rMax.x - rMin.x) - textSize.x) * 0.5f;
float ty = rMin.y + ((rMax.y - rMin.y) - textSize.y) * 0.5f;
ImGui::GetWindowDrawList()->AddText({tx, ty}, IM_COL32(255, 255, 255, 220), overlayText);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Overlap");
}
}
// ── Display ──
ImGui::Spacing();
if (ImGui::CollapsingHeader("Display", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::SetNextItemWidth(-1);
ImGui::DragFloatRange2("##dbrange", &ui.minDB, &ui.maxDB, 1.0f, -200.0f, 20.0f,
"Min: %.0f dB", "Max: %.0f dB");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("dB Range (min / max)");
ImGui::Checkbox("Peak Hold", &specDisplay.peakHoldEnable);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Draws a \"maximum\" line in the spectrogram");
if (specDisplay.peakHoldEnable) {
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x
- ImGui::CalcTextSize("Clear").x
- ImGui::GetStyle().ItemSpacing.x
- ImGui::GetStyle().FramePadding.x * 2);
ImGui::SliderFloat("##decay", &specDisplay.peakHoldDecay, 0.0f, 120.0f, "%.0f dB/s");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Decay rate");
ImGui::SameLine();
if (ImGui::SmallButton("Clear##peakhold"))
specDisplay.clearPeakHold();
}
{
bool isLog = (ui.freqScale == FreqScale::Logarithmic);
bool canLog = !settings.isIQ;
ImGui::AlignTextToFramePadding();
ImGui::Text("Freq. scale:");
ImGui::SameLine();
if (ImGui::Button(isLog ? "Logarithmic" : "Linear", {ImGui::GetContentRegionAvail().x, 0})) {
if (canLog) {
constexpr float kMinBF = 0.001f;
float logMin = std::log10(kMinBF);
auto screenToBin = [&](float sf) -> float {
if (isLog) return std::pow(10.0f, logMin + sf * (0.0f - logMin));
return sf;
};
auto binToScreen = [&](float bf, bool toLog) -> float {
if (toLog) {
if (bf < kMinBF) bf = kMinBF;
return (std::log10(bf) - logMin) / (0.0f - logMin);
}
return bf;
};
float bfLo = screenToBin(ui.viewLo);
float bfHi = screenToBin(ui.viewHi);
bool newLog = !isLog;
ui.freqScale = newLog ? FreqScale::Logarithmic : FreqScale::Linear;
ui.viewLo = std::clamp(binToScreen(bfLo, newLog), 0.0f, 1.0f);
ui.viewHi = std::clamp(binToScreen(bfHi, newLog), 0.0f, 1.0f);
if (ui.viewHi <= ui.viewLo) { ui.viewLo = 0.0f; ui.viewHi = 1.0f; }
flagSave();
}
}
if (!canLog && ImGui::IsItemHovered())
ImGui::SetTooltip("Log scale not available in I/Q mode");
}
{
float span = ui.viewHi - ui.viewLo;
float zoomX = 1.0f / span;
float resetBtnW = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
float zoomLabelW = ImGui::CalcTextSize("Zoom:").x + ImGui::GetStyle().ItemSpacing.x;
float sliderW = ImGui::GetContentRegionAvail().x - zoomLabelW - resetBtnW - ImGui::GetStyle().ItemSpacing.x;
ImGui::AlignTextToFramePadding();
ImGui::Text("Zoom:");
ImGui::SameLine();
ImGui::SetNextItemWidth(sliderW);
if (ImGui::SliderFloat("##zoom", &zoomX, 1.0f, 200.0f, "%.1fx", ImGuiSliderFlags_Logarithmic)) {
zoomX = std::clamp(zoomX, 1.0f, 1000.0f);
float newSpan = 1.0f / zoomX;
ui.viewLo = 0.0f;
ui.viewHi = std::clamp(newSpan, 0.0f, 1.0f);
}
ImGui::SameLine();
if (ImGui::SmallButton("Reset##zoom")) {
ui.viewLo = 0.0f;
ui.viewHi = 0.5f;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reset to 2x zoom");
}
}
// ── Channels ──
ImGui::Spacing();
{
int nCh = audio.totalNumSpectra();
bool isMulti = ui.waterfallMultiCh && nCh > 1;
float widgetW = (nCh > 1) ? ImGui::CalcTextSize(" Multi ").x + ImGui::GetStyle().FramePadding.x * 2 : 0.0f;
float gap = ImGui::GetStyle().ItemSpacing.x * 0.25f;
ImVec2 hdrMin = ImGui::GetCursorScreenPos();
float winLeft = ImGui::GetWindowPos().x;
float hdrRight = hdrMin.x + ImGui::GetContentRegionAvail().x;
ImGui::PushClipRect({winLeft, hdrMin.y}, {hdrRight - widgetW - gap, hdrMin.y + 200}, true);
bool headerOpen = ImGui::CollapsingHeader("##channels_hdr",
ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
ImGui::PopClipRect();
ImGui::SameLine();
ImGui::Text("Channels");
if (nCh > 1) {
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - widgetW + ImGui::GetStyle().FramePadding.x);
if (ImGui::Button(isMulti ? " Multi " : "Single ", {widgetW, 0})) {
ui.waterfallMultiCh = !ui.waterfallMultiCh;
}
}
if (headerOpen) {
if (isMulti) {
static const char* defaultNames[] = {
"Left", "Right", "Ch 3", "Ch 4", "Ch 5", "Ch 6", "Ch 7", "Ch 8"
};
for (int ch = 0; ch < nCh && ch < kMaxChannels; ++ch) {
ImGui::PushID(ch);
ImGui::Checkbox("##en", &ui.channelEnabled[ch]);
ImGui::SameLine();
ImGui::ColorEdit3(defaultNames[ch], &ui.channelColors[ch].x,
ImGuiColorEditFlags_NoInputs);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", audio.getDeviceName(ch));
ImGui::PopID();
}
} else {
const char* cmNames[] = {"Magma", "Viridis", "Inferno", "Plasma", "Grayscale"};
ImGui::SetNextItemWidth(-1);
if (ImGui::Combo("##colormap", &colorMapIdx, cmNames,
static_cast<int>(ColorMapType::Count))) {
colorMap.setType(static_cast<ColorMapType>(colorMapIdx));
waterfall.setColorMap(colorMap);
flagSave();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Color Map");
if (nCh > 1) {
ImGui::SetNextItemWidth(-1);
if (ImGui::SliderInt("##wfch", &ui.waterfallChannel, 0, nCh - 1))
ui.waterfallChannel = std::clamp(ui.waterfallChannel, 0, nCh - 1);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Waterfall Channel");
}
}
}
}
// ── Math ──
ImGui::Spacing();
{
float btnW2 = ImGui::GetFrameHeight();
float gap = ImGui::GetStyle().ItemSpacing.x * 0.25f;
ImVec2 hdrMin = ImGui::GetCursorScreenPos();
float winLeft = ImGui::GetWindowPos().x;
float hdrRight = hdrMin.x + ImGui::GetContentRegionAvail().x;
ImGui::PushClipRect({winLeft, hdrMin.y}, {hdrRight - btnW2 - gap, hdrMin.y + 200}, true);
bool mathOpen = ImGui::CollapsingHeader("##math_hdr",
ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
ImGui::PopClipRect();
ImGui::SameLine();
ImGui::Text("Math");
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - btnW2 + ImGui::GetStyle().FramePadding.x);
if (ImGui::Button("+##addmath", {btnW2, 0})) {
int nPhys = audio.totalNumSpectra();
MathChannel mc;
mc.op = MathOp::Subtract;
mc.sourceX = 0;
mc.sourceY = std::min(1, nPhys - 1);
mc.color[0] = 1.0f; mc.color[1] = 1.0f; mc.color[2] = 0.5f; mc.color[3] = 1.0f;
audio.mathChannels().push_back(mc);
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add math channel");
if (mathOpen) {
renderMathPanel(audio);
}
}
// ── Cursors ──
ImGui::Spacing();
{
float btnW2 = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
float gap = ImGui::GetStyle().ItemSpacing.x * 0.25f;
ImVec2 hdrMin = ImGui::GetCursorScreenPos();
float winLeft = ImGui::GetWindowPos().x;
float hdrRight = hdrMin.x + ImGui::GetContentRegionAvail().x;
ImGui::PushClipRect({winLeft, hdrMin.y}, {hdrRight - btnW2 - gap, hdrMin.y + 200}, true);
bool cursorsOpen = ImGui::CollapsingHeader("##cursors_hdr",
ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
ImGui::PopClipRect();
ImGui::SameLine();
ImGui::Text("Cursors");
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - btnW2 + ImGui::GetStyle().FramePadding.x);
if (ImGui::SmallButton("Reset##cursors")) {
cursors.cursorA.active = false;
cursors.cursorB.active = false;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Clear cursors A and B");
if (cursorsOpen) {
bool prevSnap = cursors.snapToPeaks;
cursors.drawPanel();
if (cursors.snapToPeaks != prevSnap) flagSave();
}
}
// ── Measurements ──
ImGui::Spacing();
{
float cbW = ImGui::GetFrameHeight();
float gap = ImGui::GetStyle().ItemSpacing.x * 0.25f;
ImVec2 hdrMin = ImGui::GetCursorScreenPos();
float winLeft = ImGui::GetWindowPos().x;
float hdrRight = hdrMin.x + ImGui::GetContentRegionAvail().x;
ImGui::PushClipRect({winLeft, hdrMin.y}, {hdrRight - cbW - gap, hdrMin.y + 200}, true);
bool headerOpen = ImGui::CollapsingHeader("##meas_hdr",
ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
ImGui::PopClipRect();
ImGui::SameLine();
ImGui::Text("Measurements");
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - cbW + ImGui::GetStyle().FramePadding.x);
ImGui::Checkbox("##meas_en", &measurements.enabled);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Enable measurements");
if (headerOpen) {
float prevMin = measurements.traceMinFreq;
float prevMax = measurements.traceMaxFreq;
measurements.drawPanel();
if (measurements.traceMinFreq != prevMin || measurements.traceMaxFreq != prevMax)
flagSave();
}
}
// ── Status (bottom) ──
ImGui::Separator();
ImGui::TextDisabled("Mode: %s", settings.isIQ ? "I/Q"
: (settings.numChannels > 1 ? "Multi-ch" : "Real"));
}
void ControlPanel::renderMathPanel(AudioEngine& audio) {
int nPhys = audio.totalNumSpectra();
auto& mathChannels = audio.mathChannels();
static const char* chNames[] = {
"Ch 0 (L)", "Ch 1 (R)", "Ch 2", "Ch 3", "Ch 4", "Ch 5", "Ch 6", "Ch 7"
};
int toRemove = -1;
for (int mi = 0; mi < static_cast<int>(mathChannels.size()); ++mi) {
auto& mc = mathChannels[mi];
ImGui::PushID(1000 + mi);
ImGui::Checkbox("##en", &mc.enabled);
ImGui::SameLine();
ImGui::ColorEdit3("##col", mc.color, ImGuiColorEditFlags_NoInputs);
ImGui::SameLine();
if (ImGui::BeginCombo("##op", mathOpName(mc.op), ImGuiComboFlags_NoPreview)) {
for (int o = 0; o < static_cast<int>(MathOp::Count); ++o) {
auto op = static_cast<MathOp>(o);
if (ImGui::Selectable(mathOpName(op), mc.op == op))
mc.op = op;
}
ImGui::EndCombo();
}
ImGui::SameLine();
ImGui::Text("%s", mathOpName(mc.op));
ImGui::SetNextItemWidth(80);
ImGui::Combo("X", &mc.sourceX, chNames, std::min(nPhys, kMaxChannels));
if (mathOpIsBinary(mc.op)) {
ImGui::SameLine();
ImGui::SetNextItemWidth(80);
ImGui::Combo("Y", &mc.sourceY, chNames, std::min(nPhys, kMaxChannels));
}
ImGui::SameLine();
ImGui::Checkbox("WF", &mc.waterfall);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Show on waterfall");
ImGui::SameLine();
if (ImGui::SmallButton("X##del"))
toRemove = mi;
ImGui::PopID();
}
if (toRemove >= 0)
mathChannels.erase(mathChannels.begin() + toRemove);
}
} // namespace baudmine

53
src/ui/ControlPanel.h Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include "core/Types.h"
#include "ui/UIState.h"
#include <string>
namespace baudmine {
class AudioEngine;
class SpectrumDisplay;
class Cursors;
class Measurements;
class ColorMap;
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; }
// FFT / analysis controls
static constexpr int kFFTSizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536};
static constexpr int kNumFFTSizes = 9;
int fftSizeIdx = 4;
float overlapPct = 50.0f;
int windowIdx = static_cast<int>(WindowType::BlackmanHarris);
int colorMapIdx = static_cast<int>(ColorMapType::Magma);
// File playback
std::string filePath;
int fileFormatIdx = 0;
float fileSampleRate = 48000.0f;
bool fileLoop = true;
private:
void renderMathPanel(AudioEngine& audio);
bool needsSave_ = false;
bool needsUpdate_ = false;
void flagSave() { needsSave_ = true; }
void flagUpdate() { needsUpdate_ = true; }
};
} // namespace baudmine

478
src/ui/DisplayPanel.cpp Normal file
View File

@@ -0,0 +1,478 @@
#include "ui/DisplayPanel.h"
#include "audio/AudioEngine.h"
#include <imgui.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
namespace baudmine {
bool DisplayPanel::splitReleased() {
bool v = splitWasReleased_;
splitWasReleased_ = false;
return v;
}
void DisplayPanel::renderSpectrum(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
Measurements& measurements) {
const auto& settings = audio.settings();
float availW = ImGui::GetContentRegionAvail().x;
float specH = ImGui::GetContentRegionAvail().y;
ImVec2 pos = ImGui::GetCursorScreenPos();
specPosX = pos.x;
specPosY = pos.y;
specSizeX = availW;
specSizeY = specH;
int nPhys = audio.totalNumSpectra();
const auto& mathChannels = audio.mathChannels();
const auto& mathSpectra = audio.mathSpectra();
int nMath = static_cast<int>(mathSpectra.size());
allSpectraScratch_.clear();
stylesScratch_.clear();
for (int ch = 0; ch < nPhys; ++ch) {
if (!ui.channelEnabled[ch % kMaxChannels]) continue;
allSpectraScratch_.push_back(audio.getSpectrum(ch));
const auto& c = ui.channelColors[ch % kMaxChannels];
uint8_t r = static_cast<uint8_t>(c.x * 255);
uint8_t g = static_cast<uint8_t>(c.y * 255);
uint8_t b = static_cast<uint8_t>(c.z * 255);
stylesScratch_.push_back({IM_COL32(r, g, b, 220), IM_COL32(r, g, b, 35)});
}
for (int mi = 0; mi < nMath; ++mi) {
if (mi < static_cast<int>(mathChannels.size()) && mathChannels[mi].enabled) {
allSpectraScratch_.push_back(mathSpectra[mi]);
const auto& c = mathChannels[mi].color;
uint8_t r = static_cast<uint8_t>(c[0] * 255);
uint8_t g = static_cast<uint8_t>(c[1] * 255);
uint8_t b = static_cast<uint8_t>(c[2] * 255);
stylesScratch_.push_back({IM_COL32(r, g, b, 220), IM_COL32(r, g, b, 35)});
}
}
specDisplay.updatePeakHold(allSpectraScratch_);
specDisplay.draw(allSpectraScratch_, stylesScratch_, ui.minDB, ui.maxDB,
settings.sampleRate, settings.isIQ, ui.freqScale,
specPosX, specPosY, specSizeX, specSizeY,
ui.viewLo, ui.viewHi);
cursors.draw(specDisplay, specPosX, specPosY, specSizeX, specSizeY,
settings.sampleRate, settings.isIQ, ui.freqScale, ui.minDB, ui.maxDB,
ui.viewLo, ui.viewHi);
measurements.draw(specDisplay, specPosX, specPosY, specSizeX, specSizeY,
settings.sampleRate, settings.isIQ, ui.freqScale, ui.minDB, ui.maxDB,
ui.viewLo, ui.viewHi);
handleSpectrumInput(audio, ui, specDisplay, cursors,
specPosX, specPosY, specSizeX, specSizeY);
ImGui::Dummy({availW, specH});
}
void DisplayPanel::renderWaterfall(AudioEngine& audio, UIState& ui,
WaterfallDisplay& waterfall, SpectrumDisplay& specDisplay,
Cursors& cursors, Measurements& measurements,
ColorMap& colorMap) {
const auto& settings = audio.settings();
float availW = ImGui::GetContentRegionAvail().x;
constexpr float kSplitterH = 6.0f;
float parentH = ImGui::GetContentRegionAvail().y;
float availH = (parentH - kSplitterH) * (1.0f - spectrumFrac);
int neededH = std::max(1024, static_cast<int>(availH) + 1);
int binCount = std::max(1, audio.spectrumSize());
if (binCount != waterfall.width() || waterfall.height() < neededH) {
waterfall.resize(binCount, neededH);
waterfall.setColorMap(colorMap);
}
if (waterfall.textureID()) {
ImVec2 pos = ImGui::GetCursorScreenPos();
ImDrawList* dl = ImGui::GetWindowDrawList();
auto texID = static_cast<ImTextureID>(waterfall.textureID());
int h = waterfall.height();
int screenRows = std::min(static_cast<int>(availH), h);
int newestRow = (waterfall.currentRow() + 1) % h;
float rowToV = 1.0f / h;
bool logMode = (ui.freqScale == FreqScale::Logarithmic && !settings.isIQ);
auto drawSpan = [&](int rowStart, int rowCount, float yStart, float spanH) {
float v0 = rowStart * rowToV;
float v1 = (rowStart + rowCount) * rowToV;
if (!logMode) {
dl->AddImage(texID,
{pos.x, yStart},
{pos.x + availW, yStart + spanH},
{ui.viewLo, v1}, {ui.viewHi, v0});
} else {
constexpr float kMinBinFrac = 0.001f;
float logMin2 = std::log10(kMinBinFrac);
float logMax2 = 0.0f;
int numStrips = std::min(512, static_cast<int>(availW));
for (int s = 0; s < numStrips; ++s) {
float sL = static_cast<float>(s) / numStrips;
float sR = static_cast<float>(s + 1) / numStrips;
float vfL = ui.viewLo + sL * (ui.viewHi - ui.viewLo);
float vfR = ui.viewLo + sR * (ui.viewHi - ui.viewLo);
float uL = std::pow(10.0f, logMin2 + vfL * (logMax2 - logMin2));
float uR = std::pow(10.0f, logMin2 + vfR * (logMax2 - logMin2));
dl->AddImage(texID,
{pos.x + sL * availW, yStart},
{pos.x + sR * availW, yStart + spanH},
{uL, v1}, {uR, v0});
}
}
};
float pxPerRow = availH / static_cast<float>(screenRows);
if (newestRow + screenRows <= h) {
drawSpan(newestRow, screenRows, pos.y, availH);
} else {
int firstCount = h - newestRow;
int secondCount = screenRows - firstCount;
float secondH = secondCount * pxPerRow;
if (secondCount > 0)
drawSpan(0, secondCount, pos.y, secondH);
float firstH = availH - secondH;
drawSpan(newestRow, firstCount, pos.y + secondH, firstH);
}
// ── 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;
auto viewFracToFreq = [&](float vf) -> double {
if (logMode) {
constexpr float kMinBinFrac = 0.001f;
float logMin2 = std::log10(kMinBinFrac);
float logMax2 = 0.0f;
float binFrac = std::pow(10.0f, logMin2 + vf * (logMax2 - logMin2));
return freqFullMin + binFrac * (freqFullMax - freqFullMin);
}
return freqFullMin + vf * (freqFullMax - freqFullMin);
};
int numLabels = 8;
for (int i = 0; i <= numLabels; ++i) {
float frac = static_cast<float>(i) / numLabels;
float vf = ui.viewLo + frac * (ui.viewHi - ui.viewLo);
double freq = viewFracToFreq(vf);
float x = pos.x + frac * availW;
char label[32];
if (std::abs(freq) >= 1e6)
std::snprintf(label, sizeof(label), "%.2fM", freq / 1e6);
else if (std::abs(freq) >= 1e3)
std::snprintf(label, sizeof(label), "%.1fk", freq / 1e3);
else
std::snprintf(label, sizeof(label), "%.0f", freq);
dl->AddText({x + 2, pos.y + 2}, textCol, label);
}
wfPosX = pos.x; wfPosY = pos.y; wfSizeX = availW; wfSizeY = availH;
measurements.drawWaterfall(specDisplay, wfPosX, wfPosY, wfSizeX, wfSizeY,
settings.sampleRate, settings.isIQ, ui.freqScale,
ui.viewLo, ui.viewHi, screenRows, audio.spectrumSize());
// ── Mouse interaction: zoom, pan & hover on waterfall ──
ImGuiIO& io = ImGui::GetIO();
float mx = io.MousePos.x;
float my = io.MousePos.y;
bool inWaterfall = mx >= pos.x && mx <= pos.x + availW &&
my >= pos.y && my <= pos.y + availH;
if (inWaterfall) {
hoverPanel = HoverPanel::Waterfall;
double freq = specDisplay.screenXToFreq(mx, pos.x, availW,
settings.sampleRate,
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;
int bin = static_cast<int>((freq - fMin) / (fMax - fMin) * (bins - 1));
bin = std::clamp(bin, 0, bins - 1);
float yFrac = 1.0f - (my - pos.y) / availH;
int hopSamples = static_cast<int>(settings.fftSize * (1.0f - settings.overlap));
if (hopSamples < 1) hopSamples = 1;
double secondsPerLine = static_cast<double>(hopSamples) / settings.sampleRate;
hoverWfTimeOff = static_cast<float>(yFrac * screenRows * secondsPerLine);
int curCh = std::clamp(ui.waterfallChannel, 0, audio.totalNumSpectra() - 1);
const auto& spec = audio.getSpectrum(curCh);
if (!spec.empty()) {
cursors.hover = {true, freq, spec[bin], bin};
}
}
if (inWaterfall) {
if (io.MouseWheel != 0) {
float cursorFrac = (mx - pos.x) / availW;
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; }
// 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::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) {
ui.viewLo = 0.0f;
ui.viewHi = 1.0f;
}
}
}
ImGui::Dummy({availW, availH});
}
void DisplayPanel::renderHoverOverlay(const AudioEngine& audio, const UIState& ui,
const Cursors& cursors, const SpectrumDisplay& specDisplay) {
if (!cursors.hover.active || specSizeX <= 0 || wfSizeX <= 0)
return;
const auto& settings = audio.settings();
ImDrawList* dl = ImGui::GetWindowDrawList();
float hx = specDisplay.freqToScreenX(cursors.hover.freq,
specPosX, specSizeX, settings.sampleRate,
settings.isIQ, ui.freqScale, ui.viewLo, ui.viewHi);
ImU32 hoverCol = IM_COL32(200, 200, 200, 80);
dl->AddLine({hx, specPosY}, {hx, specPosY + specSizeY}, hoverCol, 1.0f);
char freqLabel[48];
fmtFreq(freqLabel, sizeof(freqLabel), cursors.hover.freq);
ImVec2 tSz = ImGui::CalcTextSize(freqLabel);
float lx = std::min(hx + 4, wfPosX + wfSizeX - tSz.x - 4);
float ly = wfPosY + 2;
dl->AddRectFilled({lx - 2, ly - 1}, {lx + tSz.x + 2, ly + tSz.y + 1},
IM_COL32(0, 0, 0, 180));
dl->AddText({lx, ly}, IM_COL32(220, 220, 240, 240), freqLabel);
// 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 binCenterFreq = fMin + (static_cast<double>(cursors.hover.bin) + 0.5)
/ bins * (fMax - fMin);
char hoverBuf[128];
if (hoverPanel == HoverPanel::Spectrum) {
fmtFreqDB(hoverBuf, sizeof(hoverBuf), "", binCenterFreq, cursors.hover.dB);
} else if (hoverPanel == HoverPanel::Waterfall) {
fmtFreqTime(hoverBuf, sizeof(hoverBuf), "", binCenterFreq, -hoverWfTimeOff);
} else {
fmtFreq(hoverBuf, sizeof(hoverBuf), binCenterFreq);
}
ImU32 hoverTextCol = IM_COL32(100, 230, 130, 240);
float rightEdge = specPosX + specSizeX - 8;
float hy2 = specPosY + 4;
ImVec2 hSz = ImGui::CalcTextSize(hoverBuf);
dl->AddText({rightEdge - hSz.x, hy2}, hoverTextCol, hoverBuf);
}
}
void DisplayPanel::handleSpectrumInput(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
float posX, float posY, float sizeX, float sizeY) {
const auto& settings = audio.settings();
ImGuiIO& io = ImGui::GetIO();
float mx = io.MousePos.x;
float my = io.MousePos.y;
bool inRegion = mx >= posX && mx <= posX + sizeX &&
my >= posY && my <= posY + sizeY;
if (inRegion) {
hoverPanel = HoverPanel::Spectrum;
double freq = specDisplay.screenXToFreq(mx, posX, sizeX,
settings.sampleRate,
settings.isIQ, ui.freqScale,
ui.viewLo, ui.viewHi);
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));
bin = std::clamp(bin, 0, bins - 1);
int curCh = std::clamp(ui.waterfallChannel, 0, audio.totalNumSpectra() - 1);
const auto& spec = audio.getSpectrum(curCh);
if (!spec.empty()) {
dB = spec[bin];
cursors.hover = {true, freq, dB, bin};
}
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
int cBin = cursors.snapToPeaks ? cursors.findLocalPeak(spec, bin, 10) : bin;
double cFreq = audio.binToFreq(cBin);
cursors.setCursorA(cFreq, spec[cBin], cBin);
}
if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
int cBin = cursors.snapToPeaks ? cursors.findLocalPeak(spec, bin, 10) : bin;
double cFreq = audio.binToFreq(cBin);
cursors.setCursorB(cFreq, spec[cBin], cBin);
}
{
if (io.MouseWheel != 0 && (io.KeyCtrl || io.KeyShift)) {
float zoom = io.MouseWheel * 5.0f;
ui.minDB += zoom;
ui.maxDB -= zoom;
if (ui.maxDB - ui.minDB < 10.0f) {
float mid = (ui.minDB + ui.maxDB) / 2.0f;
ui.minDB = mid - 5.0f;
ui.maxDB = mid + 5.0f;
}
}
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);
}
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::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) {
ui.viewLo = 0.0f;
ui.viewHi = 1.0f;
}
}
} else {
if (hoverPanel != HoverPanel::Waterfall) {
hoverPanel = HoverPanel::None;
cursors.hover.active = false;
}
}
}
void DisplayPanel::handleTouch(const SDL_Event& event, UIState& ui, SDL_Window* window) {
if (event.type == SDL_FINGERDOWN) {
++touch_.count;
} else if (event.type == SDL_FINGERUP) {
touch_.count = std::max(0, touch_.count - 1);
}
if (touch_.count == 2 && event.type == SDL_FINGERDOWN) {
int w, h;
SDL_GetWindowSize(window, &w, &h);
SDL_TouchID tid = event.tfinger.touchId;
int nf = SDL_GetNumTouchFingers(tid);
if (nf >= 2) {
SDL_Finger* f0 = SDL_GetTouchFinger(tid, 0);
SDL_Finger* f1 = SDL_GetTouchFinger(tid, 1);
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);
touch_.lastDist = touch_.startDist;
touch_.startCenterX = (x0 + x1) * 0.5f;
touch_.lastCenterX = touch_.startCenterX;
touch_.startLo = ui.viewLo;
touch_.startHi = ui.viewHi;
}
}
if (touch_.count == 2 && event.type == SDL_FINGERMOTION) {
int w, h;
SDL_GetWindowSize(window, &w, &h);
SDL_TouchID tid = event.tfinger.touchId;
int nf = SDL_GetNumTouchFingers(tid);
if (nf >= 2) {
SDL_Finger* f0 = SDL_GetTouchFinger(tid, 0);
SDL_Finger* f1 = SDL_GetTouchFinger(tid, 1);
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);
float centerX = (x0 + x1) * 0.5f;
if (touch_.startDist > 1.0f) {
float span0 = touch_.startHi - touch_.startLo;
float ratio = touch_.startDist / std::max(dist, 1.0f);
float newSpan = std::clamp(span0 * ratio, 0.001f, 1.0f);
float panelW = wfSizeX > 0 ? wfSizeX : static_cast<float>(w);
float panelX = wfPosX;
float midFrac = (touch_.startCenterX - panelX) / panelW;
float midView = touch_.startLo + midFrac * span0;
float panDelta = -(centerX - touch_.startCenterX) / panelW * newSpan;
float newLo = midView - midFrac * newSpan + panDelta;
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);
}
}
}
}
} // namespace baudmine

72
src/ui/DisplayPanel.h Normal file
View File

@@ -0,0 +1,72 @@
#pragma once
#include "core/Types.h"
#include "ui/UIState.h"
#include "ui/WaterfallDisplay.h"
#include "ui/SpectrumDisplay.h"
#include "ui/Cursors.h"
#include "ui/Measurements.h"
#include "ui/ColorMap.h"
#include <SDL.h>
#include <vector>
namespace baudmine {
class AudioEngine;
class DisplayPanel {
public:
void renderSpectrum(AudioEngine& audio, UIState& ui,
SpectrumDisplay& specDisplay, Cursors& cursors,
Measurements& measurements);
void renderWaterfall(AudioEngine& audio, UIState& ui,
WaterfallDisplay& waterfall, SpectrumDisplay& specDisplay,
Cursors& cursors, Measurements& measurements,
ColorMap& colorMap);
void renderHoverOverlay(const AudioEngine& audio, const UIState& ui,
const Cursors& cursors, const SpectrumDisplay& specDisplay);
void handleTouch(const SDL_Event& event, UIState& ui, SDL_Window* window);
// Panel geometry (read by Application for layout)
float specPosX = 0, specPosY = 0, specSizeX = 0, specSizeY = 0;
float wfPosX = 0, wfPosY = 0, wfSizeX = 0, wfSizeY = 0;
enum class HoverPanel { None, Spectrum, Waterfall };
HoverPanel hoverPanel = HoverPanel::None;
float hoverWfTimeOff = 0.0f;
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,
float posX, float posY, float sizeX, float sizeY);
struct TouchState {
int count = 0;
float startDist = 0.0f;
float startLo = 0.0f;
float startHi = 0.0f;
float startCenterX = 0.0f;
float lastCenterX = 0.0f;
float lastDist = 0.0f;
} touch_;
bool splitWasReleased_ = false;
// Scratch buffers
std::vector<std::vector<float>> wfSpectraScratch_;
std::vector<WaterfallChannelInfo> wfChInfoScratch_;
std::vector<std::vector<float>> allSpectraScratch_;
std::vector<ChannelStyle> stylesScratch_;
};
} // namespace baudmine

32
src/ui/UIState.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include "core/Types.h"
#include <imgui.h>
namespace baudmine {
// Shared display state accessed by both ControlPanel and DisplayPanel.
struct UIState {
float minDB = -120.0f;
float maxDB = 0.0f;
FreqScale freqScale = FreqScale::Linear;
float viewLo = 0.0f;
float viewHi = 0.5f;
bool paused = false;
int waterfallChannel = 0;
bool waterfallMultiCh = true;
bool channelEnabled[kMaxChannels] = {true,true,true,true,true,true,true,true};
ImVec4 channelColors[kMaxChannels] = {
{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
{0.00f, 0.75f, 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
};
};
} // namespace baudmine