2026-08-29 13:17:11 +02:00
|
|
|
#include "EqualizerDSP.h"
|
|
|
|
|
|
|
|
|
|
namespace gelyk
|
|
|
|
|
{
|
|
|
|
|
|
2026-08-29 15:42:21 +02:00
|
|
|
void computeEQResponse(const FilterBand::Type* types,
|
|
|
|
|
const float* gains, const float* freqs, const float* qs,
|
|
|
|
|
int numBands, double sampleRate, float* outDb, int numBins)
|
|
|
|
|
{
|
|
|
|
|
if (numBins <= 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
for (int b = 0; b < numBins; ++b)
|
|
|
|
|
outDb[b] = 0.0f;
|
|
|
|
|
|
|
|
|
|
const float fMin = 20.0f;
|
|
|
|
|
const float fMax = 20000.0f;
|
|
|
|
|
const float logMin = std::log10(fMin);
|
|
|
|
|
const float logMax = std::log10(fMax);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < numBands; ++i)
|
|
|
|
|
{
|
|
|
|
|
const float g = juce::jlimit(-24.0f, 24.0f, gains[i]);
|
|
|
|
|
const float gainFactor = juce::Decibels::decibelsToGain(g);
|
|
|
|
|
const float f = juce::jlimit(20.0f, 20000.0f, freqs[i]);
|
|
|
|
|
const float q = juce::jlimit(0.1f, 6.0f, qs[i]);
|
|
|
|
|
|
|
|
|
|
FilterBand::Coeffs::Ptr c;
|
|
|
|
|
switch (types[i])
|
|
|
|
|
{
|
|
|
|
|
case FilterBand::Type::lowShelf:
|
|
|
|
|
c = FilterBand::Coeffs::makeLowShelf(sampleRate, f, q, gainFactor);
|
|
|
|
|
break;
|
|
|
|
|
case FilterBand::Type::highShelf:
|
|
|
|
|
c = FilterBand::Coeffs::makeHighShelf(sampleRate, f, q, gainFactor);
|
|
|
|
|
break;
|
|
|
|
|
case FilterBand::Type::peak:
|
|
|
|
|
default:
|
|
|
|
|
c = FilterBand::Coeffs::makePeakFilter(sampleRate, f, q, gainFactor);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int b = 0; b < numBins; ++b)
|
|
|
|
|
{
|
|
|
|
|
const float ratio = (float) b / (float) (numBins - 1);
|
|
|
|
|
const double freq = std::pow(10.0, logMin + ratio * (logMax - logMin));
|
|
|
|
|
outDb[b] += 20.0f * std::log10((float) c->getMagnitudeForFrequency(freq, sampleRate));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 13:17:11 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// FilterBand
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void FilterBand::prepare(double sampleRate, int blockSize)
|
|
|
|
|
{
|
2026-08-29 22:55:17 +02:00
|
|
|
this->sampleRate = sampleRate;
|
|
|
|
|
|
2026-08-29 13:17:11 +02:00
|
|
|
juce::dsp::ProcessSpec spec;
|
|
|
|
|
spec.sampleRate = sampleRate;
|
|
|
|
|
spec.maximumBlockSize = (juce::uint32) blockSize;
|
|
|
|
|
spec.numChannels = 2;
|
|
|
|
|
|
|
|
|
|
left.prepare(spec);
|
|
|
|
|
right.prepare(spec);
|
|
|
|
|
|
2026-08-29 22:55:17 +02:00
|
|
|
// Start from the current (target) parameters with no ramp.
|
|
|
|
|
targetGain = gainDb;
|
|
|
|
|
targetFreq = freq;
|
|
|
|
|
targetQ = q;
|
|
|
|
|
curGain = gainDb;
|
|
|
|
|
curFreq = freq;
|
|
|
|
|
curQ = q;
|
|
|
|
|
|
|
|
|
|
updateCoeffs();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FilterBand::setTargets(float gain, float freq, float q)
|
|
|
|
|
{
|
|
|
|
|
gainDb = gain;
|
|
|
|
|
this->freq = freq;
|
|
|
|
|
this->q = q;
|
|
|
|
|
targetGain = gain;
|
|
|
|
|
targetFreq = freq;
|
|
|
|
|
targetQ = q;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FilterBand::updateCoeffs()
|
|
|
|
|
{
|
|
|
|
|
const float db = juce::jlimit(-24.0f, 24.0f, curGain);
|
2026-08-29 13:17:11 +02:00
|
|
|
Coeffs::Ptr c;
|
|
|
|
|
switch (type)
|
|
|
|
|
{
|
|
|
|
|
case Type::lowShelf:
|
2026-08-29 22:55:17 +02:00
|
|
|
c = Coeffs::makeLowShelf(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db));
|
2026-08-29 13:17:11 +02:00
|
|
|
break;
|
|
|
|
|
case Type::highShelf:
|
2026-08-29 22:55:17 +02:00
|
|
|
c = Coeffs::makeHighShelf(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db));
|
2026-08-29 13:17:11 +02:00
|
|
|
break;
|
|
|
|
|
case Type::peak:
|
|
|
|
|
default:
|
2026-08-29 22:55:17 +02:00
|
|
|
c = Coeffs::makePeakFilter(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db));
|
2026-08-29 13:17:11 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
left.coefficients = c;
|
|
|
|
|
right.coefficients = c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FilterBand::reset()
|
|
|
|
|
{
|
|
|
|
|
left.reset();
|
|
|
|
|
right.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FilterBand::process(juce::dsp::AudioBlock<float>& block)
|
|
|
|
|
{
|
2026-08-29 22:55:17 +02:00
|
|
|
const int numSamples = (int) block.getNumSamples();
|
|
|
|
|
const auto numChannels = block.getNumChannels();
|
|
|
|
|
|
|
|
|
|
// One-pole (exponential) smoothing evaluated at block rate, using the real
|
|
|
|
|
// number of samples in this block so the ramp is independent of host buffer
|
|
|
|
|
// size. ~50 ms time constant: starts responding immediately and settles
|
|
|
|
|
// within ~200 ms, giving a gentle glide without the multi-second lag the old
|
|
|
|
|
// per-sample SmoothedValue ramp produced when only advanced once per block.
|
|
|
|
|
if (numSamples > 0 && numChannels > 0)
|
|
|
|
|
{
|
|
|
|
|
const float alpha = 1.0f - std::exp(-(float) numSamples / (sampleRate * 0.05f));
|
|
|
|
|
|
|
|
|
|
const float g = curGain + (targetGain - curGain) * alpha;
|
|
|
|
|
const float f = curFreq + (targetFreq - curFreq) * alpha;
|
|
|
|
|
const float qv = curQ + (targetQ - curQ) * alpha;
|
|
|
|
|
|
|
|
|
|
if (std::abs(g - curGain) > 0.001f ||
|
|
|
|
|
std::abs(f - curFreq) > 0.01f ||
|
|
|
|
|
std::abs(qv - curQ) > 0.001f)
|
|
|
|
|
{
|
|
|
|
|
curGain = g;
|
|
|
|
|
curFreq = f;
|
|
|
|
|
curQ = qv;
|
|
|
|
|
updateCoeffs();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (numChannels < 1)
|
2026-08-29 13:17:11 +02:00
|
|
|
return;
|
|
|
|
|
|
2026-08-29 22:55:17 +02:00
|
|
|
// JUCE's IIR::Filter only processes mono (one channel pointer), so each
|
|
|
|
|
// instance must be run on a single channel's own block. Previously the whole
|
|
|
|
|
// (stereo) block was passed in, which made both filters operate on channel 0
|
|
|
|
|
// only: the left went through twice and the right not at all. Splitting the
|
|
|
|
|
// channels fixes the dead right channel and the doubled (rumble) low end.
|
2026-08-29 13:17:11 +02:00
|
|
|
left.snapToZero();
|
|
|
|
|
right.snapToZero();
|
2026-08-29 22:55:17 +02:00
|
|
|
|
|
|
|
|
auto leftBlock = block.getSingleChannelBlock(0);
|
|
|
|
|
left.process(juce::dsp::ProcessContextReplacing<float>(leftBlock));
|
|
|
|
|
|
|
|
|
|
if (numChannels > 1)
|
|
|
|
|
{
|
|
|
|
|
auto rightBlock = block.getSingleChannelBlock(1);
|
|
|
|
|
right.process(juce::dsp::ProcessContextReplacing<float>(rightBlock));
|
|
|
|
|
}
|
2026-08-29 13:17:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// SpectrumAnalyser
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
SpectrumAnalyser::SpectrumAnalyser()
|
2026-08-29 13:26:26 +02:00
|
|
|
: fft(12) // 4096 point FFT
|
2026-08-29 13:17:11 +02:00
|
|
|
{
|
2026-08-29 13:26:26 +02:00
|
|
|
const int size = fft.getSize();
|
|
|
|
|
fftBuffer.resize((size_t) (size * 2), 0.0f);
|
2026-08-29 13:17:11 +02:00
|
|
|
// The timer drives periodic background analysis.
|
|
|
|
|
startTimerHz(30);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::prepare(double sr, int maxBlockSize)
|
|
|
|
|
{
|
|
|
|
|
const int size = 4096;
|
2026-08-29 13:26:26 +02:00
|
|
|
(void) maxBlockSize;
|
2026-08-29 13:17:11 +02:00
|
|
|
sampleRate = sr;
|
|
|
|
|
ring.setSize(2, size);
|
|
|
|
|
ring.clear();
|
|
|
|
|
ringFill = 0;
|
|
|
|
|
ringChannels = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::push(const float* const* channels, int numChannels, int numSamples)
|
|
|
|
|
{
|
|
|
|
|
if (!enabled.load() || ring.getNumSamples() == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const int n = (int) ring.getNumSamples();
|
|
|
|
|
const int c = juce::jmin(ringChannels, numChannels);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < numSamples; ++i)
|
|
|
|
|
{
|
|
|
|
|
const int dst = (ringFill + i) % n;
|
|
|
|
|
for (int ch = 0; ch < c; ++ch)
|
|
|
|
|
ring.setSample(ch, dst, channels[ch][i]);
|
|
|
|
|
}
|
|
|
|
|
ringFill = (ringFill + numSamples) % n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::startBackground()
|
|
|
|
|
{
|
|
|
|
|
startTimerHz(30);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::stopBackground()
|
|
|
|
|
{
|
|
|
|
|
stopTimer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::timerCallback()
|
|
|
|
|
{
|
|
|
|
|
if (!enabled.load())
|
|
|
|
|
return;
|
|
|
|
|
runAnalysis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::runAnalysis()
|
|
|
|
|
{
|
|
|
|
|
const int n = (int) ring.getNumSamples();
|
|
|
|
|
if (n == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-08-29 13:26:26 +02:00
|
|
|
// Mesa of the two channels, Hann-windowed into the FFT buffer.
|
|
|
|
|
const auto fftSize = fft.getSize();
|
|
|
|
|
for (int i = 0; i < fftSize; ++i)
|
2026-08-29 13:17:11 +02:00
|
|
|
{
|
2026-08-29 13:26:26 +02:00
|
|
|
const int src = (ringFill - fftSize + i + n) % n; // contiguously oldest of last fftSize
|
|
|
|
|
float v = ring.getSample(0, src);
|
|
|
|
|
if (ringChannels > 1)
|
|
|
|
|
v = 0.5f * (v + ring.getSample(1, src));
|
|
|
|
|
const float win = 0.5f * (1.0f - std::cos(2.0f * juce::MathConstants<float>::pi * i / (fftSize - 1)));
|
|
|
|
|
fftBuffer[(size_t) i] = v * win;
|
2026-08-29 13:17:11 +02:00
|
|
|
}
|
2026-08-29 13:26:26 +02:00
|
|
|
for (size_t i = (size_t) fftSize; i < fftBuffer.size(); ++i)
|
|
|
|
|
fftBuffer[i] = 0.0f;
|
2026-08-29 13:17:11 +02:00
|
|
|
|
2026-08-29 13:26:26 +02:00
|
|
|
fft.performFrequencyOnlyForwardTransform(fftBuffer.data());
|
|
|
|
|
|
|
|
|
|
// Magnitude values (non-negative frequencies) are in the first size/2+1 entries.
|
2026-08-29 22:55:17 +02:00
|
|
|
// Normalise so a full-scale sine reads as ~0 dB: JUCE's frequency-only FFT gives
|
|
|
|
|
// a magnitude of ~N/2 for a unit sine at its bin, and the Hann window halves that
|
|
|
|
|
// to N/4. Dividing by that reference keeps the -72..+12 dB display range meaningful.
|
|
|
|
|
const float norm = 0.25f * (float) fftSize;
|
2026-08-29 13:26:26 +02:00
|
|
|
const int numBins = juce::jmin((int) fftBuffer.size(), fftSize / 2 + 1);
|
2026-08-29 13:17:11 +02:00
|
|
|
juce::Array<float> mags;
|
|
|
|
|
mags.ensureStorageAllocated(numBins);
|
2026-08-29 13:26:26 +02:00
|
|
|
for (int i = 0; i < numBins; ++i)
|
2026-08-29 13:17:11 +02:00
|
|
|
{
|
2026-08-29 13:26:26 +02:00
|
|
|
float mag = fftBuffer[(size_t) i];
|
2026-08-29 22:55:17 +02:00
|
|
|
float db = 20.0f * std::log10(mag / norm + 1.0e-9f);
|
2026-08-29 13:17:11 +02:00
|
|
|
mags.add(db);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
juce::ScopedLock sl(spectrumLock);
|
|
|
|
|
readySpectrum = mags;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int SpectrumAnalyser::copySpectrum(juce::Array<float>& dest, juce::Array<float>& freqBins,
|
|
|
|
|
int maxBins) const
|
|
|
|
|
{
|
|
|
|
|
juce::Array<float> snapshot;
|
|
|
|
|
{
|
|
|
|
|
juce::ScopedLock sl(spectrumLock);
|
|
|
|
|
snapshot = readySpectrum;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (snapshot.size() == 0)
|
|
|
|
|
{
|
|
|
|
|
dest.clear();
|
|
|
|
|
freqBins.clear();
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log-spaced aggregation from ~20 Hz to 20 kHz.
|
|
|
|
|
const float fMin = 20.0f;
|
|
|
|
|
const float fMax = 20000.0f;
|
|
|
|
|
const double nfft = (double)(snapshot.size() * 2);
|
|
|
|
|
const float binFreq = (float)(sampleRate / nfft);
|
|
|
|
|
|
|
|
|
|
dest.clear();
|
|
|
|
|
freqBins.clear();
|
|
|
|
|
|
|
|
|
|
const int bands = juce::jmin(maxBins, snapshot.size());
|
|
|
|
|
for (int b = 0; b < bands; ++b)
|
|
|
|
|
{
|
|
|
|
|
const float f = fMin * std::pow(fMax / fMin, (float) b / (float) (bands - 1));
|
|
|
|
|
freqBins.add(f);
|
|
|
|
|
const int startBin = juce::jmax(1, (int) std::floor(f / binFreq));
|
|
|
|
|
const int endBin = juce::jmin(snapshot.size() - 1, (int) std::ceil((f * 1.5f) / binFreq));
|
|
|
|
|
float total = 0.0f;
|
|
|
|
|
int count = 0;
|
|
|
|
|
for (int i = startBin; i <= endBin; ++i)
|
|
|
|
|
{
|
|
|
|
|
total += snapshot[i];
|
|
|
|
|
++count;
|
|
|
|
|
}
|
|
|
|
|
dest.add(count > 0 ? total / count : -96.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dest.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpectrumAnalyser::setEnabled(bool e)
|
|
|
|
|
{
|
|
|
|
|
enabled.store(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace gelyk
|