| 43 | FrequencyBands::~FrequencyBands() FL_NOEXCEPT = default; |
| 44 | |
| 45 | void FrequencyBands::update(shared_ptr<Context> context) { |
| 46 | // Use sample rate from context if available |
| 47 | mSampleRate = context->getSampleRate(); |
| 48 | |
| 49 | // Use shared master fft::FFT via context (downsampled to our config) |
| 50 | mRetainedFFT = context->getFFT(kNumBands, kFFTMinFreq, kFFTMaxFreq); |
| 51 | const fft::Bins& fftBins = *mRetainedFFT; |
| 52 | sFrequencyBandsFFTCount++; // Diagnostic counter (no private fft::FFT anymore) |
| 53 | |
| 54 | span<const i16> pcm = context->getPCM(); |
| 55 | |
| 56 | // Calculate energy for each band using fractional bin overlap |
| 57 | float bassEnergy = calculateBandEnergy(fftBins, mBassMin, mBassMax, kFFTMinFreq, kFFTMaxFreq); |
| 58 | float midEnergy = calculateBandEnergy(fftBins, mMidMin, mMidMax, kFFTMinFreq, kFFTMaxFreq); |
| 59 | float trebleEnergy = calculateBandEnergy(fftBins, mTrebleMin, mTrebleMax, kFFTMinFreq, kFFTMaxFreq); |
| 60 | |
| 61 | // Compute dt from actual audio buffer duration: pcmSize / sampleRate |
| 62 | const float dt = computeAudioDt(pcm.size(), mSampleRate); |
| 63 | mBass = mBassSmoother.update(bassEnergy, dt); |
| 64 | mMid = mMidSmoother.update(midEnergy, dt); |
| 65 | mTreble = mTrebleSmoother.update(trebleEnergy, dt); |
| 66 | |
| 67 | // Per-band normalization — mirrors EnergyAnalyzer pattern |
| 68 | auto normalizeBand = [](float val, AttackDecayFilter<float>& filter, |
| 69 | float frameDt) -> float { |
| 70 | float runningMax = filter.update(val, frameDt); |
| 71 | if (runningMax < 0.001f) runningMax = 0.001f; |
| 72 | return fl::min(1.0f, val / runningMax); |
| 73 | }; |
| 74 | mBassNorm = normalizeBand(mBass, mBassMaxFilter, dt); |
| 75 | mMidNorm = normalizeBand(mMid, mMidMaxFilter, dt); |
| 76 | mTrebleNorm = normalizeBand(mTreble, mTrebleMaxFilter, dt); |
| 77 | } |
| 78 | |
| 79 | void FrequencyBands::fireCallbacks() { |
| 80 | if (onLevelsUpdate) { |
nothing calls this directly
no test coverage detected