commit no. 7

This commit is contained in:
2026-03-25 19:48:24 +01:00
parent cf397eaa2d
commit 7b9a87fbc0
8 changed files with 628 additions and 274 deletions

View File

@@ -80,7 +80,10 @@ bool Application::init(int argc, char** argv) {
// Enumerate audio devices
paDevices_ = PortAudioSource::listInputDevices();
// Default settings
// Load saved config (overwrites defaults for FFT size, overlap, window, etc.)
loadConfig();
// Apply loaded settings
settings_.fftSize = kFFTSizes[fftSizeIdx_];
settings_.overlap = overlapPct_ / 100.0f;
settings_.window = static_cast<WindowType>(windowIdx_);
@@ -243,22 +246,94 @@ void Application::render() {
// Menu bar
if (ImGui::BeginMenuBar()) {
// Sidebar toggle (leftmost)
if (ImGui::Button(showSidebar_ ? " << " : " >> ")) {
showSidebar_ = !showSidebar_;
saveConfig();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(showSidebar_ ? "Hide sidebar" : "Show sidebar");
ImGui::Separator();
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Open WAV...")) {
// TODO: file dialog integration
// ── File input ──
static char filePathBuf[512] = "";
if (filePath_.size() < sizeof(filePathBuf))
std::strncpy(filePathBuf, filePath_.c_str(), sizeof(filePathBuf) - 1);
ImGui::SetNextItemWidth(200);
if (ImGui::InputText("Path", filePathBuf, sizeof(filePathBuf)))
filePath_ = filePathBuf;
const char* formatNames[] = {"Float32 I/Q", "Int16 I/Q", "Uint8 I/Q", "WAV"};
ImGui::SetNextItemWidth(140);
ImGui::Combo("Format", &fileFormatIdx_, formatNames, 4);
ImGui::SetNextItemWidth(140);
ImGui::DragFloat("Sample Rate", &fileSampleRate_, 1000.0f, 1000.0f, 100e6f, "%.0f Hz");
ImGui::Checkbox("Loop", &fileLoop_);
if (ImGui::MenuItem("Open File")) {
InputFormat fmt;
switch (fileFormatIdx_) {
case 0: fmt = InputFormat::Float32IQ; break;
case 1: fmt = InputFormat::Int16IQ; break;
case 2: fmt = InputFormat::Uint8IQ; break;
default: fmt = InputFormat::WAV; break;
}
openFile(filePath_, fmt, fileSampleRate_);
updateAnalyzerSettings();
}
ImGui::Separator();
// ── Audio device ──
if (!paDevices_.empty()) {
ImGui::Text("Audio Device");
std::vector<const char*> devNames;
for (auto& d : paDevices_) devNames.push_back(d.name.c_str());
ImGui::SetNextItemWidth(250);
if (ImGui::Combo("##device", &paDeviceIdx_, devNames.data(),
static_cast<int>(devNames.size()))) {
openPortAudio();
updateAnalyzerSettings();
saveConfig();
}
}
if (ImGui::MenuItem("Open PortAudio")) {
openPortAudio();
updateAnalyzerSettings();
}
ImGui::Separator();
if (ImGui::MenuItem("Quit", "Esc")) running_ = false;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Grid", nullptr, &specDisplay_.showGrid);
ImGui::MenuItem("Fill Spectrum", nullptr, &specDisplay_.fillSpectrum);
ImGui::Separator();
// Frequency scale
int fs = static_cast<int>(freqScale_);
const char* fsNames[] = {"Linear", "Logarithmic"};
ImGui::SetNextItemWidth(120);
if (ImGui::Combo("Freq Scale", &fs, fsNames, 2)) {
freqScale_ = static_cast<FreqScale>(fs);
saveConfig();
}
ImGui::Separator();
if (ImGui::MenuItem("VSync", nullptr, &vsync_)) {
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
saveConfig();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Debug")) {
ImGui::MenuItem("Metrics/Debugger", nullptr, &showMetricsWindow_);
ImGui::MenuItem("Debug Log", nullptr, &showDebugLog_);
@@ -269,20 +344,36 @@ void Application::render() {
1000.0f / ImGui::GetIO().Framerate);
ImGui::EndMenu();
}
// Right-aligned status in menu bar
{
float barW = ImGui::GetWindowWidth();
char statusBuf[128];
std::snprintf(statusBuf, sizeof(statusBuf), "%.0f Hz | %d pt | %.1f Hz/bin | %.0f FPS",
settings_.sampleRate, settings_.fftSize,
settings_.sampleRate / settings_.fftSize,
ImGui::GetIO().Framerate);
ImVec2 textSz = ImGui::CalcTextSize(statusBuf);
ImGui::SameLine(barW - textSz.x - 16);
ImGui::TextDisabled("%s", statusBuf);
}
ImGui::EndMenuBar();
}
// Layout: controls on left (250px), spectrum+waterfall on right
float controlW = 260.0f;
float contentW = ImGui::GetContentRegionAvail().x - controlW - 8;
// Layout
float totalW = ImGui::GetContentRegionAvail().x;
float contentH = ImGui::GetContentRegionAvail().y;
float controlW = showSidebar_ ? 270.0f : 0.0f;
float contentW = totalW - (showSidebar_ ? controlW + 8 : 0);
// Control panel
ImGui::BeginChild("Controls", {controlW, contentH}, true);
renderControlPanel();
ImGui::EndChild();
ImGui::SameLine();
// Control panel (sidebar)
if (showSidebar_) {
ImGui::BeginChild("Controls", {controlW, contentH}, true);
renderControlPanel();
ImGui::EndChild();
ImGui::SameLine();
}
// Spectrum + Waterfall with draggable splitter
ImGui::BeginChild("Display", {contentW, contentH}, false);
@@ -306,6 +397,10 @@ void Application::render() {
float dy = ImGui::GetIO().MouseDelta.y;
spectrumFrac_ += dy / contentH;
spectrumFrac_ = std::clamp(spectrumFrac_, 0.1f, 0.9f);
draggingSplit_ = true;
} else if (draggingSplit_) {
draggingSplit_ = false;
saveConfig();
}
// Draw splitter line
@@ -369,239 +464,197 @@ void Application::render() {
}
void Application::renderControlPanel() {
ImGui::TextColored({0.4f, 0.8f, 1.0f, 1.0f}, "BAUDLINE");
ImGui::Separator();
// Input source
ImGui::Text("Input Source");
if (ImGui::Button("PortAudio (Mic)")) {
openPortAudio();
updateAnalyzerSettings();
}
ImGui::Separator();
ImGui::Text("File Input");
// Show file path input
static char filePathBuf[512] = "";
if (filePath_.size() < sizeof(filePathBuf))
std::strncpy(filePathBuf, filePath_.c_str(), sizeof(filePathBuf) - 1);
if (ImGui::InputText("Path", filePathBuf, sizeof(filePathBuf)))
filePath_ = filePathBuf;
const char* formatNames[] = {"Float32 I/Q", "Int16 I/Q", "Uint8 I/Q", "WAV"};
ImGui::Combo("Format", &fileFormatIdx_, formatNames, 4);
ImGui::DragFloat("Sample Rate", &fileSampleRate_, 1000.0f, 1000.0f, 100e6f, "%.0f Hz");
ImGui::Checkbox("Loop", &fileLoop_);
if (ImGui::Button("Open File")) {
InputFormat fmt;
switch (fileFormatIdx_) {
case 0: fmt = InputFormat::Float32IQ; break;
case 1: fmt = InputFormat::Int16IQ; break;
case 2: fmt = InputFormat::Uint8IQ; break;
default: fmt = InputFormat::WAV; break;
}
openFile(filePath_, fmt, fileSampleRate_);
updateAnalyzerSettings();
}
// PortAudio device list
if (!paDevices_.empty()) {
ImGui::Separator();
ImGui::Text("Audio Device");
std::vector<const char*> devNames;
for (auto& d : paDevices_) devNames.push_back(d.name.c_str());
if (ImGui::Combo("Device", &paDeviceIdx_, devNames.data(),
static_cast<int>(devNames.size()))) {
openPortAudio();
updateAnalyzerSettings();
}
}
ImGui::Separator();
ImGui::Text("FFT Settings");
// FFT size
{
const char* sizeNames[] = {"256", "512", "1024", "2048", "4096",
"8192", "16384", "32768", "65536"};
if (ImGui::Combo("FFT Size", &fftSizeIdx_, sizeNames, kNumFFTSizes)) {
settings_.fftSize = kFFTSizes[fftSizeIdx_];
updateAnalyzerSettings();
}
}
// Overlap — inverted x⁴ curve: sensitive at the high end (90%+).
{
int hopSamples = static_cast<int>(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
{
const char* winNames[] = {"Rectangular", "Hann", "Hamming", "Blackman",
"Blackman-Harris", "Kaiser", "Flat Top"};
if (ImGui::Combo("Window", &windowIdx_, winNames,
static_cast<int>(WindowType::Count))) {
settings_.window = static_cast<WindowType>(windowIdx_);
if (settings_.window == WindowType::Kaiser) {
// Show Kaiser beta slider
}
updateAnalyzerSettings();
}
}
if (settings_.window == WindowType::Kaiser) {
if (ImGui::SliderFloat("Kaiser Beta", &settings_.kaiserBeta, 0.0f, 20.0f)) {
updateAnalyzerSettings();
}
}
ImGui::Separator();
ImGui::Text("Display");
// Color map
{
const char* cmNames[] = {"Magma", "Viridis", "Inferno", "Plasma", "Grayscale"};
if (ImGui::Combo("Color Map", &colorMapIdx_, cmNames,
static_cast<int>(ColorMapType::Count))) {
colorMap_.setType(static_cast<ColorMapType>(colorMapIdx_));
waterfall_.setColorMap(colorMap_);
}
}
// Frequency scale
{
int fs = static_cast<int>(freqScale_);
const char* fsNames[] = {"Linear", "Logarithmic"};
if (ImGui::Combo("Freq Scale", &fs, fsNames, 2))
freqScale_ = static_cast<FreqScale>(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) {
ImGui::Separator();
ImGui::Text("Channels (%d)", nCh);
static const char* defaultNames[] = {
"Left", "Right", "Ch 3", "Ch 4", "Ch 5", "Ch 6", "Ch 7", "Ch 8"
};
for (int ch = 0; ch < nCh && ch < kMaxChannels; ++ch) {
ImGui::PushID(ch);
ImGui::Checkbox("##en", &channelEnabled_[ch]);
ImGui::SameLine();
ImGui::ColorEdit3(defaultNames[ch], &channelColors_[ch].x,
ImGuiColorEditFlags_NoInputs);
ImGui::PopID();
}
// Waterfall mode
ImGui::Checkbox("Multi-Ch Waterfall", &waterfallMultiCh_);
if (!waterfallMultiCh_) {
if (ImGui::SliderInt("Waterfall Ch", &waterfallChannel_, 0, nCh - 1))
waterfallChannel_ = std::clamp(waterfallChannel_, 0, nCh - 1);
}
}
// Math channels section (always shown).
ImGui::Separator();
renderMathPanel();
ImGui::Separator();
// Playback controls
if (ImGui::Button(paused_ ? "Resume [Space]" : "Pause [Space]"))
// ── Playback ──
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3.0f;
if (ImGui::Button(paused_ ? "Resume" : "Pause", {btnW, 0}))
paused_ = !paused_;
ImGui::SameLine();
if (ImGui::Button("Clear")) {
if (ImGui::Button("Clear", {btnW, 0}))
analyzer_.clearHistory();
}
ImGui::Separator();
// Cursors
cursors_.drawPanel();
ImGui::Separator();
if (ImGui::Button("Snap to Peak [P]")) {
ImGui::SameLine();
if (ImGui::Button("Peak", {btnW, 0})) {
int pkCh = std::clamp(waterfallChannel_, 0, analyzer_.numSpectra() - 1);
cursors_.snapToPeak(analyzer_.channelSpectrum(pkCh),
settings_.sampleRate, settings_.isIQ,
settings_.fftSize);
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Snap cursor A to peak");
// Status
// ── FFT ──
ImGui::Spacing();
if (ImGui::CollapsingHeader("FFT", ImGuiTreeNodeFlags_DefaultOpen)) {
const char* sizeNames[] = {"256", "512", "1024", "2048", "4096",
"8192", "16384", "32768", "65536"};
ImGui::SetNextItemWidth(-1);
if (ImGui::Combo("##fftsize", &fftSizeIdx_, sizeNames, kNumFFTSizes)) {
settings_.fftSize = kFFTSizes[fftSizeIdx_];
updateAnalyzerSettings();
saveConfig();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("FFT Size");
const char* winNames[] = {"Rectangular", "Hann", "Hamming", "Blackman",
"Blackman-Harris", "Kaiser", "Flat Top"};
ImGui::SetNextItemWidth(-1);
if (ImGui::Combo("##window", &windowIdx_, winNames,
static_cast<int>(WindowType::Count))) {
settings_.window = static_cast<WindowType>(windowIdx_);
updateAnalyzerSettings();
saveConfig();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Window Function");
if (settings_.window == WindowType::Kaiser) {
ImGui::SetNextItemWidth(-1);
if (ImGui::SliderFloat("##kaiser", &settings_.kaiserBeta, 0.0f, 20.0f, "Kaiser: %.1f"))
updateAnalyzerSettings();
}
// Overlap
{
int hopSamples = static_cast<int>(settings_.fftSize * (1.0f - settings_.overlap));
if (hopSamples < 1) hopSamples = 1;
int overlapSamples = settings_.fftSize - hopSamples;
ImGui::SetNextItemWidth(-1);
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();
saveConfig();
}
char overlayText[64];
std::snprintf(overlayText, sizeof(overlayText), "%.1f%% (%d samples)", overlapPct_, overlapSamples);
ImVec2 textSize = ImGui::CalcTextSize(overlayText);
ImVec2 rMin = ImGui::GetItemRectMin();
ImVec2 rMax = ImGui::GetItemRectMax();
float tx = rMin.x + ((rMax.x - rMin.x) - textSize.x) * 0.5f;
float ty = rMin.y + ((rMax.y - rMin.y) - textSize.y) * 0.5f;
ImGui::GetForegroundDrawList()->AddText({tx, ty}, IM_COL32(255, 255, 255, 220), overlayText);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Overlap");
}
}
// ── Display ──
ImGui::Spacing();
if (ImGui::CollapsingHeader("Display", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::SetNextItemWidth(-1);
ImGui::DragFloatRange2("##dbrange", &minDB_, &maxDB_, 1.0f, -200.0f, 20.0f,
"Min: %.0f dB", "Max: %.0f dB");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("dB Range (min / max)");
ImGui::Checkbox("Peak Hold", &specDisplay_.peakHoldEnable);
if (specDisplay_.peakHoldEnable) {
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x
- ImGui::CalcTextSize("Clear").x
- ImGui::GetStyle().ItemSpacing.x
- ImGui::GetStyle().FramePadding.x * 2);
ImGui::SliderFloat("##decay", &specDisplay_.peakHoldDecay, 0.0f, 120.0f, "%.0f dB/s");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Decay rate");
ImGui::SameLine();
if (ImGui::SmallButton("Clear##peakhold"))
specDisplay_.clearPeakHold();
}
if (viewLo_ > 0.0f || viewHi_ < 1.0f) {
float zoomPct = 1.0f / (viewHi_ - viewLo_);
ImGui::Text("Zoom: %.1fx", zoomPct);
ImGui::SameLine();
if (ImGui::SmallButton("Reset##zoom")) {
viewLo_ = 0.0f;
viewHi_ = 1.0f;
}
}
}
// ── Channels ──
ImGui::Spacing();
{
int nCh = analyzer_.numSpectra();
bool isMulti = waterfallMultiCh_ && nCh > 1;
// Header with inline Single/Multi toggle
bool headerOpen = ImGui::CollapsingHeader("##channels_hdr",
ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
ImGui::SameLine();
ImGui::Text("Channels");
if (nCh > 1) {
ImGui::SameLine();
float btnW = 60.0f;
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - btnW);
if (ImGui::Button(isMulti ? " Multi " : "Single ", {btnW, 0})) {
waterfallMultiCh_ = !waterfallMultiCh_;
}
}
if (headerOpen) {
if (isMulti) {
// Multi-channel: per-channel colors and enable
static const char* defaultNames[] = {
"Left", "Right", "Ch 3", "Ch 4", "Ch 5", "Ch 6", "Ch 7", "Ch 8"
};
for (int ch = 0; ch < nCh && ch < kMaxChannels; ++ch) {
ImGui::PushID(ch);
ImGui::Checkbox("##en", &channelEnabled_[ch]);
ImGui::SameLine();
ImGui::ColorEdit3(defaultNames[ch], &channelColors_[ch].x,
ImGuiColorEditFlags_NoInputs);
ImGui::PopID();
}
} else {
// Single-channel: color map + channel selector
const char* cmNames[] = {"Magma", "Viridis", "Inferno", "Plasma", "Grayscale"};
ImGui::SetNextItemWidth(-1);
if (ImGui::Combo("##colormap", &colorMapIdx_, cmNames,
static_cast<int>(ColorMapType::Count))) {
colorMap_.setType(static_cast<ColorMapType>(colorMapIdx_));
waterfall_.setColorMap(colorMap_);
saveConfig();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Color Map");
if (nCh > 1) {
ImGui::SetNextItemWidth(-1);
if (ImGui::SliderInt("##wfch", &waterfallChannel_, 0, nCh - 1))
waterfallChannel_ = std::clamp(waterfallChannel_, 0, nCh - 1);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Waterfall Channel");
}
}
}
}
// ── Math ──
ImGui::Spacing();
if (ImGui::CollapsingHeader("Math")) {
renderMathPanel();
}
// ── Cursors ──
ImGui::Spacing();
if (ImGui::CollapsingHeader("Cursors", ImGuiTreeNodeFlags_DefaultOpen)) {
cursors_.drawPanel();
}
// ── Status (bottom) ──
ImGui::Separator();
ImGui::Text("FFT: %d pt, %.1f Hz/bin",
settings_.fftSize,
settings_.sampleRate / settings_.fftSize);
ImGui::Text("Sample Rate: %.0f Hz", settings_.sampleRate);
ImGui::Text("Mode: %s", settings_.isIQ ? "I/Q (Complex)"
: (settings_.numChannels > 1 ? "Multi-channel Real" : "Real"));
ImGui::TextDisabled("Mode: %s", settings_.isIQ ? "I/Q"
: (settings_.numChannels > 1 ? "Multi-ch" : "Real"));
int pkCh2 = std::clamp(waterfallChannel_, 0, analyzer_.numSpectra() - 1);
auto [peakBin, peakDB] = analyzer_.findPeak(pkCh2);
double peakFreq = analyzer_.binToFreq(peakBin);
if (std::abs(peakFreq) >= 1e6)
ImGui::Text("Peak: %.6f MHz, %.1f dB", peakFreq / 1e6, peakDB);
ImGui::TextDisabled("Peak: %.6f MHz, %.1f dB", peakFreq / 1e6, peakDB);
else if (std::abs(peakFreq) >= 1e3)
ImGui::Text("Peak: %.3f kHz, %.1f dB", peakFreq / 1e3, peakDB);
ImGui::TextDisabled("Peak: %.3f kHz, %.1f dB", peakFreq / 1e3, peakDB);
else
ImGui::Text("Peak: %.1f Hz, %.1f dB", peakFreq, peakDB);
ImGui::TextDisabled("Peak: %.1f Hz, %.1f dB", peakFreq, peakDB);
}
void Application::renderSpectrumPanel() {
@@ -1129,9 +1182,6 @@ void Application::computeMathChannels() {
}
void Application::renderMathPanel() {
ImGui::Text("Channel Math");
ImGui::Separator();
int nPhys = analyzer_.numSpectra();
// Build source channel name list.
@@ -1197,4 +1247,66 @@ void Application::renderMathPanel() {
}
}
void Application::loadConfig() {
config_.load();
fftSizeIdx_ = config_.getInt("fft_size_idx", fftSizeIdx_);
overlapPct_ = config_.getFloat("overlap_pct", overlapPct_);
windowIdx_ = config_.getInt("window_idx", windowIdx_);
colorMapIdx_ = config_.getInt("colormap_idx", colorMapIdx_);
minDB_ = config_.getFloat("min_db", minDB_);
maxDB_ = config_.getFloat("max_db", maxDB_);
int fs = config_.getInt("freq_scale", static_cast<int>(freqScale_));
freqScale_ = static_cast<FreqScale>(fs);
vsync_ = config_.getBool("vsync", vsync_);
spectrumFrac_ = config_.getFloat("spectrum_frac", spectrumFrac_);
showSidebar_ = config_.getBool("show_sidebar", showSidebar_);
specDisplay_.peakHoldEnable = config_.getBool("peak_hold", specDisplay_.peakHoldEnable);
specDisplay_.peakHoldDecay = config_.getFloat("peak_hold_decay", specDisplay_.peakHoldDecay);
// Clamp
fftSizeIdx_ = std::clamp(fftSizeIdx_, 0, kNumFFTSizes - 1);
windowIdx_ = std::clamp(windowIdx_, 0, static_cast<int>(WindowType::Count) - 1);
colorMapIdx_ = std::clamp(colorMapIdx_, 0, static_cast<int>(ColorMapType::Count) - 1);
spectrumFrac_ = std::clamp(spectrumFrac_, 0.1f, 0.9f);
// Find device by saved name.
std::string devName = config_.getString("device_name", "");
if (!devName.empty()) {
for (int i = 0; i < static_cast<int>(paDevices_.size()); ++i) {
if (paDevices_[i].name == devName) {
paDeviceIdx_ = i;
break;
}
}
}
// Apply
settings_.fftSize = kFFTSizes[fftSizeIdx_];
settings_.overlap = overlapPct_ / 100.0f;
settings_.window = static_cast<WindowType>(windowIdx_);
colorMap_.setType(static_cast<ColorMapType>(colorMapIdx_));
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
}
void Application::saveConfig() const {
Config cfg;
cfg.setInt("fft_size_idx", fftSizeIdx_);
cfg.setFloat("overlap_pct", overlapPct_);
cfg.setInt("window_idx", windowIdx_);
cfg.setInt("colormap_idx", colorMapIdx_);
cfg.setFloat("min_db", minDB_);
cfg.setFloat("max_db", maxDB_);
cfg.setInt("freq_scale", static_cast<int>(freqScale_));
cfg.setBool("vsync", vsync_);
cfg.setFloat("spectrum_frac", spectrumFrac_);
cfg.setBool("show_sidebar", showSidebar_);
cfg.setBool("peak_hold", specDisplay_.peakHoldEnable);
cfg.setFloat("peak_hold_decay", specDisplay_.peakHoldDecay);
if (paDeviceIdx_ >= 0 && paDeviceIdx_ < static_cast<int>(paDevices_.size()))
cfg.setString("device_name", paDevices_[paDeviceIdx_].name);
cfg.save();
}
} // namespace baudline