split off AudioEngine
This commit is contained in:
340
src/audio/AudioEngine.cpp
Normal file
340
src/audio/AudioEngine.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
#include "audio/AudioEngine.h"
|
||||
#include "audio/FileSource.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace baudmine {
|
||||
|
||||
AudioEngine::AudioEngine() = default;
|
||||
|
||||
// ── Device enumeration ───────────────────────────────────────────────────────
|
||||
|
||||
void AudioEngine::enumerateDevices() {
|
||||
devices_ = MiniAudioSource::listInputDevices();
|
||||
}
|
||||
|
||||
void AudioEngine::clearDeviceSelections() {
|
||||
std::memset(deviceSelected_, 0, sizeof(deviceSelected_));
|
||||
}
|
||||
|
||||
// ── Source management ────────────────────────────────────────────────────────
|
||||
|
||||
void AudioEngine::closeAll() {
|
||||
if (audioSource_) audioSource_->close();
|
||||
audioSource_.reset();
|
||||
extraDevices_.clear();
|
||||
}
|
||||
|
||||
void AudioEngine::openDevice(int deviceListIdx) {
|
||||
closeAll();
|
||||
|
||||
int deviceIdx = -1;
|
||||
double sr = 48000.0;
|
||||
if (deviceListIdx >= 0 && deviceListIdx < static_cast<int>(devices_.size())) {
|
||||
deviceIdx = devices_[deviceListIdx].index;
|
||||
sr = devices_[deviceListIdx].defaultSampleRate;
|
||||
}
|
||||
|
||||
int reqCh = 2;
|
||||
if (deviceListIdx >= 0 && deviceListIdx < static_cast<int>(devices_.size()))
|
||||
reqCh = std::min(devices_[deviceListIdx].maxInputChannels, kMaxChannels);
|
||||
if (reqCh < 1) reqCh = 1;
|
||||
|
||||
auto src = std::make_unique<MiniAudioSource>(sr, reqCh, deviceIdx);
|
||||
if (src->open()) {
|
||||
audioSource_ = std::move(src);
|
||||
settings_.sampleRate = audioSource_->sampleRate();
|
||||
settings_.isIQ = false;
|
||||
settings_.numChannels = audioSource_->channels();
|
||||
} else {
|
||||
std::fprintf(stderr, "Failed to open audio device\n");
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::openMultiDevice(const bool selected[], int maxDevs) {
|
||||
closeAll();
|
||||
|
||||
std::vector<int> sel;
|
||||
for (int i = 0; i < maxDevs; ++i)
|
||||
if (selected[i]) sel.push_back(i);
|
||||
if (sel.empty()) return;
|
||||
|
||||
// First selected device becomes the primary source.
|
||||
{
|
||||
int idx = sel[0];
|
||||
double sr = devices_[idx].defaultSampleRate;
|
||||
int reqCh = std::min(devices_[idx].maxInputChannels, kMaxChannels);
|
||||
if (reqCh < 1) reqCh = 1;
|
||||
auto src = std::make_unique<MiniAudioSource>(sr, reqCh, devices_[idx].index);
|
||||
if (src->open()) {
|
||||
audioSource_ = std::move(src);
|
||||
settings_.sampleRate = audioSource_->sampleRate();
|
||||
settings_.isIQ = false;
|
||||
settings_.numChannels = audioSource_->channels();
|
||||
} else {
|
||||
std::fprintf(stderr, "Failed to open primary device %s\n",
|
||||
devices_[idx].name.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining selected devices become extra sources.
|
||||
int totalCh = settings_.numChannels;
|
||||
for (size_t s = 1; s < sel.size() && totalCh < kMaxChannels; ++s) {
|
||||
int idx = sel[s];
|
||||
double sr = devices_[idx].defaultSampleRate;
|
||||
int reqCh = std::min(devices_[idx].maxInputChannels, kMaxChannels - totalCh);
|
||||
if (reqCh < 1) reqCh = 1;
|
||||
auto src = std::make_unique<MiniAudioSource>(sr, reqCh, devices_[idx].index);
|
||||
if (src->open()) {
|
||||
auto ed = std::make_unique<ExtraDevice>();
|
||||
ed->source = std::move(src);
|
||||
AnalyzerSettings es = settings_;
|
||||
es.sampleRate = ed->source->sampleRate();
|
||||
es.numChannels = ed->source->channels();
|
||||
es.isIQ = false;
|
||||
ed->analyzer.configure(es);
|
||||
totalCh += ed->source->channels();
|
||||
extraDevices_.push_back(std::move(ed));
|
||||
} else {
|
||||
std::fprintf(stderr, "Failed to open extra device %s\n",
|
||||
devices_[idx].name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::openFile(const std::string& path, InputFormat format,
|
||||
double sampleRate, bool loop) {
|
||||
closeAll();
|
||||
|
||||
bool isIQ = (format != InputFormat::WAV);
|
||||
auto src = std::make_unique<FileSource>(path, format, sampleRate, loop);
|
||||
if (src->open()) {
|
||||
settings_.sampleRate = src->sampleRate();
|
||||
settings_.isIQ = isIQ;
|
||||
settings_.numChannels = isIQ ? 1 : src->channels();
|
||||
audioSource_ = std::move(src);
|
||||
} else {
|
||||
std::fprintf(stderr, "Failed to open file: %s\n", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Analyzer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void AudioEngine::configure(const AnalyzerSettings& s) {
|
||||
settings_ = s;
|
||||
analyzer_.configure(settings_);
|
||||
|
||||
for (auto& ed : extraDevices_) {
|
||||
AnalyzerSettings es = settings_;
|
||||
es.sampleRate = ed->source->sampleRate();
|
||||
es.numChannels = ed->source->channels();
|
||||
es.isIQ = false;
|
||||
ed->analyzer.configure(es);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::clearHistory() {
|
||||
analyzer_.clearHistory();
|
||||
for (auto& ed : extraDevices_)
|
||||
ed->analyzer.clearHistory();
|
||||
}
|
||||
|
||||
int AudioEngine::processAudio() {
|
||||
if (!audioSource_) return 0;
|
||||
|
||||
int channels = audioSource_->channels();
|
||||
size_t hopFrames = static_cast<size_t>(
|
||||
settings_.fftSize * (1.0f - settings_.overlap));
|
||||
if (hopFrames < 1) hopFrames = 1;
|
||||
audioBuf_.resize(hopFrames * channels);
|
||||
|
||||
constexpr int kMaxSpectraPerFrame = 8;
|
||||
|
||||
// Process primary source.
|
||||
int spectraThisFrame = 0;
|
||||
while (spectraThisFrame < kMaxSpectraPerFrame) {
|
||||
size_t framesRead = audioSource_->read(audioBuf_.data(), hopFrames);
|
||||
if (framesRead == 0) break;
|
||||
analyzer_.pushSamples(audioBuf_.data(), framesRead);
|
||||
if (analyzer_.hasNewSpectrum())
|
||||
++spectraThisFrame;
|
||||
}
|
||||
|
||||
// Process extra devices independently.
|
||||
for (auto& ed : extraDevices_) {
|
||||
int edCh = ed->source->channels();
|
||||
const auto& edSettings = ed->analyzer.settings();
|
||||
size_t edHop = static_cast<size_t>(edSettings.fftSize * (1.0f - edSettings.overlap));
|
||||
if (edHop < 1) edHop = 1;
|
||||
ed->audioBuf.resize(edHop * edCh);
|
||||
|
||||
int edSpectra = 0;
|
||||
while (edSpectra < kMaxSpectraPerFrame) {
|
||||
size_t framesRead = ed->source->read(ed->audioBuf.data(), edHop);
|
||||
if (framesRead == 0) break;
|
||||
ed->analyzer.pushSamples(ed->audioBuf.data(), framesRead);
|
||||
if (ed->analyzer.hasNewSpectrum())
|
||||
++edSpectra;
|
||||
}
|
||||
}
|
||||
|
||||
return spectraThisFrame;
|
||||
}
|
||||
|
||||
void AudioEngine::drainSources() {
|
||||
if (audioSource_ && audioSource_->isRealTime()) {
|
||||
int channels = audioSource_->channels();
|
||||
std::vector<float> drain(4096 * channels);
|
||||
while (audioSource_->read(drain.data(), 4096) > 0) {}
|
||||
}
|
||||
for (auto& ed : extraDevices_) {
|
||||
if (ed->source && ed->source->isRealTime()) {
|
||||
int ch = ed->source->channels();
|
||||
std::vector<float> drain(4096 * ch);
|
||||
while (ed->source->read(drain.data(), 4096) > 0) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unified channel view ─────────────────────────────────────────────────────
|
||||
|
||||
int AudioEngine::totalNumSpectra() const {
|
||||
int n = analyzer_.numSpectra();
|
||||
for (auto& ed : extraDevices_)
|
||||
n += ed->analyzer.numSpectra();
|
||||
return n;
|
||||
}
|
||||
|
||||
const std::vector<float>& AudioEngine::getSpectrum(int globalCh) const {
|
||||
int n = analyzer_.numSpectra();
|
||||
if (globalCh < n) return analyzer_.channelSpectrum(globalCh);
|
||||
globalCh -= n;
|
||||
for (auto& ed : extraDevices_) {
|
||||
int en = ed->analyzer.numSpectra();
|
||||
if (globalCh < en) return ed->analyzer.channelSpectrum(globalCh);
|
||||
globalCh -= en;
|
||||
}
|
||||
return analyzer_.channelSpectrum(0);
|
||||
}
|
||||
|
||||
const std::vector<std::complex<float>>& AudioEngine::getComplex(int globalCh) const {
|
||||
int n = analyzer_.numSpectra();
|
||||
if (globalCh < n) return analyzer_.channelComplex(globalCh);
|
||||
globalCh -= n;
|
||||
for (auto& ed : extraDevices_) {
|
||||
int en = ed->analyzer.numSpectra();
|
||||
if (globalCh < en) return ed->analyzer.channelComplex(globalCh);
|
||||
globalCh -= en;
|
||||
}
|
||||
return analyzer_.channelComplex(0);
|
||||
}
|
||||
|
||||
const char* AudioEngine::getDeviceName(int globalCh) const {
|
||||
int n = analyzer_.numSpectra();
|
||||
if (globalCh < n) {
|
||||
if (deviceIdx_ >= 0 && deviceIdx_ < static_cast<int>(devices_.size()))
|
||||
return devices_[deviceIdx_].name.c_str();
|
||||
for (int i = 0; i < static_cast<int>(devices_.size()); ++i)
|
||||
if (deviceSelected_[i]) return devices_[i].name.c_str();
|
||||
return "Audio Device";
|
||||
}
|
||||
globalCh -= n;
|
||||
int devSel = 0;
|
||||
for (int i = 0; i < static_cast<int>(devices_.size()) && i < kMaxChannels; ++i) {
|
||||
if (!deviceSelected_[i]) continue;
|
||||
++devSel;
|
||||
if (devSel <= 1) continue;
|
||||
int edIdx = devSel - 2;
|
||||
if (edIdx < static_cast<int>(extraDevices_.size())) {
|
||||
int en = extraDevices_[edIdx]->analyzer.numSpectra();
|
||||
if (globalCh < en) return devices_[i].name.c_str();
|
||||
globalCh -= en;
|
||||
}
|
||||
}
|
||||
return "Audio Device";
|
||||
}
|
||||
|
||||
// ── Math channels ────────────────────────────────────────────────────────────
|
||||
|
||||
void AudioEngine::computeMathChannels() {
|
||||
int nPhys = totalNumSpectra();
|
||||
int specSz = analyzer_.spectrumSize();
|
||||
mathSpectra_.resize(mathChannels_.size());
|
||||
|
||||
for (size_t mi = 0; mi < mathChannels_.size(); ++mi) {
|
||||
const auto& mc = mathChannels_[mi];
|
||||
auto& out = mathSpectra_[mi];
|
||||
out.resize(specSz);
|
||||
|
||||
if (!mc.enabled) {
|
||||
std::fill(out.begin(), out.end(), -200.0f);
|
||||
continue;
|
||||
}
|
||||
|
||||
int sx = std::clamp(mc.sourceX, 0, nPhys - 1);
|
||||
int sy = std::clamp(mc.sourceY, 0, nPhys - 1);
|
||||
const auto& xDB = getSpectrum(sx);
|
||||
const auto& yDB = getSpectrum(sy);
|
||||
const auto& xC = getComplex(sx);
|
||||
const auto& yC = getComplex(sy);
|
||||
|
||||
for (int i = 0; i < specSz; ++i) {
|
||||
float val = -200.0f;
|
||||
switch (mc.op) {
|
||||
case MathOp::Negate: val = -xDB[i]; break;
|
||||
case MathOp::Absolute: val = std::abs(xDB[i]); break;
|
||||
case MathOp::Square: val = 2.0f * xDB[i]; break;
|
||||
case MathOp::Cube: val = 3.0f * xDB[i]; break;
|
||||
case MathOp::Sqrt: val = 0.5f * xDB[i]; break;
|
||||
case MathOp::Log: {
|
||||
float lin = std::pow(10.0f, xDB[i] / 10.0f);
|
||||
val = 10.0f * std::log10(lin + 1e-30f);
|
||||
break;
|
||||
}
|
||||
case MathOp::Add: {
|
||||
float lx = std::pow(10.0f, xDB[i] / 10.0f);
|
||||
float ly = std::pow(10.0f, yDB[i] / 10.0f);
|
||||
float s = lx + ly;
|
||||
val = (s > 1e-20f) ? 10.0f * std::log10(s) : -200.0f;
|
||||
break;
|
||||
}
|
||||
case MathOp::Subtract: {
|
||||
float lx = std::pow(10.0f, xDB[i] / 10.0f);
|
||||
float ly = std::pow(10.0f, yDB[i] / 10.0f);
|
||||
float d = std::abs(lx - ly);
|
||||
val = (d > 1e-20f) ? 10.0f * std::log10(d) : -200.0f;
|
||||
break;
|
||||
}
|
||||
case MathOp::Multiply:
|
||||
val = xDB[i] + yDB[i];
|
||||
break;
|
||||
case MathOp::Phase: {
|
||||
if (i < static_cast<int>(xC.size()) &&
|
||||
i < static_cast<int>(yC.size())) {
|
||||
auto cross = xC[i] * std::conj(yC[i]);
|
||||
val = std::atan2(cross.imag(), cross.real())
|
||||
* (180.0f / 3.14159265f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MathOp::CrossCorr: {
|
||||
if (i < static_cast<int>(xC.size()) &&
|
||||
i < static_cast<int>(yC.size())) {
|
||||
auto cross = xC[i] * std::conj(yC[i]);
|
||||
float mag2 = std::norm(cross);
|
||||
val = (mag2 > 1e-20f) ? 10.0f * std::log10(mag2) : -200.0f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
out[i] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace baudmine
|
||||
93
src/audio/AudioEngine.h
Normal file
93
src/audio/AudioEngine.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Types.h"
|
||||
#include "audio/AudioSource.h"
|
||||
#include "audio/MiniAudioSource.h"
|
||||
#include "dsp/SpectrumAnalyzer.h"
|
||||
|
||||
#include <complex>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace baudmine {
|
||||
|
||||
class AudioEngine {
|
||||
public:
|
||||
AudioEngine();
|
||||
|
||||
// ── Device enumeration ──
|
||||
void enumerateDevices();
|
||||
const std::vector<MiniAudioSource::DeviceInfo>& devices() const { return devices_; }
|
||||
|
||||
// ── Source management ──
|
||||
void openDevice(int deviceListIdx);
|
||||
void openMultiDevice(const bool selected[], int maxDevs);
|
||||
void openFile(const std::string& path, InputFormat format, double sampleRate, bool loop);
|
||||
void closeAll();
|
||||
|
||||
bool hasSource() const { return audioSource_ != nullptr; }
|
||||
AudioSource* source() { return audioSource_.get(); }
|
||||
|
||||
// ── Analyzer ──
|
||||
AnalyzerSettings& settings() { return settings_; }
|
||||
const AnalyzerSettings& settings() const { return settings_; }
|
||||
const SpectrumAnalyzer& primaryAnalyzer() const { return analyzer_; }
|
||||
|
||||
void configure(const AnalyzerSettings& s);
|
||||
int processAudio(); // returns number of new spectra from primary analyzer
|
||||
void clearHistory();
|
||||
void drainSources(); // flush stale audio from all real-time sources
|
||||
|
||||
// ── Unified channel view across all analyzers ──
|
||||
int totalNumSpectra() const;
|
||||
const std::vector<float>& getSpectrum(int globalCh) const;
|
||||
const std::vector<std::complex<float>>& getComplex(int globalCh) const;
|
||||
const char* getDeviceName(int globalCh) const;
|
||||
int spectrumSize() const { return analyzer_.spectrumSize(); }
|
||||
double binToFreq(int bin) const { return analyzer_.binToFreq(bin); }
|
||||
|
||||
// ── Math channels ──
|
||||
std::vector<MathChannel>& mathChannels() { return mathChannels_; }
|
||||
const std::vector<MathChannel>& mathChannels() const { return mathChannels_; }
|
||||
std::vector<std::vector<float>>& mathSpectra() { return mathSpectra_; }
|
||||
const std::vector<std::vector<float>>& mathSpectra() const { return mathSpectra_; }
|
||||
void computeMathChannels();
|
||||
|
||||
// ── Device selection state (for config persistence) ──
|
||||
int deviceIdx() const { return deviceIdx_; }
|
||||
void setDeviceIdx(int i) { deviceIdx_ = i; }
|
||||
bool multiDeviceMode() const { return multiDeviceMode_; }
|
||||
void setMultiDeviceMode(bool m) { multiDeviceMode_ = m; }
|
||||
bool deviceSelected(int i) const { return (i >= 0 && i < kMaxChannels) ? deviceSelected_[i] : false; }
|
||||
void setDeviceSelected(int i, bool v) { if (i >= 0 && i < kMaxChannels) deviceSelected_[i] = v; }
|
||||
void clearDeviceSelections();
|
||||
|
||||
private:
|
||||
struct ExtraDevice {
|
||||
std::unique_ptr<AudioSource> source;
|
||||
SpectrumAnalyzer analyzer;
|
||||
std::vector<float> audioBuf;
|
||||
};
|
||||
|
||||
// Sources
|
||||
std::unique_ptr<AudioSource> audioSource_;
|
||||
std::vector<float> audioBuf_;
|
||||
std::vector<std::unique_ptr<ExtraDevice>> extraDevices_;
|
||||
|
||||
// DSP
|
||||
SpectrumAnalyzer analyzer_;
|
||||
AnalyzerSettings settings_;
|
||||
|
||||
// Math
|
||||
std::vector<MathChannel> mathChannels_;
|
||||
std::vector<std::vector<float>> mathSpectra_;
|
||||
|
||||
// Device state
|
||||
std::vector<MiniAudioSource::DeviceInfo> devices_;
|
||||
int deviceIdx_ = 0;
|
||||
bool multiDeviceMode_ = false;
|
||||
bool deviceSelected_[kMaxChannels] = {};
|
||||
};
|
||||
|
||||
} // namespace baudmine
|
||||
@@ -157,4 +157,53 @@ struct Color3 {
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
// ── Channel math operations ──────────────────────────────────────────────────
|
||||
|
||||
enum class MathOp {
|
||||
// Unary (on channel X)
|
||||
Negate, // -x (negate dB)
|
||||
Absolute, // |x| (absolute value of dB)
|
||||
Square, // x^2 in linear → 2*x_dB
|
||||
Cube, // x^3 in linear → 3*x_dB
|
||||
Sqrt, // sqrt in linear → 0.5*x_dB
|
||||
Log, // 10*log10(10^(x_dB/10) + 1) — compressed scale
|
||||
// Binary (on channels X and Y)
|
||||
Add, // linear(x) + linear(y) → dB
|
||||
Subtract, // |linear(x) - linear(y)| → dB
|
||||
Multiply, // x_dB + y_dB (multiply in linear = add in dB)
|
||||
Phase, // angle(X_cplx * conj(Y_cplx)) in degrees
|
||||
CrossCorr, // |X_cplx * conj(Y_cplx)| → dB
|
||||
Count
|
||||
};
|
||||
|
||||
inline bool mathOpIsBinary(MathOp op) {
|
||||
return op >= MathOp::Add;
|
||||
}
|
||||
|
||||
inline const char* mathOpName(MathOp op) {
|
||||
switch (op) {
|
||||
case MathOp::Negate: return "-x";
|
||||
case MathOp::Absolute: return "|x|";
|
||||
case MathOp::Square: return "x^2";
|
||||
case MathOp::Cube: return "x^3";
|
||||
case MathOp::Sqrt: return "sqrt(x)";
|
||||
case MathOp::Log: return "log(x)";
|
||||
case MathOp::Add: return "x + y";
|
||||
case MathOp::Subtract: return "x - y";
|
||||
case MathOp::Multiply: return "x * y";
|
||||
case MathOp::Phase: return "phase(x,y)";
|
||||
case MathOp::CrossCorr: return "xcorr(x,y)";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
struct MathChannel {
|
||||
MathOp op = MathOp::Subtract;
|
||||
int sourceX = 0;
|
||||
int sourceY = 1;
|
||||
float color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
bool enabled = true;
|
||||
bool waterfall = false;
|
||||
};
|
||||
|
||||
} // namespace baudmine
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,7 @@
|
||||
|
||||
#include "core/Types.h"
|
||||
#include "core/Config.h"
|
||||
#include "dsp/SpectrumAnalyzer.h"
|
||||
#include "audio/AudioSource.h"
|
||||
#include "audio/MiniAudioSource.h"
|
||||
#include "audio/AudioEngine.h"
|
||||
#include "ui/ColorMap.h"
|
||||
#include "ui/WaterfallDisplay.h"
|
||||
#include "ui/SpectrumDisplay.h"
|
||||
@@ -12,62 +10,11 @@
|
||||
#include "ui/Measurements.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include <complex>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace baudmine {
|
||||
|
||||
// ── Channel math operations ──────────────────────────────────────────────────
|
||||
|
||||
enum class MathOp {
|
||||
// Unary (on channel X)
|
||||
Negate, // -x (negate dB)
|
||||
Absolute, // |x| (absolute value of dB)
|
||||
Square, // x^2 in linear → 2*x_dB
|
||||
Cube, // x^3 in linear → 3*x_dB
|
||||
Sqrt, // sqrt in linear → 0.5*x_dB
|
||||
Log, // 10*log10(10^(x_dB/10) + 1) — compressed scale
|
||||
// Binary (on channels X and Y)
|
||||
Add, // linear(x) + linear(y) → dB
|
||||
Subtract, // |linear(x) - linear(y)| → dB
|
||||
Multiply, // x_dB + y_dB (multiply in linear = add in dB)
|
||||
Phase, // angle(X_cplx * conj(Y_cplx)) in degrees
|
||||
CrossCorr, // |X_cplx * conj(Y_cplx)| → dB
|
||||
Count
|
||||
};
|
||||
|
||||
inline bool mathOpIsBinary(MathOp op) {
|
||||
return op >= MathOp::Add;
|
||||
}
|
||||
|
||||
inline const char* mathOpName(MathOp op) {
|
||||
switch (op) {
|
||||
case MathOp::Negate: return "-x";
|
||||
case MathOp::Absolute: return "|x|";
|
||||
case MathOp::Square: return "x^2";
|
||||
case MathOp::Cube: return "x^3";
|
||||
case MathOp::Sqrt: return "sqrt(x)";
|
||||
case MathOp::Log: return "log(x)";
|
||||
case MathOp::Add: return "x + y";
|
||||
case MathOp::Subtract: return "x - y";
|
||||
case MathOp::Multiply: return "x * y";
|
||||
case MathOp::Phase: return "phase(x,y)";
|
||||
case MathOp::CrossCorr: return "xcorr(x,y)";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
struct MathChannel {
|
||||
MathOp op = MathOp::Subtract;
|
||||
int sourceX = 0;
|
||||
int sourceY = 1;
|
||||
ImVec4 color = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
bool enabled = true;
|
||||
bool waterfall = false; // include on waterfall overlay
|
||||
};
|
||||
|
||||
class Application {
|
||||
public:
|
||||
Application();
|
||||
@@ -86,11 +33,10 @@ private:
|
||||
void renderWaterfallPanel();
|
||||
void handleSpectrumInput(float posX, float posY, float sizeX, float sizeY);
|
||||
|
||||
void openPortAudio();
|
||||
void openDevice();
|
||||
void openMultiDevice();
|
||||
void openFile(const std::string& path, InputFormat format, double sampleRate);
|
||||
void updateAnalyzerSettings();
|
||||
void computeMathChannels();
|
||||
void renderMathPanel();
|
||||
|
||||
void loadConfig();
|
||||
@@ -101,28 +47,8 @@ private:
|
||||
SDL_GLContext glContext_ = nullptr;
|
||||
bool running_ = false;
|
||||
|
||||
// Audio
|
||||
std::unique_ptr<AudioSource> audioSource_;
|
||||
std::vector<float> audioBuf_; // temp read buffer
|
||||
|
||||
// Extra devices (multi-device mode): each gets its own source + analyzer.
|
||||
struct ExtraDevice {
|
||||
std::unique_ptr<AudioSource> source;
|
||||
SpectrumAnalyzer analyzer;
|
||||
std::vector<float> audioBuf;
|
||||
};
|
||||
std::vector<std::unique_ptr<ExtraDevice>> extraDevices_;
|
||||
|
||||
// Helpers to present a unified channel view across all analyzers.
|
||||
int totalNumSpectra() const;
|
||||
const std::vector<float>& getSpectrum(int globalCh) const;
|
||||
const std::vector<std::complex<float>>& getComplex(int globalCh) const;
|
||||
// Returns the device name that owns a given global channel index.
|
||||
const char* getDeviceName(int globalCh) const;
|
||||
|
||||
// DSP
|
||||
SpectrumAnalyzer analyzer_;
|
||||
AnalyzerSettings settings_;
|
||||
// Audio engine (owns sources, analyzers, math channels)
|
||||
AudioEngine audio_;
|
||||
|
||||
// UI state
|
||||
ColorMap colorMap_;
|
||||
@@ -145,8 +71,6 @@ private:
|
||||
void applyUIScale(float scale);
|
||||
void requestUIScale(float scale); // safe to call mid-frame
|
||||
void syncCanvasSize();
|
||||
// (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};
|
||||
@@ -168,13 +92,7 @@ private:
|
||||
float fileSampleRate_ = 48000.0f;
|
||||
bool fileLoop_ = true;
|
||||
|
||||
// Device selection
|
||||
std::vector<MiniAudioSource::DeviceInfo> paDevices_;
|
||||
int paDeviceIdx_ = 0;
|
||||
bool paDeviceSelected_[kMaxChannels] = {}; // multi-device checkboxes
|
||||
bool multiDeviceMode_ = false; // true = use multiple devices as channels
|
||||
|
||||
// Channel colors (up to kMaxChannels). Defaults: L=purple, R=green.
|
||||
// Channel colors (up to kMaxChannels). Defaults: L=green, R=purple.
|
||||
ImVec4 channelColors_[kMaxChannels] = {
|
||||
{0.20f, 0.90f, 0.30f, 1.0f}, // green
|
||||
{0.70f, 0.30f, 1.00f, 1.0f}, // purple
|
||||
@@ -189,10 +107,6 @@ private:
|
||||
bool waterfallMultiCh_ = true; // true = multi-channel overlay mode
|
||||
bool channelEnabled_[kMaxChannels] = {true,true,true,true,true,true,true,true};
|
||||
|
||||
// Math channels
|
||||
std::vector<MathChannel> mathChannels_;
|
||||
std::vector<std::vector<float>> mathSpectra_; // computed each frame
|
||||
|
||||
// Frequency zoom/pan (normalized 0–1 over full bandwidth)
|
||||
float viewLo_ = 0.0f; // left edge
|
||||
float viewHi_ = 0.5f; // right edge (default 2x zoom from left)
|
||||
|
||||
Reference in New Issue
Block a user