split off AudioEngine

This commit is contained in:
2026-03-28 15:39:47 +01:00
parent 37437b3212
commit b5906d2ab1
6 changed files with 697 additions and 664 deletions

View File

@@ -47,6 +47,7 @@ set(SOURCES
src/dsp/SpectrumAnalyzer.cpp src/dsp/SpectrumAnalyzer.cpp
src/audio/MiniAudioSource.cpp src/audio/MiniAudioSource.cpp
src/audio/FileSource.cpp src/audio/FileSource.cpp
src/audio/AudioEngine.cpp
src/ui/ColorMap.cpp src/ui/ColorMap.cpp
src/ui/WaterfallDisplay.cpp src/ui/WaterfallDisplay.cpp
src/ui/SpectrumDisplay.cpp src/ui/SpectrumDisplay.cpp

340
src/audio/AudioEngine.cpp Normal file
View 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
View 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

View File

@@ -157,4 +157,53 @@ struct Color3 {
uint8_t r, g, b; 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 } // namespace baudmine

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,7 @@
#include "core/Types.h" #include "core/Types.h"
#include "core/Config.h" #include "core/Config.h"
#include "dsp/SpectrumAnalyzer.h" #include "audio/AudioEngine.h"
#include "audio/AudioSource.h"
#include "audio/MiniAudioSource.h"
#include "ui/ColorMap.h" #include "ui/ColorMap.h"
#include "ui/WaterfallDisplay.h" #include "ui/WaterfallDisplay.h"
#include "ui/SpectrumDisplay.h" #include "ui/SpectrumDisplay.h"
@@ -12,62 +10,11 @@
#include "ui/Measurements.h" #include "ui/Measurements.h"
#include <SDL.h> #include <SDL.h>
#include <complex>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
namespace baudmine { 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 { class Application {
public: public:
Application(); Application();
@@ -86,11 +33,10 @@ private:
void renderWaterfallPanel(); void renderWaterfallPanel();
void handleSpectrumInput(float posX, float posY, float sizeX, float sizeY); void handleSpectrumInput(float posX, float posY, float sizeX, float sizeY);
void openPortAudio(); void openDevice();
void openMultiDevice(); void openMultiDevice();
void openFile(const std::string& path, InputFormat format, double sampleRate); void openFile(const std::string& path, InputFormat format, double sampleRate);
void updateAnalyzerSettings(); void updateAnalyzerSettings();
void computeMathChannels();
void renderMathPanel(); void renderMathPanel();
void loadConfig(); void loadConfig();
@@ -101,28 +47,8 @@ private:
SDL_GLContext glContext_ = nullptr; SDL_GLContext glContext_ = nullptr;
bool running_ = false; bool running_ = false;
// Audio // Audio engine (owns sources, analyzers, math channels)
std::unique_ptr<AudioSource> audioSource_; AudioEngine audio_;
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_;
// UI state // UI state
ColorMap colorMap_; ColorMap colorMap_;
@@ -145,8 +71,6 @@ private:
void applyUIScale(float scale); void applyUIScale(float scale);
void requestUIScale(float scale); // safe to call mid-frame void requestUIScale(float scale); // safe to call mid-frame
void syncCanvasSize(); void syncCanvasSize();
// (waterfallW_ removed — texture width tracks bin count automatically)
// (waterfallH_ removed — fixed history depth of 1024 rows)
// FFT size options // FFT size options
static constexpr int kFFTSizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536}; static constexpr int kFFTSizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536};
@@ -168,13 +92,7 @@ private:
float fileSampleRate_ = 48000.0f; float fileSampleRate_ = 48000.0f;
bool fileLoop_ = true; bool fileLoop_ = true;
// Device selection // Channel colors (up to kMaxChannels). Defaults: L=green, R=purple.
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.
ImVec4 channelColors_[kMaxChannels] = { ImVec4 channelColors_[kMaxChannels] = {
{0.20f, 0.90f, 0.30f, 1.0f}, // green {0.20f, 0.90f, 0.30f, 1.0f}, // green
{0.70f, 0.30f, 1.00f, 1.0f}, // purple {0.70f, 0.30f, 1.00f, 1.0f}, // purple
@@ -189,10 +107,6 @@ private:
bool waterfallMultiCh_ = true; // true = multi-channel overlay mode bool waterfallMultiCh_ = true; // true = multi-channel overlay mode
bool channelEnabled_[kMaxChannels] = {true,true,true,true,true,true,true,true}; 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 01 over full bandwidth) // Frequency zoom/pan (normalized 01 over full bandwidth)
float viewLo_ = 0.0f; // left edge float viewLo_ = 0.0f; // left edge
float viewHi_ = 0.5f; // right edge (default 2x zoom from left) float viewHi_ = 0.5f; // right edge (default 2x zoom from left)