Files
baudmine/src/ui/Cursors.cpp

200 lines
7.3 KiB
C++

#include "ui/Cursors.h"
#include <imgui.h>
#include <cmath>
#include <algorithm>
namespace baudmine {
static double binToFreqHelper(int bin, double sampleRate, bool isIQ, int fftSize) {
if (isIQ) {
return -sampleRate / 2.0 + (static_cast<double>(bin) / fftSize) * sampleRate;
} else {
return (static_cast<double>(bin) / fftSize) * sampleRate;
}
}
void Cursors::pushAvg(AvgState& st, float dB, int bin) const {
// Reset if cursor moved to a different bin or averaging was reduced.
if (bin != st.lastBin) {
st.samples.clear();
st.sum = 0.0;
st.lastBin = bin;
}
st.samples.push_back(dB);
st.sum += dB;
int maxN = std::max(1, avgCount);
while (static_cast<int>(st.samples.size()) > maxN) {
st.sum -= st.samples.front();
st.samples.pop_front();
}
}
float Cursors::avgDBA() const {
return avgA_.samples.empty() ? cursorA.dB
: static_cast<float>(avgA_.sum / avgA_.samples.size());
}
float Cursors::avgDBB() const {
return avgB_.samples.empty() ? cursorB.dB
: static_cast<float>(avgB_.sum / avgB_.samples.size());
}
void Cursors::update(const std::vector<float>& spectrumDB,
double sampleRate, bool isIQ, int fftSize) {
// Update dB values at cursor bin positions
if (cursorA.active && cursorA.bin >= 0 &&
cursorA.bin < static_cast<int>(spectrumDB.size())) {
cursorA.dB = spectrumDB[cursorA.bin];
pushAvg(avgA_, cursorA.dB, cursorA.bin);
}
if (cursorB.active && cursorB.bin >= 0 &&
cursorB.bin < static_cast<int>(spectrumDB.size())) {
cursorB.dB = spectrumDB[cursorB.bin];
pushAvg(avgB_, cursorB.dB, cursorB.bin);
}
}
void Cursors::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 {
ImDrawList* dl = ImGui::GetWindowDrawList();
// Draw cursor lines and crosshairs (no labels here).
auto drawCursorMarker = [&](const CursorInfo& c, float dispDB, ImU32 color) {
if (!c.active) return;
float x = specDisplay.freqToScreenX(c.freq, posX, sizeX,
sampleRate, isIQ, freqScale,
viewLo, viewHi);
float dbNorm = (dispDB - minDB) / (maxDB - minDB);
dbNorm = std::clamp(dbNorm, 0.0f, 1.0f);
float y = posY + sizeY * (1.0f - dbNorm);
dl->AddLine({x, posY}, {x, posY + sizeY}, color, 1.0f);
dl->AddLine({posX, y}, {posX + sizeX, y}, color & 0x80FFFFFF, 1.0f);
dl->AddCircle({x, y}, 5.0f, color, 12, 2.0f);
};
// Format a cursor label string.
auto formatLabel = [](char* buf, size_t sz, const char* label, double freq, float dB) {
fmtFreqDB(buf, sz, label, freq, dB);
};
float aDB = avgDBA(), bDB = avgDBB();
drawCursorMarker(cursorA, aDB, IM_COL32(255, 255, 0, 220));
drawCursorMarker(cursorB, bDB, IM_COL32(100, 220, 255, 220));
// Draw labels at the top, touching the cursor's vertical line.
// If the label would overflow the right edge, flip it to the left side.
auto drawCursorLabel = [&](const CursorInfo& c, float dispDB, ImU32 color,
const char* label, int row) {
if (!c.active) return;
float x = specDisplay.freqToScreenX(c.freq, posX, sizeX,
sampleRate, isIQ, freqScale,
viewLo, viewHi);
char buf[128];
formatLabel(buf, sizeof(buf), label, c.freq, dispDB);
ImVec2 sz = ImGui::CalcTextSize(buf);
float lineH = ImGui::GetTextLineHeight();
// draw starting from the second line -- on the first line, we have cursor data
float ty = posY + 4 + (row + 1) * (lineH + 4);
// Place right of cursor line; flip left if it would overflow.
float tx;
if (x + 6 + sz.x + 2 <= posX + sizeX)
tx = x + 6;
else
tx = x - 6 - sz.x;
dl->AddRectFilled({tx - 2, ty - 1}, {tx + sz.x + 2, ty + sz.y + 1},
IM_COL32(0, 0, 0, 180));
dl->AddText({tx, ty}, color, buf);
};
drawCursorLabel(cursorA, aDB, IM_COL32(255, 255, 0, 220), "A", 0);
drawCursorLabel(cursorB, bDB, IM_COL32(100, 220, 255, 220), "B", cursorA.active ? 1 : 0);
// Delta display (two lines, column-aligned on '=')
if (showDelta && cursorA.active && cursorB.active) {
double dFreq = cursorB.freq - cursorA.freq;
float dDB = bDB - aDB;
char deltaBuf[128];
fmtFreqDB(deltaBuf, sizeof(deltaBuf), "D", dFreq, dDB);
ImVec2 dSz = ImGui::CalcTextSize(deltaBuf);
// Reserve space for hover label to the right.
float reserveW = ImGui::CalcTextSize(" 00.000 kHz 000.0 dB").x;
float tx = posX + sizeX - dSz.x - reserveW;
float lineH = ImGui::GetTextLineHeight();
float ty = posY + 4;
ImU32 col = IM_COL32(255, 200, 100, 255);
dl->AddText({tx, ty}, col, deltaBuf);
}
// (Hover cursor line is drawn cross-panel by Application.)
}
void Cursors::drawPanel() {
auto showCursor = [](const char* label, const CursorInfo& c, float dispDB) {
if (!c.active) {
ImGui::TextDisabled("%s: --", label);
return;
}
char buf[128];
fmtFreqDB(buf, sizeof(buf), label, c.freq, dispDB);
ImGui::Text("%s", buf);
};
float aDB = avgDBA(), bDB = avgDBB();
showCursor("A", cursorA, aDB);
showCursor("B", cursorB, bDB);
if (cursorA.active && cursorB.active) {
double dF = cursorB.freq - cursorA.freq;
float dA = bDB - aDB;
char dbuf[128];
fmtFreqDB(dbuf, sizeof(dbuf), "D", dF, dA);
ImGui::Text("%s", dbuf);
}
ImGui::Checkbox("Snap to peaks", &snapToPeaks);
// Averaging slider (logarithmic scale)
ImGui::SetNextItemWidth(-1);
ImGui::SliderInt("##avgcount", &avgCount, 1, 20000, avgCount == 1 ? "No avg" : "Avg: %d",
ImGuiSliderFlags_Logarithmic);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Cursor averaging (samples)");
}
void Cursors::setCursorA(double freq, float dB, int bin) {
cursorA = {true, freq, dB, bin};
}
void Cursors::setCursorB(double freq, float dB, int bin) {
cursorB = {true, freq, dB, bin};
}
void Cursors::snapToPeak(const std::vector<float>& spectrumDB,
double sampleRate, bool isIQ, int fftSize) {
if (spectrumDB.empty()) return;
auto it = std::max_element(spectrumDB.begin(), spectrumDB.end());
int bin = static_cast<int>(std::distance(spectrumDB.begin(), it));
double freq = binToFreqHelper(bin, sampleRate, isIQ, fftSize);
setCursorA(freq, *it, bin);
}
int Cursors::findLocalPeak(const std::vector<float>& spectrumDB,
int centerBin, int window) const {
int bins = static_cast<int>(spectrumDB.size());
int lo = std::max(0, centerBin - window);
int hi = std::min(bins - 1, centerBin + window);
int best = lo;
for (int i = lo + 1; i <= hi; ++i) {
if (spectrumDB[i] > spectrumDB[best]) best = i;
}
return best;
}
} // namespace baudmine