commit no. 10
This commit is contained in:
@@ -61,6 +61,7 @@ set(SOURCES
|
|||||||
src/ui/WaterfallDisplay.cpp
|
src/ui/WaterfallDisplay.cpp
|
||||||
src/ui/SpectrumDisplay.cpp
|
src/ui/SpectrumDisplay.cpp
|
||||||
src/ui/Cursors.cpp
|
src/ui/Cursors.cpp
|
||||||
|
src/ui/Measurements.cpp
|
||||||
src/ui/Application.cpp
|
src/ui/Application.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ void Application::processAudio() {
|
|||||||
int curCh = std::clamp(waterfallChannel_, 0, nSpec - 1);
|
int curCh = std::clamp(waterfallChannel_, 0, nSpec - 1);
|
||||||
cursors_.update(analyzer_.channelSpectrum(curCh),
|
cursors_.update(analyzer_.channelSpectrum(curCh),
|
||||||
settings_.sampleRate, settings_.isIQ, settings_.fftSize);
|
settings_.sampleRate, settings_.isIQ, settings_.fftSize);
|
||||||
|
measurements_.update(analyzer_.channelSpectrum(curCh),
|
||||||
|
settings_.sampleRate, settings_.isIQ, settings_.fftSize);
|
||||||
++spectraThisFrame;
|
++spectraThisFrame;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,6 +642,24 @@ void Application::renderControlPanel() {
|
|||||||
cursors_.drawPanel();
|
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) ──
|
// ── Status (bottom) ──
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::TextDisabled("Mode: %s", settings_.isIQ ? "I/Q"
|
ImGui::TextDisabled("Mode: %s", settings_.isIQ ? "I/Q"
|
||||||
@@ -706,6 +726,10 @@ void Application::renderSpectrumPanel() {
|
|||||||
settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_,
|
settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_,
|
||||||
viewLo_, viewHi_);
|
viewLo_, viewHi_);
|
||||||
|
|
||||||
|
measurements_.draw(specDisplay_, specPosX_, specPosY_, specSizeX_, specSizeY_,
|
||||||
|
settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_,
|
||||||
|
viewLo_, viewHi_);
|
||||||
|
|
||||||
handleSpectrumInput(specPosX_, specPosY_, specSizeX_, specSizeY_);
|
handleSpectrumInput(specPosX_, specPosY_, specSizeX_, specSizeY_);
|
||||||
|
|
||||||
ImGui::Dummy({availW, specH});
|
ImGui::Dummy({availW, specH});
|
||||||
@@ -843,6 +867,10 @@ void Application::renderWaterfallPanel() {
|
|||||||
// Store waterfall geometry for cross-panel cursor drawing.
|
// Store waterfall geometry for cross-panel cursor drawing.
|
||||||
wfPosX_ = pos.x; wfPosY_ = pos.y; wfSizeX_ = availW; wfSizeY_ = availH;
|
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 ──
|
// ── Mouse interaction: zoom, pan & hover on waterfall ──
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
float mx = io.MousePos.x;
|
float mx = io.MousePos.x;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "ui/WaterfallDisplay.h"
|
#include "ui/WaterfallDisplay.h"
|
||||||
#include "ui/SpectrumDisplay.h"
|
#include "ui/SpectrumDisplay.h"
|
||||||
#include "ui/Cursors.h"
|
#include "ui/Cursors.h"
|
||||||
|
#include "ui/Measurements.h"
|
||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include <complex>
|
#include <complex>
|
||||||
@@ -111,6 +112,7 @@ private:
|
|||||||
WaterfallDisplay waterfall_;
|
WaterfallDisplay waterfall_;
|
||||||
SpectrumDisplay specDisplay_;
|
SpectrumDisplay specDisplay_;
|
||||||
Cursors cursors_;
|
Cursors cursors_;
|
||||||
|
Measurements measurements_;
|
||||||
|
|
||||||
// Display settings
|
// Display settings
|
||||||
float minDB_ = -120.0f;
|
float minDB_ = -120.0f;
|
||||||
|
|||||||
203
src/ui/Measurements.cpp
Normal file
203
src/ui/Measurements.cpp
Normal 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
58
src/ui/Measurements.h
Normal 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 (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<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
|
||||||
Reference in New Issue
Block a user