diff --git a/src/core/Types.h b/src/core/Types.h index 8b7040b..6faf27d 100644 --- a/src/core/Types.h +++ b/src/core/Types.h @@ -103,7 +103,6 @@ struct AnalyzerSettings { bool isIQ = false; // true → complex input (2-ch interleaved) int numChannels = 1; // real channels (ignored when isIQ) double sampleRate = 48000.0; - int averaging = 1; // number of spectra to average (1 = none) // Effective input channel count (for buffer sizing / deinterleaving). int inputChannels() const { return isIQ ? 2 : numChannels; } diff --git a/src/dsp/SpectrumAnalyzer.cpp b/src/dsp/SpectrumAnalyzer.cpp index 71d3fb4..5ae6be7 100644 --- a/src/dsp/SpectrumAnalyzer.cpp +++ b/src/dsp/SpectrumAnalyzer.cpp @@ -37,9 +37,6 @@ void SpectrumAnalyzer::configure(const AnalyzerSettings& settings) { channelComplex_.assign(nSpec, std::vector>(specSz, {0,0})); channelWaterfalls_.assign(nSpec, {}); - avgAccum_.assign(nSpec, std::vector(specSz, 0.0f)); - avgCount_ = 0; - newSpectrumReady_ = false; } } @@ -105,28 +102,6 @@ void SpectrumAnalyzer::processBlock() { for (float& v : db) v += correction; - // Averaging (dB only; complex is not averaged — last block is kept). - if (settings_.averaging > 1) { - if (static_cast(avgAccum_[0].size()) != specSz) { - for (auto& a : avgAccum_) a.assign(specSz, 0.0f); - avgCount_ = 0; - } - for (int ch = 0; ch < nSpec; ++ch) - for (int i = 0; i < specSz; ++i) - avgAccum_[ch][i] += tempDBs[ch][i]; - avgCount_++; - - if (avgCount_ >= settings_.averaging) { - for (int ch = 0; ch < nSpec; ++ch) - for (int i = 0; i < specSz; ++i) - tempDBs[ch][i] = avgAccum_[ch][i] / avgCount_; - for (auto& a : avgAccum_) a.assign(specSz, 0.0f); - avgCount_ = 0; - } else { - return; - } - } - for (int ch = 0; ch < nSpec; ++ch) { channelSpectra_[ch] = tempDBs[ch]; channelComplex_[ch] = tempCplx[ch]; diff --git a/src/dsp/SpectrumAnalyzer.h b/src/dsp/SpectrumAnalyzer.h index f3c247a..e1e3a58 100644 --- a/src/dsp/SpectrumAnalyzer.h +++ b/src/dsp/SpectrumAnalyzer.h @@ -58,10 +58,6 @@ private: size_t accumPos_ = 0; size_t hopSize_ = 0; - // Per-channel averaging - std::vector> avgAccum_; - int avgCount_ = 0; - // Per-channel output: magnitude (dB) and complex std::vector> channelSpectra_; std::vector>> channelComplex_; diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 7292e73..352bbe5 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -270,13 +270,69 @@ void Application::render() { ImGui::SameLine(); - // Spectrum + Waterfall + // Spectrum + Waterfall with draggable splitter ImGui::BeginChild("Display", {contentW, contentH}, false); - float specH = contentH * 0.35f; - float waterfH = contentH * 0.65f - 4; + { + constexpr float kSplitterH = 6.0f; + float specH = contentH * spectrumFrac_; + float waterfH = contentH - specH - kSplitterH; - renderSpectrumPanel(); - renderWaterfallPanel(); + renderSpectrumPanel(); + + // ── Draggable splitter bar ── + ImVec2 splPos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("##splitter", {contentW, kSplitterH}); + bool hovered = ImGui::IsItemHovered(); + bool active = ImGui::IsItemActive(); + + if (hovered || active) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); + + if (active) { + float dy = ImGui::GetIO().MouseDelta.y; + spectrumFrac_ += dy / contentH; + spectrumFrac_ = std::clamp(spectrumFrac_, 0.1f, 0.9f); + } + + // Draw splitter line + ImU32 splCol = (hovered || active) + ? IM_COL32(100, 150, 255, 220) + : IM_COL32(80, 80, 100, 150); + ImDrawList* dl = ImGui::GetWindowDrawList(); + float cy = splPos.y + kSplitterH * 0.5f; + dl->AddLine({splPos.x, cy}, {splPos.x + contentW, cy}, splCol, 2.0f); + + renderWaterfallPanel(); + + // ── Cross-panel hover line & frequency label ── + if (cursors_.hover.active && specSizeX_ > 0 && wfSizeX_ > 0) { + ImDrawList* dlp = ImGui::GetWindowDrawList(); + float hx = specDisplay_.freqToScreenX(cursors_.hover.freq, + specPosX_, specSizeX_, settings_.sampleRate, + settings_.isIQ, freqScale_, viewLo_, viewHi_); + ImU32 hoverCol = IM_COL32(200, 200, 200, 80); + + // Line spanning spectrum + splitter + waterfall + dlp->AddLine({hx, specPosY_}, {hx, wfPosY_ + wfSizeY_}, hoverCol, 1.0f); + + // Frequency label at top of the line + char freqLabel[48]; + double hf = cursors_.hover.freq; + if (std::abs(hf) >= 1e6) + std::snprintf(freqLabel, sizeof(freqLabel), "%.6f MHz", hf / 1e6); + else if (std::abs(hf) >= 1e3) + std::snprintf(freqLabel, sizeof(freqLabel), "%.3f kHz", hf / 1e3); + else + std::snprintf(freqLabel, sizeof(freqLabel), "%.1f Hz", hf); + + ImVec2 tSz = ImGui::CalcTextSize(freqLabel); + float lx = std::min(hx + 4, specPosX_ + specSizeX_ - tSz.x - 4); + float ly = specPosY_ + 2; + dlp->AddRectFilled({lx - 2, ly - 1}, {lx + tSz.x + 2, ly + tSz.y + 1}, + IM_COL32(0, 0, 0, 180)); + dlp->AddText({lx, ly}, IM_COL32(220, 220, 240, 240), freqLabel); + } + } ImGui::EndChild(); ImGui::End(); @@ -356,10 +412,33 @@ void Application::renderControlPanel() { } } - // Overlap - if (ImGui::SliderFloat("Overlap", &overlapPct_, 0.0f, 95.0f, "%.1f%%")) { - settings_.overlap = overlapPct_ / 100.0f; - updateAnalyzerSettings(); + // Overlap — inverted x⁴ curve: sensitive at the high end (90%+). + { + int hopSamples = static_cast(settings_.fftSize * (1.0f - settings_.overlap)); + if (hopSamples < 1) hopSamples = 1; + int overlapSamples = settings_.fftSize - hopSamples; + + 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); + settings_.overlap = overlapPct_ / 100.0f; + updateAnalyzerSettings(); + } + + // Draw overlay text centered on the slider frame (not the label). + char overlayText[64]; + std::snprintf(overlayText, sizeof(overlayText), "%.1f%% (%d samples)", overlapPct_, overlapSamples); + ImVec2 textSize = ImGui::CalcTextSize(overlayText); + // The slider frame width = total widget width minus label. + // ImGui::CalcItemWidth() gives the frame width. + ImVec2 sliderMin = ImGui::GetItemRectMin(); + float frameW = ImGui::CalcItemWidth(); + float frameH = ImGui::GetItemRectMax().y - sliderMin.y; + float tx = sliderMin.x + (frameW - textSize.x) * 0.5f; + float ty = sliderMin.y + (frameH - textSize.y) * 0.5f; + ImGui::GetForegroundDrawList()->AddText({tx, ty}, IM_COL32(255, 255, 255, 220), overlayText); } // Window function @@ -382,9 +461,6 @@ void Application::renderControlPanel() { } } - // Averaging - ImGui::SliderInt("Averaging", &settings_.averaging, 1, 32); - ImGui::Separator(); ImGui::Text("Display"); @@ -406,10 +482,34 @@ void Application::renderControlPanel() { freqScale_ = static_cast(fs); } + // Zoom info & reset + if (viewLo_ > 0.0f || viewHi_ < 1.0f) { + float zoomPct = 1.0f / (viewHi_ - viewLo_); + ImGui::Text("Zoom: %.1fx", zoomPct); + ImGui::SameLine(); + if (ImGui::SmallButton("Reset")) { + viewLo_ = 0.0f; + viewHi_ = 1.0f; + } + } + ImGui::TextDisabled("Scroll: freq zoom | MMB drag: pan"); + ImGui::TextDisabled("Ctrl+Scroll: dB zoom | MMB dbl: reset"); + // dB range ImGui::DragFloatRange2("dB Range", &minDB_, &maxDB_, 1.0f, -200.0f, 20.0f, "Min: %.0f", "Max: %.0f"); + // Peak hold + ImGui::Checkbox("Peak Hold", &specDisplay_.peakHoldEnable); + if (specDisplay_.peakHoldEnable) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); + ImGui::SliderFloat("Decay", &specDisplay_.peakHoldDecay, 0.0f, 120.0f, "%.0f dB/s"); + ImGui::SameLine(); + if (ImGui::SmallButton("Clear##peakhold")) + specDisplay_.clearPeakHold(); + } + // Channel colors (only shown for multi-channel) int nCh = analyzer_.numSpectra(); if (nCh > 1) { @@ -486,7 +586,11 @@ void Application::renderControlPanel() { void Application::renderSpectrumPanel() { float availW = ImGui::GetContentRegionAvail().x; - float specH = ImGui::GetContentRegionAvail().y * 0.35f; + // Use the parent's full content height (availY includes spectrum + splitter + waterfall) + // to compute the spectrum height from the split fraction. + constexpr float kSplitterH = 6.0f; + float parentH = ImGui::GetContentRegionAvail().y; + float specH = (parentH - kSplitterH) * spectrumFrac_; ImVec2 pos = ImGui::GetCursorScreenPos(); specPosX_ = pos.x; @@ -526,12 +630,15 @@ void Application::renderSpectrumPanel() { } } + specDisplay_.updatePeakHold(allSpectra); specDisplay_.draw(allSpectra, styles, minDB_, maxDB_, settings_.sampleRate, settings_.isIQ, freqScale_, - specPosX_, specPosY_, specSizeX_, specSizeY_); + specPosX_, specPosY_, specSizeX_, specSizeY_, + viewLo_, viewHi_); cursors_.draw(specDisplay_, specPosX_, specPosY_, specSizeX_, specSizeY_, - settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_); + settings_.sampleRate, settings_.isIQ, freqScale_, minDB_, maxDB_, + viewLo_, viewHi_); handleSpectrumInput(specPosX_, specPosY_, specSizeX_, specSizeY_); @@ -542,64 +649,113 @@ void Application::renderWaterfallPanel() { float availW = ImGui::GetContentRegionAvail().x; float availH = ImGui::GetContentRegionAvail().y; - int newW = static_cast(availW); - int newH = static_cast(availH); - if (newW < 1) newW = 1; - if (newH < 1) newH = 1; + // Fixed history depth — independent of screen height, so resizing the + // splitter doesn't recreate the texture every frame. + constexpr int kHistoryRows = 1024; - if (newW != waterfallW_ || newH != waterfallH_) { - waterfallW_ = newW; - waterfallH_ = newH; - waterfall_.resize(waterfallW_, waterfallH_); + // Only recreate when the bin count (FFT size) changes. + int binCount = std::max(1, analyzer_.spectrumSize()); + if (binCount != waterfall_.width() || waterfall_.height() != kHistoryRows) { + waterfall_.resize(binCount, kHistoryRows); waterfall_.setColorMap(colorMap_); } if (waterfall_.textureID()) { - // Render waterfall texture with circular buffer offset. - // The texture rows wrap: currentRow_ is where the *next* line will go, - // so the *newest* line is at currentRow_+1. - float rowFrac = static_cast(waterfall_.currentRow() + 1) / - waterfall_.height(); - - // UV coordinates: bottom of display = newest = rowFrac - // top of display = oldest = rowFrac + 1.0 (wraps) - // We'll use two draw calls to handle the wrap, or use GL_REPEAT. - // Simplest: just render with ImGui::Image and accept minor visual glitch, - // or split into two parts. - ImVec2 pos = ImGui::GetCursorScreenPos(); ImDrawList* dl = ImGui::GetWindowDrawList(); auto texID = static_cast(waterfall_.textureID()); int h = waterfall_.height(); - int cur = (waterfall_.currentRow() + 1) % h; - float splitFrac = static_cast(h - cur) / h; + // The newest row was just written at currentRow()+1 (mod h) — but + // advanceRow already decremented, so currentRow() IS the newest. + // The row *after* currentRow() (i.e. currentRow()+1) is the oldest + // visible row. We only want the most recent screenRows rows so + // that every texture row maps to exactly one screen pixel. + int screenRows = std::min(static_cast(availH), h); - // Top part: rows from cur to h-1 (oldest) - float topH = availH * splitFrac; - dl->AddImage(texID, - {pos.x, pos.y}, - {pos.x + availW, pos.y + topH}, - {0.0f, static_cast(cur) / h}, - {1.0f, 1.0f}); + // Newest row index in the circular buffer. + int newestRow = (waterfall_.currentRow() + 1) % h; - // Bottom part: rows from 0 to cur-1 (newest) - if (cur > 0) { - dl->AddImage(texID, - {pos.x, pos.y + topH}, - {pos.x + availW, pos.y + availH}, - {0.0f, 0.0f}, - {1.0f, static_cast(cur) / h}); + // Render 1:1 (one texture row = one screen pixel), top-aligned, + // newest line at top (right below the spectrogram), scrolling down. + // + // advanceRow() decrements currentRow_, so rows are written at + // decreasing indices. Going from newest to oldest = increasing + // index (mod h). Normal V order (no flip needed). + float rowToV = 1.0f / h; + float screenY = pos.y; + + bool logMode = (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}, + {viewLo_, v0}, {viewHi_, v1}); + } else { + constexpr float kMinBinFrac = 0.001f; + float logMin2 = std::log10(kMinBinFrac); + float logMax2 = 0.0f; + int numStrips = std::min(512, static_cast(availW)); + for (int s = 0; s < numStrips; ++s) { + float sL = static_cast(s) / numStrips; + float sR = static_cast(s + 1) / numStrips; + float vfL = viewLo_ + sL * (viewHi_ - viewLo_); + float vfR = viewLo_ + sR * (viewHi_ - 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, v0}, {uR, v1}); + } + } + }; + + // From newestRow, walk forward (increasing index mod h) for + // screenRows steps to cover newest→oldest. + if (newestRow + screenRows <= h) { + // No wrap: rows [newestRow .. newestRow+screenRows) + drawSpan(newestRow, screenRows, screenY, static_cast(screenRows)); + } else { + // Wraps: [newestRow .. h), then [0 .. remainder) + int firstCount = h - newestRow; + float firstH = static_cast(firstCount); + drawSpan(newestRow, firstCount, screenY, firstH); + + int secondCount = screenRows - firstCount; + float secondH = static_cast(secondCount); + if (secondCount > 0) + drawSpan(0, secondCount, screenY + firstH, secondH); } - // Frequency axis labels at bottom + // ── Frequency axis labels ── ImU32 textCol = IM_COL32(180, 180, 200, 200); - double freqMin = settings_.isIQ ? -settings_.sampleRate / 2.0 : 0.0; - double freqMax = settings_.isIQ ? settings_.sampleRate / 2.0 : settings_.sampleRate / 2.0; + double freqFullMin = settings_.isIQ ? -settings_.sampleRate / 2.0 : 0.0; + double freqFullMax = settings_.isIQ ? settings_.sampleRate / 2.0 : settings_.sampleRate / 2.0; + + // Map a view fraction to frequency. In log mode, viewLo_/viewHi_ + // are in screen-fraction space; convert via the log mapping. + 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(i) / numLabels; - double freq = freqMin + frac * (freqMax - freqMin); + float vf = viewLo_ + frac * (viewHi_ - viewLo_); + double freq = viewFracToFreq(vf); float x = pos.x + frac * availW; char label[32]; @@ -612,6 +768,75 @@ void Application::renderWaterfallPanel() { dl->AddText({x + 2, pos.y + availH - 14}, textCol, label); } + + // Store waterfall geometry for cross-panel cursor drawing. + wfPosX_ = pos.x; wfPosY_ = pos.y; wfSizeX_ = availW; wfSizeY_ = availH; + + // ── 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; + + // Hover cursor from waterfall + if (inWaterfall) { + double freq = specDisplay_.screenXToFreq(mx, pos.x, availW, + settings_.sampleRate, + settings_.isIQ, freqScale_, + viewLo_, viewHi_); + int bins = analyzer_.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((freq - fMin) / (fMax - fMin) * (bins - 1)); + bin = std::clamp(bin, 0, bins - 1); + + int curCh = std::clamp(waterfallChannel_, 0, analyzer_.numSpectra() - 1); + const auto& spec = analyzer_.channelSpectrum(curCh); + if (!spec.empty()) { + cursors_.hover = {true, freq, spec[bin], bin}; + } + } + + if (inWaterfall) { + // Scroll wheel: zoom centered on cursor + if (io.MouseWheel != 0) { + float cursorFrac = (mx - pos.x) / availW; // 0..1 on screen + float viewFrac = viewLo_ + cursorFrac * (viewHi_ - viewLo_); + + float zoomFactor = (io.MouseWheel > 0) ? 0.85f : 1.0f / 0.85f; + float newSpan = (viewHi_ - viewLo_) * zoomFactor; + newSpan = std::clamp(newSpan, 0.001f, 1.0f); + + float newLo = viewFrac - cursorFrac * newSpan; + float newHi = newLo + newSpan; + + // Clamp to [0, 1] + if (newLo < 0.0f) { newHi -= newLo; newLo = 0.0f; } + if (newHi > 1.0f) { newLo -= (newHi - 1.0f); newHi = 1.0f; } + viewLo_ = std::clamp(newLo, 0.0f, 1.0f); + viewHi_ = std::clamp(newHi, 0.0f, 1.0f); + } + + // Middle-click + drag: pan + if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 1.0f)) { + float dx = io.MouseDelta.x; + float panFrac = -dx / availW * (viewHi_ - viewLo_); + float newLo = viewLo_ + panFrac; + float newHi = viewHi_ + panFrac; + float span = viewHi_ - viewLo_; + if (newLo < 0.0f) { newLo = 0.0f; newHi = span; } + if (newHi > 1.0f) { newHi = 1.0f; newLo = 1.0f - span; } + viewLo_ = newLo; + viewHi_ = newHi; + } + + // Double-click: reset zoom + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) { + viewLo_ = 0.0f; + viewHi_ = 1.0f; + } + } } ImGui::Dummy({availW, availH}); @@ -630,7 +855,8 @@ void Application::handleSpectrumInput(float posX, float posY, // Update hover cursor double freq = specDisplay_.screenXToFreq(mx, posX, sizeX, settings_.sampleRate, - settings_.isIQ, freqScale_); + settings_.isIQ, freqScale_, + viewLo_, viewHi_); float dB = specDisplay_.screenYToDB(my, posY, sizeY, minDB_, maxDB_); // Find closest bin @@ -648,27 +874,65 @@ void Application::handleSpectrumInput(float posX, float posY, } // Left click: cursor A - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.WantCaptureMouse) { + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { int peakBin = cursors_.findLocalPeak(spec, bin, 10); double peakFreq = analyzer_.binToFreq(peakBin); cursors_.setCursorA(peakFreq, spec[peakBin], peakBin); } // Right click: cursor B - if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !io.WantCaptureMouse) { + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { int peakBin = cursors_.findLocalPeak(spec, bin, 10); double peakFreq = analyzer_.binToFreq(peakBin); cursors_.setCursorB(peakFreq, spec[peakBin], peakBin); } - // Scroll: zoom dB range - if (io.MouseWheel != 0 && !io.WantCaptureMouse) { - float zoom = io.MouseWheel * 5.0f; - minDB_ += zoom; - maxDB_ -= zoom; - if (maxDB_ - minDB_ < 10.0f) { - float mid = (minDB_ + maxDB_) / 2.0f; - minDB_ = mid - 5.0f; - maxDB_ = mid + 5.0f; + { + // Ctrl+Scroll or Shift+Scroll: zoom dB range + if (io.MouseWheel != 0 && (io.KeyCtrl || io.KeyShift)) { + float zoom = io.MouseWheel * 5.0f; + minDB_ += zoom; + maxDB_ -= zoom; + if (maxDB_ - minDB_ < 10.0f) { + float mid = (minDB_ + maxDB_) / 2.0f; + minDB_ = mid - 5.0f; + maxDB_ = mid + 5.0f; + } + } + // Scroll (no modifier): zoom frequency axis centered on cursor + else if (io.MouseWheel != 0) { + float cursorFrac = (mx - posX) / sizeX; + float viewFrac = viewLo_ + cursorFrac * (viewHi_ - viewLo_); + + float zoomFactor = (io.MouseWheel > 0) ? 0.85f : 1.0f / 0.85f; + float newSpan = (viewHi_ - 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; } + viewLo_ = std::clamp(newLo, 0.0f, 1.0f); + viewHi_ = std::clamp(newHi, 0.0f, 1.0f); + } + + // Middle-click + drag: pan + if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 1.0f)) { + float dx = io.MouseDelta.x; + float panFrac = -dx / sizeX * (viewHi_ - viewLo_); + float newLo = viewLo_ + panFrac; + float newHi = viewHi_ + panFrac; + float span = viewHi_ - viewLo_; + if (newLo < 0.0f) { newLo = 0.0f; newHi = span; } + if (newHi > 1.0f) { newHi = 1.0f; newLo = 1.0f - span; } + viewLo_ = newLo; + viewHi_ = newHi; + } + + // Double middle-click: reset zoom + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) { + viewLo_ = 0.0f; + viewHi_ = 1.0f; } } } else { @@ -747,8 +1011,9 @@ void Application::updateAnalyzerSettings() { // Re-init waterfall texture so the old image from a different FFT // size doesn't persist. - if (waterfallW_ > 0 && waterfallH_ > 0) - waterfall_.init(waterfallW_, waterfallH_); + constexpr int kHistoryRows = 1024; + int binCount2 = std::max(1, analyzer_.spectrumSize()); + waterfall_.init(binCount2, kHistoryRows); } } diff --git a/src/ui/Application.h b/src/ui/Application.h index ea00606..d2bea2a 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -113,8 +113,8 @@ private: float maxDB_ = 0.0f; FreqScale freqScale_ = FreqScale::Linear; bool paused_ = false; - int waterfallW_ = 0; - int waterfallH_ = 0; + // (waterfallW_ removed — texture width tracks bin count automatically) + // (waterfallH_ removed — fixed history depth of 1024 rows) // FFT size options static constexpr int kFFTSizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536}; @@ -159,8 +159,17 @@ private: std::vector mathChannels_; std::vector> mathSpectra_; // computed each frame - // Spectrum panel geometry (stored for cursor interaction) + // Frequency zoom/pan (normalized 0–1 over full bandwidth) + float viewLo_ = 0.0f; // left edge + float viewHi_ = 1.0f; // right edge + + // 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; }; } // namespace baudline diff --git a/src/ui/Cursors.cpp b/src/ui/Cursors.cpp index d7fe819..36b72a6 100644 --- a/src/ui/Cursors.cpp +++ b/src/ui/Cursors.cpp @@ -29,13 +29,15 @@ void Cursors::update(const std::vector& spectrumDB, void Cursors::draw(const SpectrumDisplay& specDisplay, float posX, float posY, float sizeX, float sizeY, double sampleRate, bool isIQ, FreqScale freqScale, - float minDB, float maxDB) const { + float minDB, float maxDB, + float viewLo, float viewHi) const { ImDrawList* dl = ImGui::GetWindowDrawList(); auto drawCursor = [&](const CursorInfo& c, ImU32 color, const char* label) { if (!c.active) return; float x = specDisplay.freqToScreenX(c.freq, posX, sizeX, - sampleRate, isIQ, freqScale); + sampleRate, isIQ, freqScale, + viewLo, viewHi); float dbNorm = (c.dB - minDB) / (maxDB - minDB); dbNorm = std::clamp(dbNorm, 0.0f, 1.0f); float y = posY + sizeY * (1.0f - dbNorm); @@ -93,12 +95,7 @@ void Cursors::draw(const SpectrumDisplay& specDisplay, dl->AddText({tx, ty}, IM_COL32(255, 200, 100, 255), buf); } - // Hover cursor - if (hover.active) { - float x = specDisplay.freqToScreenX(hover.freq, posX, sizeX, - sampleRate, isIQ, freqScale); - dl->AddLine({x, posY}, {x, posY + sizeY}, IM_COL32(200, 200, 200, 80), 1.0f); - } + // (Hover cursor line is drawn cross-panel by Application.) } void Cursors::drawPanel() const { diff --git a/src/ui/Cursors.h b/src/ui/Cursors.h index db4a21f..f2a0447 100644 --- a/src/ui/Cursors.h +++ b/src/ui/Cursors.h @@ -23,7 +23,8 @@ public: void draw(const SpectrumDisplay& specDisplay, float posX, float posY, float sizeX, float sizeY, double sampleRate, bool isIQ, FreqScale freqScale, - float minDB, float maxDB) const; + float minDB, float maxDB, + float viewLo = 0.0f, float viewHi = 1.0f) const; // Draw cursor readout panel (ImGui widgets). void drawPanel() const; diff --git a/src/ui/SpectrumDisplay.cpp b/src/ui/SpectrumDisplay.cpp index b2d678f..0956bb4 100644 --- a/src/ui/SpectrumDisplay.cpp +++ b/src/ui/SpectrumDisplay.cpp @@ -12,12 +12,35 @@ static float freqToLogFrac(double freq, double minFreq, double maxFreq) { return static_cast((logF - logMin) / (logMax - logMin)); } -// Build a decimated polyline for one spectrum. +// Map a "full-range screen fraction" (0–1) to a bin fraction, applying log if needed. +static float screenFracToBinFrac(float frac, FreqScale freqScale, bool isIQ) { + if (freqScale == FreqScale::Logarithmic && !isIQ) { + constexpr float kMinBinFrac = 0.001f; + float logMin = std::log10(kMinBinFrac); + float logMax = 0.0f; + return std::pow(10.0f, logMin + frac * (logMax - logMin)); + } + return frac; +} + +// Inverse: bin fraction → screen fraction. +static float binFracToScreenFrac(float binFrac, FreqScale freqScale, bool isIQ) { + if (freqScale == FreqScale::Logarithmic && !isIQ) { + constexpr float kMinBinFrac = 0.001f; + float logMin = std::log10(kMinBinFrac); + float logMax = 0.0f; + if (binFrac < kMinBinFrac) binFrac = kMinBinFrac; + return (std::log10(binFrac) - logMin) / (logMax - logMin); + } + return binFrac; +} + +// Build a decimated polyline for one spectrum, considering view range. static void buildPolyline(const std::vector& spectrumDB, float minDB, float maxDB, - double freqMin, double freqMax, bool isIQ, FreqScale freqScale, float posX, float posY, float sizeX, float sizeY, + float viewLo, float viewHi, std::vector& outPoints) { int bins = static_cast(spectrumDB.size()); int displayPts = std::min(bins, static_cast(sizeX)); @@ -25,27 +48,26 @@ static void buildPolyline(const std::vector& spectrumDB, outPoints.resize(displayPts); for (int idx = 0; idx < displayPts; ++idx) { - float frac = static_cast(idx) / (displayPts - 1); - float xFrac; + float screenFrac = static_cast(idx) / (displayPts - 1); + // Map screen pixel → full-range fraction via viewLo/viewHi + float viewFrac = viewLo + screenFrac * (viewHi - viewLo); + // Map to bin fraction (apply log scale if needed) + float binFrac = screenFracToBinFrac(viewFrac, freqScale, isIQ); - if (freqScale == FreqScale::Logarithmic && !isIQ) { - double freq = frac * (freqMax - freqMin) + freqMin; - double logMin = std::max(freqMin, 1.0); - xFrac = freqToLogFrac(freq, logMin, freqMax); - } else { - xFrac = frac; - } + float binF = binFrac * (bins - 1); // Bucket range for peak-hold decimation. - float binF = frac * (bins - 1); - float binPrev = (idx > 0) - ? static_cast(idx - 1) / (displayPts - 1) * (bins - 1) - : binF; - float binNext = (idx < displayPts - 1) - ? static_cast(idx + 1) / (displayPts - 1) * (bins - 1) - : binF; - int b0 = static_cast((binPrev + binF) * 0.5f); - int b1 = static_cast((binF + binNext) * 0.5f); + float prevViewFrac = (idx > 0) + ? viewLo + static_cast(idx - 1) / (displayPts - 1) * (viewHi - viewLo) + : viewFrac; + float nextViewFrac = (idx < displayPts - 1) + ? viewLo + static_cast(idx + 1) / (displayPts - 1) * (viewHi - viewLo) + : viewFrac; + float prevBinF = screenFracToBinFrac(prevViewFrac, freqScale, isIQ) * (bins - 1); + float nextBinF = screenFracToBinFrac(nextViewFrac, freqScale, isIQ) * (bins - 1); + + int b0 = static_cast((prevBinF + binF) * 0.5f); + int b1 = static_cast((binF + nextBinF) * 0.5f); b0 = std::clamp(b0, 0, bins - 1); b1 = std::clamp(b1, b0, bins - 1); @@ -53,7 +75,7 @@ static void buildPolyline(const std::vector& spectrumDB, for (int b = b0 + 1; b <= b1; ++b) peakDB = std::max(peakDB, spectrumDB[b]); - float x = posX + xFrac * sizeX; + float x = posX + screenFrac * sizeX; float dbNorm = std::clamp((peakDB - minDB) / (maxDB - minDB), 0.0f, 1.0f); float y = posY + sizeY * (1.0f - dbNorm); outPoints[idx] = {x, y}; @@ -66,12 +88,19 @@ void SpectrumDisplay::draw(const std::vector>& spectra, double sampleRate, bool isIQ, FreqScale freqScale, float posX, float posY, - float sizeX, float sizeY) const { + float sizeX, float sizeY, + float viewLo, float viewHi) const { if (spectra.empty() || spectra[0].empty() || sizeX <= 0 || sizeY <= 0) return; ImDrawList* dl = ImGui::GetWindowDrawList(); - double freqMin = isIQ ? -sampleRate / 2.0 : 0.0; - double freqMax = isIQ ? sampleRate / 2.0 : sampleRate / 2.0; + double freqFullMin = isIQ ? -sampleRate / 2.0 : 0.0; + double freqFullMax = isIQ ? sampleRate / 2.0 : sampleRate / 2.0; + + // Helper to convert a view fraction to frequency. + auto viewFracToFreq = [&](float vf) -> double { + float binFrac = screenFracToBinFrac(vf, freqScale, isIQ); + return freqFullMin + binFrac * (freqFullMax - freqFullMin); + }; // Background dl->AddRectFilled({posX, posY}, {posX + sizeX, posY + sizeY}, @@ -82,42 +111,43 @@ void SpectrumDisplay::draw(const std::vector>& spectra, ImU32 gridCol = IM_COL32(60, 60, 80, 128); ImU32 textCol = IM_COL32(180, 180, 200, 200); + // ── Horizontal (dB) grid — adapt step to available height ── + constexpr float kMinPixPerHLine = 40.0f; // minimum pixels between labels + float dbRange = maxDB - minDB; + // Pick a nice step: 5, 10, 20, 50, ... float dbStep = 10.0f; + static const float niceSteps[] = {5.0f, 10.0f, 20.0f, 50.0f, 100.0f}; + for (float s : niceSteps) { + float pixPerStep = sizeY * s / dbRange; + if (pixPerStep >= kMinPixPerHLine) { dbStep = s; break; } + } + for (float db = std::ceil(minDB / dbStep) * dbStep; db <= maxDB; db += dbStep) { float y = posY + sizeY * (1.0f - (db - minDB) / (maxDB - minDB)); dl->AddLine({posX, y}, {posX + sizeX, y}, gridCol); - char label[32]; - std::snprintf(label, sizeof(label), "%.0f dB", db); + char label[16]; + std::snprintf(label, sizeof(label), "%.0f", db); dl->AddText({posX + 2, y - 12}, textCol, label); } - int numVLines = 8; + // ── Vertical (frequency) grid — adapt count to available width ── + constexpr float kMinPixPerVLine = 80.0f; + int numVLines = std::max(2, static_cast(sizeX / kMinPixPerVLine)); + for (int i = 0; i <= numVLines; ++i) { float frac = static_cast(i) / numVLines; - double freq; - float screenFrac; - - if (freqScale == FreqScale::Linear) { - freq = freqMin + frac * (freqMax - freqMin); - screenFrac = frac; - } else { - double logMinF = std::max(freqMin, 1.0); - double logMaxF = freqMax; - freq = std::pow(10.0, std::log10(logMinF) + - frac * (std::log10(logMaxF) - std::log10(logMinF))); - screenFrac = frac; - } - - float x = posX + screenFrac * sizeX; + float vf = viewLo + frac * (viewHi - viewLo); + double freq = viewFracToFreq(vf); + float x = posX + frac * sizeX; dl->AddLine({x, posY}, {x, posY + sizeY}, gridCol); char label[32]; if (std::abs(freq) >= 1e6) - std::snprintf(label, sizeof(label), "%.2f MHz", freq / 1e6); + std::snprintf(label, sizeof(label), "%.2fM", freq / 1e6); else if (std::abs(freq) >= 1e3) - std::snprintf(label, sizeof(label), "%.1f kHz", freq / 1e3); + std::snprintf(label, sizeof(label), "%.1fk", freq / 1e3); else - std::snprintf(label, sizeof(label), "%.0f Hz", freq); + std::snprintf(label, sizeof(label), "%.0f", freq); dl->AddText({x + 2, posY + sizeY - 14}, textCol, label); } } @@ -131,8 +161,9 @@ void SpectrumDisplay::draw(const std::vector>& spectra, ? styles[ch] : styles.back(); - buildPolyline(spectra[ch], minDB, maxDB, freqMin, freqMax, - isIQ, freqScale, posX, posY, sizeX, sizeY, points); + buildPolyline(spectra[ch], minDB, maxDB, + isIQ, freqScale, posX, posY, sizeX, sizeY, + viewLo, viewHi, points); // Fill if (fillSpectrum && points.size() >= 2) { @@ -151,6 +182,26 @@ void SpectrumDisplay::draw(const std::vector>& spectra, st.lineColor, ImDrawFlags_None, 1.5f); } + // Peak hold traces (drawn as dashed-style thin lines above the live spectrum). + if (peakHoldEnable && !peakHold_.empty()) { + for (int ch = 0; ch < nCh && ch < static_cast(peakHold_.size()); ++ch) { + if (peakHold_[ch].empty()) continue; + const ChannelStyle& st = (ch < static_cast(styles.size())) + ? styles[ch] : styles.back(); + + // Use the same line color but dimmer. + ImU32 col = (st.lineColor & 0x00FFFFFF) | 0x90000000; + + buildPolyline(peakHold_[ch], minDB, maxDB, + isIQ, freqScale, posX, posY, sizeX, sizeY, + viewLo, viewHi, points); + + if (points.size() >= 2) + dl->AddPolyline(points.data(), static_cast(points.size()), + col, ImDrawFlags_None, 1.0f); + } + } + // Border dl->AddRect({posX, posY}, {posX + sizeX, posY + sizeY}, IM_COL32(100, 100, 120, 200)); @@ -172,34 +223,33 @@ void SpectrumDisplay::draw(const std::vector& spectrumDB, double SpectrumDisplay::screenXToFreq(float screenX, float posX, float sizeX, double sampleRate, bool isIQ, - FreqScale freqScale) const { - float frac = (screenX - posX) / sizeX; - frac = std::clamp(frac, 0.0f, 1.0f); + FreqScale freqScale, + float viewLo, float viewHi) const { + float screenFrac = std::clamp((screenX - posX) / sizeX, 0.0f, 1.0f); + // Map screen fraction to view fraction + float viewFrac = viewLo + screenFrac * (viewHi - viewLo); + // Map view fraction to bin fraction (undo log if needed) + float binFrac = screenFracToBinFrac(viewFrac, freqScale, isIQ); double freqMin = isIQ ? -sampleRate / 2.0 : 0.0; double freqMax = isIQ ? sampleRate / 2.0 : sampleRate / 2.0; - - if (freqScale == FreqScale::Logarithmic && !isIQ) { - double logMin = std::log10(std::max(freqMin, 1.0)); - double logMax = std::log10(freqMax); - return std::pow(10.0, logMin + frac * (logMax - logMin)); - } - return freqMin + frac * (freqMax - freqMin); + return freqMin + binFrac * (freqMax - freqMin); } float SpectrumDisplay::freqToScreenX(double freq, float posX, float sizeX, double sampleRate, bool isIQ, - FreqScale freqScale) const { + FreqScale freqScale, + float viewLo, float viewHi) const { double freqMin = isIQ ? -sampleRate / 2.0 : 0.0; double freqMax = isIQ ? sampleRate / 2.0 : sampleRate / 2.0; - float frac; - if (freqScale == FreqScale::Logarithmic && !isIQ) { - frac = freqToLogFrac(freq, std::max(freqMin, 1.0), freqMax); - } else { - frac = static_cast((freq - freqMin) / (freqMax - freqMin)); - } - return posX + frac * sizeX; + // Freq → bin fraction + float binFrac = static_cast((freq - freqMin) / (freqMax - freqMin)); + // Bin fraction → full-range screen fraction (apply log inverse) + float viewFrac = binFracToScreenFrac(binFrac, freqScale, isIQ); + // View fraction → screen fraction + float screenFrac = (viewFrac - viewLo) / (viewHi - viewLo); + return posX + screenFrac * sizeX; } float SpectrumDisplay::screenYToDB(float screenY, float posY, float sizeY, @@ -209,4 +259,41 @@ float SpectrumDisplay::screenYToDB(float screenY, float posY, float sizeY, return minDB + frac * (maxDB - minDB); } +void SpectrumDisplay::updatePeakHold(const std::vector>& spectra) { + if (!peakHoldEnable) return; + + int nCh = static_cast(spectra.size()); + + // Grow/shrink channel count. + if (static_cast(peakHold_.size()) != nCh) { + peakHold_.resize(nCh); + } + + for (int ch = 0; ch < nCh; ++ch) { + int bins = static_cast(spectra[ch].size()); + if (bins == 0) continue; + + // Reset if bin count changed. + if (static_cast(peakHold_[ch].size()) != bins) + peakHold_[ch].assign(bins, -200.0f); + + float dt = ImGui::GetIO().DeltaTime; // seconds since last frame + float decayThisFrame = peakHoldDecay * dt; + + for (int i = 0; i < bins; ++i) { + if (spectra[ch][i] >= peakHold_[ch][i]) { + peakHold_[ch][i] = spectra[ch][i]; + } else { + peakHold_[ch][i] -= decayThisFrame; + if (peakHold_[ch][i] < spectra[ch][i]) + peakHold_[ch][i] = spectra[ch][i]; + } + } + } +} + +void SpectrumDisplay::clearPeakHold() { + peakHold_.clear(); +} + } // namespace baudline diff --git a/src/ui/SpectrumDisplay.h b/src/ui/SpectrumDisplay.h index 703b454..4fa3a0b 100644 --- a/src/ui/SpectrumDisplay.h +++ b/src/ui/SpectrumDisplay.h @@ -15,12 +15,14 @@ class SpectrumDisplay { public: // Draw multiple channel spectra overlaid. // `spectra` has one entry per channel; `styles` has matching colors. + // viewLo/viewHi (0–1) control the visible frequency range (zoom/pan). void draw(const std::vector>& spectra, const std::vector& styles, float minDB, float maxDB, double sampleRate, bool isIQ, FreqScale freqScale, - float posX, float posY, float sizeX, float sizeY) const; + float posX, float posY, float sizeX, float sizeY, + float viewLo = 0.0f, float viewHi = 1.0f) const; // Convenience: single-channel draw (backward compat). void draw(const std::vector& spectrumDB, @@ -30,14 +32,26 @@ public: float posX, float posY, float sizeX, float sizeY) const; double screenXToFreq(float screenX, float posX, float sizeX, - double sampleRate, bool isIQ, FreqScale freqScale) const; + double sampleRate, bool isIQ, FreqScale freqScale, + float viewLo = 0.0f, float viewHi = 1.0f) const; float freqToScreenX(double freq, float posX, float sizeX, - double sampleRate, bool isIQ, FreqScale freqScale) const; + double sampleRate, bool isIQ, FreqScale freqScale, + float viewLo = 0.0f, float viewHi = 1.0f) const; float screenYToDB(float screenY, float posY, float sizeY, float minDB, float maxDB) const; - bool showGrid = true; - bool fillSpectrum = false; + // Peak hold: update with current spectra, then draw the held peaks. + void updatePeakHold(const std::vector>& spectra); + void clearPeakHold(); + + bool showGrid = true; + bool fillSpectrum = false; + bool peakHoldEnable = false; + float peakHoldDecay = 20.0f; // dB/second decay rate + +private: + // One peak-hold trace per channel. + mutable std::vector> peakHold_; }; } // namespace baudline diff --git a/src/ui/WaterfallDisplay.cpp b/src/ui/WaterfallDisplay.cpp index b62fb40..152b1d6 100644 --- a/src/ui/WaterfallDisplay.cpp +++ b/src/ui/WaterfallDisplay.cpp @@ -11,12 +11,12 @@ WaterfallDisplay::~WaterfallDisplay() { if (texture_) glDeleteTextures(1, &texture_); } -void WaterfallDisplay::init(int width, int height) { - width_ = width; +void WaterfallDisplay::init(int binCount, int height) { + width_ = binCount; height_ = height; currentRow_ = height_ - 1; - pixelBuf_.resize(width_ * height_ * 3, 0); + pixelBuf_.assign(width_ * height_ * 3, 0); if (texture_) glDeleteTextures(1, &texture_); glGenTextures(1, &texture_); @@ -30,17 +30,9 @@ void WaterfallDisplay::init(int width, int height) { glBindTexture(GL_TEXTURE_2D, 0); } -void WaterfallDisplay::resize(int width, int height) { - if (width == width_ && height == height_) return; - init(width, height); -} - -float WaterfallDisplay::sampleBin(const std::vector& spec, float binF) { - int bins = static_cast(spec.size()); - int b0 = static_cast(binF); - int b1 = std::min(b0 + 1, bins - 1); - float t = binF - b0; - return spec[b0] * (1.0f - t) + spec[b1] * t; +void WaterfallDisplay::resize(int binCount, int height) { + if (binCount == width_ && height == height_) return; + init(binCount, height); } void WaterfallDisplay::advanceRow() { @@ -57,9 +49,9 @@ void WaterfallDisplay::pushLine(const std::vector& spectrumDB, int row = currentRow_; int rowOffset = row * width_ * 3; + // One texel per bin — direct 1:1 mapping. for (int x = 0; x < width_; ++x) { - float frac = static_cast(x) / (width_ - 1); - float dB = sampleBin(spectrumDB, frac * (bins - 1)); + float dB = (x < bins) ? spectrumDB[x] : -200.0f; Color3 c = colorMap_.mapDB(dB, minDB, maxDB); pixelBuf_[rowOffset + x * 3 + 0] = c.r; @@ -85,10 +77,8 @@ void WaterfallDisplay::pushLineMulti( float range = maxDB - minDB; if (range < 1.0f) range = 1.0f; + // One texel per bin — direct 1:1 mapping. for (int x = 0; x < width_; ++x) { - float frac = static_cast(x) / (width_ - 1); - - // Accumulate color contributions from each enabled channel. float accR = 0.0f, accG = 0.0f, accB = 0.0f; for (int ch = 0; ch < nCh; ++ch) { @@ -97,7 +87,7 @@ void WaterfallDisplay::pushLineMulti( if (channelSpectra[ch].empty()) continue; int bins = static_cast(channelSpectra[ch].size()); - float dB = sampleBin(channelSpectra[ch], frac * (bins - 1)); + float dB = (x < bins) ? channelSpectra[ch][x] : -200.0f; float intensity = std::clamp((dB - minDB) / range, 0.0f, 1.0f); accR += channels[ch].r * intensity; diff --git a/src/ui/WaterfallDisplay.h b/src/ui/WaterfallDisplay.h index b2b8c3e..fff9a74 100644 --- a/src/ui/WaterfallDisplay.h +++ b/src/ui/WaterfallDisplay.h @@ -8,9 +8,8 @@ namespace baudline { -// Per-channel color + enable flag for multi-channel waterfall mode. struct WaterfallChannelInfo { - float r, g, b; // channel color [0,1] + float r, g, b; bool enabled; }; @@ -19,14 +18,14 @@ public: WaterfallDisplay(); ~WaterfallDisplay(); - // Initialize OpenGL texture. Call after GL context is ready. - void init(int width, int height); + // Initialize with bin-resolution width and history height. + // width = number of FFT bins (spectrum size), height = history rows. + void init(int binCount, int height); - // Single-channel mode: colormap-based. + // Single-channel colormap mode. One texel per bin — no frequency remapping. void pushLine(const std::vector& spectrumDB, float minDB, float maxDB); - // Multi-channel overlay mode: each channel is rendered in its own color, - // intensity proportional to signal level. Colors are additively blended. + // Multi-channel overlay mode. One texel per bin. void pushLineMulti(const std::vector>& channelSpectra, const std::vector& channels, float minDB, float maxDB); @@ -36,22 +35,13 @@ public: int height() const { return height_; } int currentRow() const { return currentRow_; } - void resize(int width, int height); - + void resize(int binCount, int height); void setColorMap(const ColorMap& cm) { colorMap_ = cm; } - float zoomX = 1.0f; - float zoomY = 1.0f; - float scrollX = 0.0f; - float scrollY = 0.0f; - private: void uploadRow(int row); void advanceRow(); - // Interpolate a dB value at a fractional bin position. - static float sampleBin(const std::vector& spec, float binF); - GLuint texture_ = 0; int width_ = 0; int height_ = 0;