#include "Analyser.h" #include Analyser::Analyser() = default; void Analyser::prepare (double newSampleRate, int /*blockSize*/) { sampleRate = newSampleRate > 0.0 ? newSampleRate : 44100.0; minFreq = 20.0; maxFreq = juce::jmin (20000.0, sampleRate * 0.5); computeBandEdges(); } void Analyser::computeBandEdges() { const int n = numBands; // Log-spaced band edges mapped into FFT bins. for (int b = 0; b <= n; ++b) { const double freq = minFreq * std::pow (maxFreq / minFreq, static_cast (b) / n); int bin = static_cast (std::round (freq * fftSize / sampleRate)); bin = juce::jlimit (1, static_cast (fftBins), bin); bandBinEdges[b] = bin; } // Guarantee strictly increasing edges. for (int b = 1; b <= n; ++b) if (bandBinEdges[b] <= bandBinEdges[b - 1]) bandBinEdges[b] = juce::jmin (static_cast (fftBins), bandBinEdges[b - 1] + 1); } void Analyser::setNumBands (int n) noexcept { numBands = juce::jlimit (1, maxBands, n); computeBandEdges(); } void Analyser::push (const float* channelData, int numSamples) { if (channelData == nullptr) return; for (int i = 0; i < numSamples; ++i) pushSample (channelData[i]); } void Analyser::pushSample (float sample) { ring[writePos] = sample; writePos = (writePos + 1) % fftSize; ++totalSamples; // Only start transforming once the window has filled at least once, // then recompute overlapped every `hopSize` samples. if (totalSamples >= fftSize && ++samplesSinceFFT >= hopSize) { samplesSinceFFT = 0; computeFFT(); } } void Analyser::computeFFT() { // Gather the most recent fftSize samples in order (oldest -> newest). const int start = writePos; for (int i = 0; i < fftSize; ++i) fftData[i] = ring[(start + i) % fftSize]; window.multiplyWithWindowingTable (fftData.data(), static_cast (fftSize)); juce::FloatVectorOperations::clear (fftData.data() + fftSize, fftSize); fft.performFrequencyOnlyForwardTransform (fftData.data()); for (int b = 0; b < numBands; ++b) { const int lo = bandBinEdges[b]; const int hi = bandBinEdges[b + 1]; double sum = 0.0; int count = 0; for (int k = lo; k < hi && k < fftBins; ++k) { sum += fftData[k]; ++count; } const double avg = count > 0 ? sum / count : 0.0; // Normalize the bin magnitude by the FFT size so "0 dB" corresponds // to ~full scale, otherwise the raw magnitudes sit ~66 dB too hot. const double normalized = avg / static_cast (fftSize); const double db = 20.0 * std::log10 (normalized + 1e-9); double norm = (db - minDb) / (maxDb - minDb); norm = juce::jlimit (0.0, 1.0, norm); targets[b] = static_cast (norm); } } void Analyser::update (double dt) { // Live bar level eases toward the latest FFT target with a fast attack // and a knob-controlled release (bar falloff). const double attackCoef = 1.0 - std::exp (-dt / 0.005); const double releaseCoef = 1.0 - std::exp (-dt / juce::jmax (0.001, barReleaseTau)); for (int b = 0; b < numBands; ++b) { const float target = targets[b]; if (target >= levels[b]) levels[b] += (target - levels[b]) * static_cast (attackCoef); else levels[b] += (target - levels[b]) * static_cast (releaseCoef); if (levels[b] >= peaks[b]) { peaks[b] = levels[b]; peakTimers[b] = 0.0f; } else { peakTimers[b] += static_cast (dt); if (peakTimers[b] >= grace) { peaks[b] -= static_cast (falloffRate * dt); if (peaks[b] < levels[b]) peaks[b] = levels[b]; if (peaks[b] < 0.0f) peaks[b] = 0.0f; } } } } float Analyser::freqToFraction (double freq) const noexcept { const double f = juce::jlimit (minFreq, maxFreq, freq); return static_cast (std::log (f / minFreq) / std::log (maxFreq / minFreq)); } float Analyser::dbToFraction (double db) const noexcept { return static_cast (juce::jlimit (0.0, 1.0, (db - minDb) / (maxDb - minDb))); }