Files
baudmine/src/audio/AudioEngine.cpp

393 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "audio/AudioEngine.h"
#include "audio/FileSource.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
namespace baudmine {
namespace {
constexpr float kLinearEpsilon = 1e-20f; // threshold for log10 of linear power
constexpr float kLogGuard = 1e-30f; // guard against log10(0) in compressed scale
} // namespace
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();
for (auto& mw : mathWaterfalls_)
mw.clear();
}
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);
// Drain all available audio so the scroll rate is independent of the
// display refresh rate (vsync). Real-time sources self-limit via their
// ring buffer; file sources are capped to wall-clock time so playback
// runs at 1× speed regardless of frame rate.
// For file sources, compute how many samples correspond to elapsed time.
size_t fileSampleCap = SIZE_MAX; // unlimited for real-time
if (!audioSource_->isRealTime()) {
using Clock = std::chrono::steady_clock;
static Clock::time_point lastFileTime = Clock::now();
auto now = Clock::now();
double elapsed = std::chrono::duration<double>(now - lastFileTime).count();
lastFileTime = now;
// Clamp elapsed to avoid huge bursts after pauses or stalls.
if (elapsed > 0.1) elapsed = 0.1;
fileSampleCap = static_cast<size_t>(elapsed * settings_.sampleRate) + 1;
}
// Process primary source.
int spectraThisFrame = 0;
size_t samplesRead = 0;
for (;;) {
if (samplesRead >= fileSampleCap) break;
size_t framesRead = audioSource_->read(audioBuf_.data(), hopFrames);
if (framesRead == 0) break;
samplesRead += framesRead;
analyzer_.pushSamples(audioBuf_.data(), framesRead);
if (analyzer_.hasNewSpectrum())
++spectraThisFrame;
}
// Process extra devices independently (always real-time).
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);
for (;;) {
size_t framesRead = ed->source->read(ed->audioBuf.data(), edHop);
if (framesRead == 0) break;
ed->analyzer.pushSamples(ed->audioBuf.data(), framesRead);
}
}
// Track overruns: spectra lost because they exceeded the history capacity.
if (spectraThisFrame > kWaterfallHistory)
overrunCount_ += spectraThisFrame - kWaterfallHistory;
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::deque<std::vector<float>>& AudioEngine::getWaterfallHistory(int globalCh) const {
int n = analyzer_.numSpectra();
if (globalCh < n) return analyzer_.waterfallHistory(globalCh);
globalCh -= n;
for (auto& ed : extraDevices_) {
int en = ed->analyzer.numSpectra();
if (globalCh < en) return ed->analyzer.waterfallHistory(globalCh);
globalCh -= en;
}
return analyzer_.waterfallHistory(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 ────────────────────────────────────────────────────────────
const std::deque<std::vector<float>>& AudioEngine::mathWaterfallHistory(int mi) const {
static const std::deque<std::vector<float>> empty;
if (mi < 0 || mi >= static_cast<int>(mathWaterfalls_.size())) return empty;
return mathWaterfalls_[mi];
}
void AudioEngine::computeMathChannels() {
int nPhys = totalNumSpectra();
int specSz = analyzer_.spectrumSize();
mathSpectra_.resize(mathChannels_.size());
mathWaterfalls_.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(), kNoSignalDB);
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 = kNoSignalDB;
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 + kLogGuard);
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 > kLinearEpsilon) ? 10.0f * std::log10(s) : kNoSignalDB;
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 > kLinearEpsilon) ? 10.0f * std::log10(d) : kNoSignalDB;
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 / kPi);
}
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 > kLinearEpsilon) ? 10.0f * std::log10(mag2) : kNoSignalDB;
}
break;
}
default: break;
}
out[i] = val;
}
// Push to math waterfall history.
mathWaterfalls_[mi].push_back(out);
if (mathWaterfalls_[mi].size() > kWaterfallHistory)
mathWaterfalls_[mi].pop_front();
}
}
} // namespace baudmine