From 586328a38b42b05d12288c5c44bc24d78664658f Mon Sep 17 00:00:00 2001 From: ericek111 Date: Wed, 25 Mar 2026 19:48:41 +0100 Subject: [PATCH] commit no. 10 --- CMakeLists.txt | 1 + src/ui/Application.cpp | 28 ++++++ src/ui/Application.h | 2 + src/ui/Measurements.cpp | 203 ++++++++++++++++++++++++++++++++++++++++ src/ui/Measurements.h | 58 ++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 src/ui/Measurements.cpp create mode 100644 src/ui/Measurements.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f3761e..3623a15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ set(SOURCES src/ui/WaterfallDisplay.cpp src/ui/SpectrumDisplay.cpp src/ui/Cursors.cpp + src/ui/Measurements.cpp src/ui/Application.cpp ) diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 94e1e47..b7dbedf 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -212,6 +212,8 @@ void Application::processAudio() { int curCh = std::clamp(waterfallChannel_, 0, nSpec - 1); cursors_.update(analyzer_.channelSpectrum(curCh), settings_.sampleRate, settings_.isIQ, settings_.fftSize); + measurements_.update(analyzer_.channelSpectrum(curCh), + settings_.sampleRate, settings_.isIQ, settings_.fftSize); ++spectraThisFrame; } } @@ -640,6 +642,24 @@ void Application::renderControlPanel() { cursors_.drawPanel(); } + // ── Measurements ── + ImGui::Spacing(); + { + bool headerOpen = ImGui::CollapsingHeader("##meas_hdr", + ImGuiTreeNodeFlags_AllowOverlap); + ImGui::SameLine(); + ImGui::Text("Measurements"); + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x + - ImGui::GetFrameHeight()); + ImGui::Checkbox("##meas_en", &measurements_.enabled); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Enable measurements"); + + if (headerOpen) { + measurements_.drawPanel(); + } + } + // ── Status (bottom) ── ImGui::Separator(); ImGui::TextDisabled("Mode: %s", settings_.isIQ ? "I/Q" @@ -706,6 +726,10 @@ void Application::renderSpectrumPanel() { settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_, viewLo_, viewHi_); + measurements_.draw(specDisplay_, specPosX_, specPosY_, specSizeX_, specSizeY_, + settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_, + viewLo_, viewHi_); + handleSpectrumInput(specPosX_, specPosY_, specSizeX_, specSizeY_); ImGui::Dummy({availW, specH}); @@ -843,6 +867,10 @@ void Application::renderWaterfallPanel() { // Store waterfall geometry for cross-panel cursor drawing. wfPosX_ = pos.x; wfPosY_ = pos.y; wfSizeX_ = availW; wfSizeY_ = availH; + measurements_.drawWaterfall(specDisplay_, wfPosX_, wfPosY_, wfSizeX_, wfSizeY_, + settings_.sampleRate, settings_.isIQ, freqScale_, + viewLo_, viewHi_); + // ── Mouse interaction: zoom, pan & hover on waterfall ── ImGuiIO& io = ImGui::GetIO(); float mx = io.MousePos.x; diff --git a/src/ui/Application.h b/src/ui/Application.h index 0ffc4cc..27812dd 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -9,6 +9,7 @@ #include "ui/WaterfallDisplay.h" #include "ui/SpectrumDisplay.h" #include "ui/Cursors.h" +#include "ui/Measurements.h" #include #include @@ -111,6 +112,7 @@ private: WaterfallDisplay waterfall_; SpectrumDisplay specDisplay_; Cursors cursors_; + Measurements measurements_; // Display settings float minDB_ = -120.0f; diff --git a/src/ui/Measurements.cpp b/src/ui/Measurements.cpp new file mode 100644 index 0000000..4896ebc --- /dev/null +++ b/src/ui/Measurements.cpp @@ -0,0 +1,203 @@ +#include "ui/Measurements.h" +#include +#include +#include +#include + +namespace baudline { + +double Measurements::binToFreq(int bin, double sampleRate, bool isIQ, int fftSize) { + // Use bin center (+0.5) for more accurate frequency estimation. + double b = static_cast(bin) + 0.5; + if (isIQ) { + return -sampleRate / 2.0 + (b / fftSize) * sampleRate; + } else { + return (b / fftSize) * sampleRate; + } +} + +void Measurements::findPeaks(const std::vector& spectrumDB, int maxN, + int minDist, float threshold) { + peaks_.clear(); + int bins = static_cast(spectrumDB.size()); + if (bins < 3) return; + + // Mark bins that are local maxima (higher than both neighbors). + // Collect all candidates, then pick top-N with minimum separation. + struct Candidate { int bin; float dB; }; + std::vector candidates; + candidates.reserve(bins / 2); + + for (int i = 1; i < bins - 1; ++i) { + if (spectrumDB[i] >= spectrumDB[i - 1] && + spectrumDB[i] >= spectrumDB[i + 1] && + spectrumDB[i] >= threshold) { + candidates.push_back({i, spectrumDB[i]}); + } + } + // Also check endpoints. + if (bins > 0 && spectrumDB[0] >= spectrumDB[1] && spectrumDB[0] >= threshold) + candidates.push_back({0, spectrumDB[0]}); + if (bins > 1 && spectrumDB[bins - 1] >= spectrumDB[bins - 2] && spectrumDB[bins - 1] >= threshold) + candidates.push_back({bins - 1, spectrumDB[bins - 1]}); + + // Sort by amplitude descending. + std::sort(candidates.begin(), candidates.end(), + [](const Candidate& a, const Candidate& b) { return a.dB > b.dB; }); + + // Greedy selection with minimum distance constraint. + for (const auto& c : candidates) { + if (static_cast(peaks_.size()) >= maxN) break; + bool tooClose = false; + for (const auto& p : peaks_) { + if (std::abs(c.bin - p.bin) < minDist) { + tooClose = true; + break; + } + } + if (!tooClose) { + peaks_.push_back({c.bin, 0.0, c.dB}); // freq filled below + } + } +} + +void Measurements::update(const std::vector& spectrumDB, + double sampleRate, bool isIQ, int fftSize) { + if (!enabled) { peaks_.clear(); return; } + + findPeaks(spectrumDB, maxPeaks, minPeakDist, peakThreshold); + + // Fill in frequencies. + for (auto& p : peaks_) { + p.freq = binToFreq(p.bin, sampleRate, isIQ, fftSize); + } +} + +void Measurements::draw(const SpectrumDisplay& specDisplay, + float posX, float posY, float sizeX, float sizeY, + double sampleRate, bool isIQ, FreqScale freqScale, + float minDB, float maxDB, + float viewLo, float viewHi) const { + if (!enabled || !showOnSpectrum || peaks_.empty()) return; + + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // Colors: primary peak is bright, subsequent peaks fade. + auto peakColor = [](int idx) -> ImU32 { + if (idx == 0) return IM_COL32(255, 80, 80, 220); // red — primary + return IM_COL32(255, 140, 60, 180); // orange — secondary + }; + + for (int i = 0; i < static_cast(peaks_.size()); ++i) { + const auto& p = peaks_[i]; + ImU32 col = peakColor(i); + + float x = specDisplay.freqToScreenX(p.freq, posX, sizeX, + sampleRate, isIQ, freqScale, + viewLo, viewHi); + float dbNorm = (p.dB - minDB) / (maxDB - minDB); + dbNorm = std::clamp(dbNorm, 0.0f, 1.0f); + float y = posY + sizeY * (1.0f - dbNorm); + + // Small downward triangle above the peak. + float triSize = (i == 0) ? 6.0f : 4.0f; + dl->AddTriangleFilled( + {x - triSize, y - triSize * 2}, + {x + triSize, y - triSize * 2}, + {x, y}, + col); + + // Label: "P1: freq dB" — only for the first few to avoid clutter. + if (i < 3) { + char buf[80]; + if (std::abs(p.freq) >= 1e6) + std::snprintf(buf, sizeof(buf), "P%d: %.6f MHz %.1f dB", i + 1, p.freq / 1e6, p.dB); + else if (std::abs(p.freq) >= 1e3) + std::snprintf(buf, sizeof(buf), "P%d: %.3f kHz %.1f dB", i + 1, p.freq / 1e3, p.dB); + else + std::snprintf(buf, sizeof(buf), "P%d: %.1f Hz %.1f dB", i + 1, p.freq, p.dB); + + ImVec2 sz = ImGui::CalcTextSize(buf); + float tx = x - sz.x * 0.5f; + float ty = y - triSize * 2 - sz.y - 2; + + // Clamp to display bounds. + tx = std::clamp(tx, posX + 2, posX + sizeX - sz.x - 2); + ty = std::max(ty, posY + 2); + + dl->AddRectFilled({tx - 2, ty - 1}, {tx + sz.x + 2, ty + sz.y + 1}, + IM_COL32(0, 0, 0, 160)); + dl->AddText({tx, ty}, col, buf); + } + } +} + +void Measurements::drawWaterfall(const SpectrumDisplay& specDisplay, + float posX, float posY, float sizeX, float sizeY, + double sampleRate, bool isIQ, FreqScale freqScale, + float viewLo, float viewHi) const { + if (!enabled || !showOnWaterfall || peaks_.empty()) return; + + ImDrawList* dl = ImGui::GetWindowDrawList(); + + auto peakColor = [](int idx) -> ImU32 { + if (idx == 0) return IM_COL32(255, 80, 80, 120); + return IM_COL32(255, 140, 60, 80); + }; + + for (int i = 0; i < static_cast(peaks_.size()); ++i) { + const auto& p = peaks_[i]; + float x = specDisplay.freqToScreenX(p.freq, posX, sizeX, + sampleRate, isIQ, freqScale, + viewLo, viewHi); + ImU32 col = peakColor(i); + float thickness = (i == 0) ? 1.5f : 1.0f; + dl->AddLine({x, posY}, {x, posY + sizeY}, col, thickness); + } +} + +void Measurements::drawPanel() { + if (!enabled) { + ImGui::TextDisabled("Disabled"); + return; + } + + ImGui::SetNextItemWidth(-1); + ImGui::SliderInt("##maxpeaks", &maxPeaks, 1, 20, "Peaks: %d"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Number of peaks to detect"); + + ImGui::SetNextItemWidth(-1); + ImGui::SliderInt("##mindist", &minPeakDist, 1, 200, "Min dist: %d bins", + ImGuiSliderFlags_Logarithmic); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Minimum distance between peaks (bins)"); + + ImGui::SetNextItemWidth(-1); + ImGui::SliderFloat("##threshold", &peakThreshold, -200.0f, 0.0f, "Thresh: %.0f dB"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Ignore peaks below this level"); + + ImGui::Text("Markers:"); + ImGui::SameLine(); + ImGui::Checkbox("Spectrum##mkr", &showOnSpectrum); + ImGui::SameLine(); + ImGui::Checkbox("Waterfall##mkr", &showOnWaterfall); + + // Peak readout table. + if (!peaks_.empty()) { + ImGui::Separator(); + for (int i = 0; i < static_cast(peaks_.size()); ++i) { + const auto& p = peaks_[i]; + ImU32 col = (i == 0) ? IM_COL32(255, 80, 80, 255) + : IM_COL32(255, 140, 60, 255); + ImGui::PushStyleColor(ImGuiCol_Text, col); + if (std::abs(p.freq) >= 1e6) + ImGui::Text("P%d: %.6f MHz, %.1f dB", i + 1, p.freq / 1e6, p.dB); + else if (std::abs(p.freq) >= 1e3) + ImGui::Text("P%d: %.3f kHz, %.1f dB", i + 1, p.freq / 1e3, p.dB); + else + ImGui::Text("P%d: %.1f Hz, %.1f dB", i + 1, p.freq, p.dB); + ImGui::PopStyleColor(); + } + } +} + +} // namespace baudline diff --git a/src/ui/Measurements.h b/src/ui/Measurements.h new file mode 100644 index 0000000..3228818 --- /dev/null +++ b/src/ui/Measurements.h @@ -0,0 +1,58 @@ +#pragma once + +#include "core/Types.h" +#include "ui/SpectrumDisplay.h" +#include + +namespace baudline { + +struct PeakInfo { + int bin = 0; + double freq = 0.0; // Hz + float dB = -200.0f; +}; + +class Measurements { +public: + // Detect peaks from the spectrum. Call once per frame with fresh data. + void update(const std::vector& spectrumDB, + double sampleRate, bool isIQ, int fftSize); + + // Draw markers on the spectrum display area. + void draw(const SpectrumDisplay& specDisplay, + float posX, float posY, float sizeX, float sizeY, + double sampleRate, bool isIQ, FreqScale freqScale, + float minDB, float maxDB, + float viewLo, float viewHi) const; + + // Draw vertical markers on the waterfall panel. + void drawWaterfall(const SpectrumDisplay& specDisplay, + float posX, float posY, float sizeX, float sizeY, + double sampleRate, bool isIQ, FreqScale freqScale, + float viewLo, float viewHi) const; + + // Draw sidebar panel (ImGui widgets). + void drawPanel(); + + // Current detected peaks (sorted by amplitude, highest first). + const std::vector& peaks() const { return peaks_; } + + // Settings + bool enabled = false; // master enable + int maxPeaks = 5; // how many peaks to detect (1–20) + int minPeakDist = 10; // minimum distance between peaks in bins + float peakThreshold = -120.0f; // ignore peaks below this dB + bool showOnSpectrum = true; // draw markers on spectrum + bool showOnWaterfall = false; // draw vertical lines on waterfall + +private: + std::vector peaks_; + + // Find top-N peaks with minimum bin separation. + void findPeaks(const std::vector& spectrumDB, int maxN, + int minDist, float threshold); + + static double binToFreq(int bin, double sampleRate, bool isIQ, int fftSize); +}; + +} // namespace baudline