beamgrid/Source/Analyser.cpp

154 lines
4.4 KiB
C++
Raw Normal View History

#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;
maxFreq = 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<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);
bandBinEdges[n] = fftBins;
}
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)));
}