ambivalence/Source/DSP/AcousticMetrics.cpp

168 lines
6.8 KiB
C++
Raw Normal View History

2026-08-15 15:36:28 +02:00
#include "AcousticMetrics.h"
#include <algorithm>
#include <cmath>
namespace FDNReverb {
void AcousticMetrics::prepare(double sr, float windowMs) {
sampleRate = sr;
analysisWindowMs = windowMs;
// sample rate time sample count
samples50ms = static_cast<int>(0.050 * sr);
samples80ms = static_cast<int>(0.080 * sr);
analysisWindowSamples = static_cast<int>(windowMs * 0.001 * sr);
// read from the history buffer size analysis +
size_t bufferSize = static_cast<size_t>(analysisWindowSamples + samples80ms + 64);
energyHistory.assign(bufferSize, 0.0f);
reset();
}
void AcousticMetrics::reset() noexcept {
std::fill(energyHistory.begin(), energyHistory.end(), 0.0f);
historyWritePos = 0;
recent50msEnergy = 0.0;
recent80msEnergy = 0.0;
totalEnergy = 0.0;
energyPeak = 0.0f;
energyPeakPos = 0;
updateCounter = 0;
d50.store(0.0f, std::memory_order_relaxed);
c50.store(0.0f, std::memory_order_relaxed);
c80.store(0.0f, std::memory_order_relaxed);
edt.store(0.0f, std::memory_order_relaxed);
}
void AcousticMetrics::processSample(float sample) noexcept {
if (energyHistory.empty()) return;
const int bufferSize = static_cast<int>(energyHistory.size());
// current sample energy ( squared )
float currentEnergy = sample * sample;
// read from the history buffer
energyHistory[historyWritePos] = currentEnergy;
// update the running sums (50 ms / 80 ms / full window)
// add the current sample, subtract the value from 50 ms ago
const int read50Pos = (historyWritePos - samples50ms + bufferSize) % bufferSize;
const int read80Pos = (historyWritePos - samples80ms + bufferSize) % bufferSize;
const int readWindowPos = (historyWritePos - analysisWindowSamples + bufferSize) % bufferSize;
recent50msEnergy += currentEnergy - energyHistory[read50Pos];
recent80msEnergy += currentEnergy - energyHistory[read80Pos];
totalEnergy += currentEnergy - energyHistory[readWindowPos];
// peak detection (EDT estimate )
if (currentEnergy > energyPeak) {
energyPeak = currentEnergy;
energyPeakPos = historyWritePos;
}
//
historyWritePos = (historyWritePos + 1) % bufferSize;
// value stable ( cumulative value 0 )
if (recent50msEnergy < 0.0) recent50msEnergy = 0.0;
if (recent80msEnergy < 0.0) recent80msEnergy = 0.0;
if (totalEnergy < 0.0) totalEnergy = 0.0;
// value interval update
if (++updateCounter >= kUpdateInterval) {
updateMetrics();
updateCounter = 0;
}
}
void AcousticMetrics::updateMetrics() noexcept {
// 50ms energy
double energy50ToInf = totalEnergy - recent50msEnergy;
if (energy50ToInf < 1e-12) energy50ToInf = 1e-12;
// 80ms energy
double energy80ToInf = totalEnergy - recent80msEnergy;
if (energy80ToInf < 1e-12) energy80ToInf = 1e-12;
// entire energy ( minimum value clipping )
double totalSafe = std::max(1e-12, totalEnergy);
// -- D50 compute (0~1 ) --
float d50val = static_cast<float>(recent50msEnergy / totalSafe);
d50val = std::min(1.0f, std::max(0.0f, d50val));
d50.store(d50val, std::memory_order_relaxed);
// -- C50 compute (dB) --
float c50val = static_cast<float>(10.0 * std::log10(recent50msEnergy / energy50ToInf));
c50val = std::min(60.0f, std::max(-60.0f, c50val));
c50.store(c50val, std::memory_order_relaxed);
// -- C80 compute (dB) --
float c80val = static_cast<float>(10.0 * std::log10(recent80msEnergy / energy80ToInf));
c80val = std::min(60.0f, std::max(-60.0f, c80val));
c80.store(c80val, std::memory_order_relaxed);
// -- EDT estimate (running) --
// after the peak, the time until energy falls to 1/10 (-10 dB decay)
// * exact EDT needs offline IR analysis; here we estimate from the peak decay time
float edtVal = 0.0f;
if (energyPeak > 1e-9f) {
// analysis peak
// scan from the peak sample until the energy reaches 1/10
const int bufferSize = static_cast<int>(energyHistory.size());
int searchStart = energyPeakPos;
float threshold = energyPeak * 0.1f; // 10dB decay
int decaySamples = 0;
for (int i = 1; i < analysisWindowSamples; ++i) {
int pos = (searchStart + i) % bufferSize;
if (energyHistory[pos] < threshold) {
decaySamples = i;
break;
}
}
edtVal = static_cast<float>(decaySamples) / static_cast<float>(sampleRate) * 6.0f;
// * 10 dB decay time x 6 ~= EDT (60 dB decay correction)
}
edt.store(edtVal, std::memory_order_relaxed);
}
// -----------------------------------------------------------------------------
// drawing: get instantaneous energy at a past time offset
// -----------------------------------------------------------------------------
// secondsAgo: how many seconds in the past to look up
// returns: the energy value at that time (squared)
//
// reads the history buffer directly for the GUI.
// out of range returns 0.
// -----------------------------------------------------------------------------
float AcousticMetrics::getEnergyAtTimeOffset(float secondsAgo) const noexcept {
if (energyHistory.empty()) return 0.0f;
const int bufferSize = static_cast<int>(energyHistory.size());
int offsetSamples = static_cast<int>(secondsAgo * static_cast<float>(sampleRate));
// clamp to range
if (offsetSamples < 0) offsetSamples = 0;
if (offsetSamples >= analysisWindowSamples) return 0.0f;
// read from the history buffer
int readPos = (historyWritePos - 1 - offsetSamples + bufferSize) % bufferSize;
return energyHistory[readPos];
}
// -----------------------------------------------------------------------------
// input activity detection: energy over the last 50 ms
// -----------------------------------------------------------------------------
// used by the GUI to hide the "measured line" when inactive.
// threshold: -60 dBFS (1e-6) energy
// -----------------------------------------------------------------------------
bool AcousticMetrics::isActive() const noexcept {
// energy over the last 50 ms determines activity
constexpr double kActivityThreshold = 1e-6; // -60dBFS
return recent50msEnergy > kActivityThreshold;
}
} // namespace FDNReverb