mirror of
https://codeberg.org/armin/beamgrid.git
synced 2026-09-01 04:10:47 +02:00
Initial BEAMGRID VST3/AU spectrum analyzer
This commit is contained in:
commit
0a3fe6d853
15 changed files with 908 additions and 0 deletions
154
Source/Analyser.cpp
Normal file
154
Source/Analyser.cpp
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#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)));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue