commit no. 10

This commit is contained in:
2026-03-25 19:48:41 +01:00
parent 1670e4eea6
commit 586328a38b
5 changed files with 292 additions and 0 deletions

View File

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

View File

@@ -9,6 +9,7 @@
#include "ui/WaterfallDisplay.h"
#include "ui/SpectrumDisplay.h"
#include "ui/Cursors.h"
#include "ui/Measurements.h"
#include <SDL.h>
#include <complex>
@@ -111,6 +112,7 @@ private:
WaterfallDisplay waterfall_;
SpectrumDisplay specDisplay_;
Cursors cursors_;
Measurements measurements_;
// Display settings
float minDB_ = -120.0f;

203
src/ui/Measurements.cpp Normal file
View File

@@ -0,0 +1,203 @@
#include "ui/Measurements.h"
#include <imgui.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
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<double>(bin) + 0.5;
if (isIQ) {
return -sampleRate / 2.0 + (b / fftSize) * sampleRate;
} else {
return (b / fftSize) * sampleRate;
}
}
void Measurements::findPeaks(const std::vector<float>& spectrumDB, int maxN,
int minDist, float threshold) {
peaks_.clear();
int bins = static_cast<int>(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<Candidate> 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<int>(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<float>& 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<int>(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<int>(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<int>(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

58
src/ui/Measurements.h Normal file
View File

@@ -0,0 +1,58 @@
#pragma once
#include "core/Types.h"
#include "ui/SpectrumDisplay.h"
#include <vector>
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<float>& 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<PeakInfo>& peaks() const { return peaks_; }
// Settings
bool enabled = false; // master enable
int maxPeaks = 5; // how many peaks to detect (120)
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<PeakInfo> peaks_;
// Find top-N peaks with minimum bin separation.
void findPeaks(const std::vector<float>& spectrumDB, int maxN,
int minDist, float threshold);
static double binToFreq(int bin, double sampleRate, bool isIQ, int fftSize);
};
} // namespace baudline