2026-08-13 23:15:38 +02:00
|
|
|
#include "Analyser.h"
|
|
|
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
|
|
|
|
Analyser::Analyser() = default;
|
|
|
|
|
|
|
|
|
|
void Analyser::prepare (double newSampleRate, int /*blockSize*/)
|
|
|
|
|
{
|
|
|
|
|
sampleRate = newSampleRate > 0.0 ? newSampleRate : 44100.0;
|
|
|
|
|
|
|
|
|
|
minFreq = 20.0;
|
2026-08-14 01:33:20 +02:00
|
|
|
maxFreq = juce::jmin (20000.0, sampleRate * 0.5);
|
2026-08-13 23:15:38 +02:00
|
|
|
|
|
|
|
|
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<double> (b) / n);
|
|
|
|
|
int bin = static_cast<int> (std::round (freq * fftSize / sampleRate));
|
|
|
|
|
bin = juce::jlimit (1, static_cast<int> (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<int> (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<size_t> (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<double> (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<float> (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<float> (attackCoef);
|
|
|
|
|
else
|
|
|
|
|
levels[b] += (target - levels[b]) * static_cast<float> (releaseCoef);
|
|
|
|
|
|
|
|
|
|
if (levels[b] >= peaks[b])
|
|
|
|
|
{
|
|
|
|
|
peaks[b] = levels[b];
|
|
|
|
|
peakTimers[b] = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
peakTimers[b] += static_cast<float> (dt);
|
|
|
|
|
|
|
|
|
|
if (peakTimers[b] >= grace)
|
|
|
|
|
{
|
|
|
|
|
peaks[b] -= static_cast<float> (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<float> (std::log (f / minFreq) / std::log (maxFreq / minFreq));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float Analyser::dbToFraction (double db) const noexcept
|
|
|
|
|
{
|
|
|
|
|
return static_cast<float> (juce::jlimit (0.0, 1.0, (db - minDb) / (maxDb - minDb)));
|
|
|
|
|
}
|