mirror of
https://codeberg.org/armin/ambivalence.git
synced 2026-09-01 04:10:48 +02:00
168 lines
6.8 KiB
C++
168 lines
6.8 KiB
C++
|
|
#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
|