#include "AcousticMetrics.h" #include #include namespace FDNReverb { void AcousticMetrics::prepare(double sr, float windowMs) { sampleRate = sr; analysisWindowMs = windowMs; // sample rate time sample count samples50ms = static_cast(0.050 * sr); samples80ms = static_cast(0.080 * sr); analysisWindowSamples = static_cast(windowMs * 0.001 * sr); // read from the history buffer size analysis + size_t bufferSize = static_cast(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(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(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(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(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(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(decaySamples) / static_cast(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(energyHistory.size()); int offsetSamples = static_cast(secondsAgo * static_cast(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