#include "EqualizerDSP.h" namespace gelyk { 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)); } } } // --------------------------------------------------------------------------- // FilterBand // --------------------------------------------------------------------------- void FilterBand::prepare(double sampleRate, int blockSize) { this->sampleRate = sampleRate; juce::dsp::ProcessSpec spec; spec.sampleRate = sampleRate; spec.maximumBlockSize = (juce::uint32) blockSize; spec.numChannels = 2; left.prepare(spec); right.prepare(spec); // 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); Coeffs::Ptr c; switch (type) { case Type::lowShelf: c = Coeffs::makeLowShelf(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db)); break; case Type::highShelf: c = Coeffs::makeHighShelf(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db)); break; case Type::peak: default: c = Coeffs::makePeakFilter(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db)); break; } left.coefficients = c; right.coefficients = c; } void FilterBand::reset() { left.reset(); right.reset(); } void FilterBand::process(juce::dsp::AudioBlock& block) { 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) return; // 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. left.snapToZero(); right.snapToZero(); auto leftBlock = block.getSingleChannelBlock(0); left.process(juce::dsp::ProcessContextReplacing(leftBlock)); if (numChannels > 1) { auto rightBlock = block.getSingleChannelBlock(1); right.process(juce::dsp::ProcessContextReplacing(rightBlock)); } } // --------------------------------------------------------------------------- // SpectrumAnalyser // --------------------------------------------------------------------------- SpectrumAnalyser::SpectrumAnalyser() : fft(12) // 4096 point FFT { const int size = fft.getSize(); fftBuffer.resize((size_t) (size * 2), 0.0f); // The timer drives periodic background analysis. startTimerHz(30); } void SpectrumAnalyser::prepare(double sr, int maxBlockSize) { const int size = 4096; (void) maxBlockSize; 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; // Mesa of the two channels, Hann-windowed into the FFT buffer. const auto fftSize = fft.getSize(); for (int i = 0; i < fftSize; ++i) { 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::pi * i / (fftSize - 1))); fftBuffer[(size_t) i] = v * win; } for (size_t i = (size_t) fftSize; i < fftBuffer.size(); ++i) fftBuffer[i] = 0.0f; fft.performFrequencyOnlyForwardTransform(fftBuffer.data()); // Magnitude values (non-negative frequencies) are in the first size/2+1 entries. // 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; const int numBins = juce::jmin((int) fftBuffer.size(), fftSize / 2 + 1); juce::Array mags; mags.ensureStorageAllocated(numBins); for (int i = 0; i < numBins; ++i) { float mag = fftBuffer[(size_t) i]; float db = 20.0f * std::log10(mag / norm + 1.0e-9f); mags.add(db); } { juce::ScopedLock sl(spectrumLock); readySpectrum = mags; } } int SpectrumAnalyser::copySpectrum(juce::Array& dest, juce::Array& freqBins, int maxBins) const { juce::Array 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