@@ -304,7 +304,7 @@ class SoundAnalyzer : public ISoundAnalyzer
304304 // I'm old enough I can only hear up to about 12000Hz, but feel free to adjust. Remember from
305305 // school that you need to sample at double the frequency you want to process, so 24000 samples is 12000Hz
306306
307- static constexpr size_t SAMPLING_FREQUENCY = 20000 ;
307+ static constexpr size_t SAMPLING_FREQUENCY = 24000 ;
308308 static constexpr size_t LOWEST_FREQ = 100 ;
309309 static constexpr size_t HIGHEST_FREQ = SAMPLING_FREQUENCY / 2 ;
310310
@@ -324,6 +324,7 @@ class SoundAnalyzer : public ISoundAnalyzer
324324 float _oldPeakVU; // Old peak VU value for damping
325325 float _oldMinVU; // Old min VU value for damping
326326 float * _vPeaks; // Normalized band energies 0..1
327+ float * _livePeaks; // Attack-limited display peaks per band
327328 int _bandBinStart[NUM_BANDS ];
328329 int _bandBinEnd[NUM_BANDS ];
329330 float _energyMaxEnv = 1 .0f ; // adaptive envelope for autoscaling
@@ -346,6 +347,7 @@ class SoundAnalyzer : public ISoundAnalyzer
346347 //
347348
348349 private:
350+ static constexpr int kBandOffset = 1 ; // number of lowest source bands to skip in layout
349351 float *_vReal = nullptr ;
350352 float *_vImaginary = nullptr ;
351353 std::unique_ptr<int16_t []> ptrSampleBuffer; // sample buffer storage
@@ -455,7 +457,9 @@ class SoundAnalyzer : public ISoundAnalyzer
455457#endif
456458 for (int b = 0 ; b < NUM_BANDS ; b++)
457459 {
458- float fracHi = (float )(b + 1 ) / (float )NUM_BANDS ;
460+ // Shift the effective band index by kBandOffset so logical band 0 starts higher
461+ const int logicalIdx = b + kBandOffset ;
462+ const float fracHi = (float )(logicalIdx + 1 ) / (float )(NUM_BANDS + kBandOffset );
459463#if SPECTRUM_BAND_SCALE_MEL
460464 float edgeMel = melMin + (melMax - melMin) * fracHi;
461465 float edgeHiFreq = melToHz (edgeMel);
@@ -474,10 +478,31 @@ class SoundAnalyzer : public ISoundAnalyzer
474478
475479 PeakData ProcessPeaksEnergy ()
476480 {
477- float frameMax = 0 .0f ;
478- float frameRawMax = 0 .0f ; // max pre-subtraction band power (with compensation)
479- float noiseSum = 0 .0f ; // accumulate noise floor for mean
480- float noiseMaxAll = 0 .0f ; // track max noise floor across bands
481+ // Band offset handled in ComputeBandLayout so index 0 is the lowest VISIBLE band
482+ float frameMax = 0 .0f ;
483+ float frameSumRaw = 0 .0f ;
484+ float noiseSum = 0 .0f ;
485+ float noiseMaxAll = 0 .0f ; // Tracks max noise floor across bands
486+ // Display gain and band floor constants
487+ constexpr float kDisplayGain = 1 .5f ;
488+ constexpr float kBandFloorMin = 0 .01f ;
489+ constexpr float kBandFloorScale = 0 .05f ;
490+
491+ // Remember that a slower attack will yield a smoother display but a lower one, as it's always
492+ // below the actual for some amount of time
493+
494+ constexpr float kLiveAttackPerSec = 5 .0f ;
495+
496+ // Envelope gating factor and max relative EMA
497+ float _gateEnv = 1 .0f ;
498+ float _vMaxRelEMA = 1 .0f ;
499+ // Delta time for attack limiting (replace with actual frame time if available)
500+ float dt = 0 .016f ;
501+ // Allocate _livePeaks if not already done
502+ if (!_livePeaks) {
503+ _livePeaks = (float *)PreferPSRAMAlloc (NUM_BANDS * sizeof (_livePeaks[0 ]));
504+ for (int i = 0 ; i < NUM_BANDS ; ++i) _livePeaks[i] = 0 .0f ;
505+ }
481506 _framesProcessed++;
482507 const float binWidth = (float )SAMPLING_FREQUENCY / (MAX_SAMPLES / 2 .0f );
483508 for (int b = 0 ; b < NUM_BANDS ; b++)
@@ -504,8 +529,8 @@ class SoundAnalyzer : public ISoundAnalyzer
504529 avgPower *= compensation;
505530
506531 // Track pre-subtraction max for SNR gating
507- if (avgPower > frameRawMax )
508- frameRawMax = avgPower;
532+ if (avgPower > frameSumRaw )
533+ frameSumRaw = avgPower;
509534
510535 if (avgPower > _noiseFloor[b])
511536 _noiseFloor[b] = _noiseFloor[b] * (1 .0f - _params.energyNoiseAdapt ) + (float )avgPower * _params.energyNoiseAdapt ;
@@ -520,11 +545,20 @@ class SoundAnalyzer : public ISoundAnalyzer
520545 float signal = (float )avgPower - _noiseFloor[b];
521546 if (signal < 0 .0f )
522547 signal = 0 .0f ;
523- float smoothed = _rawPrev[b] * (1 .0f - _params.energySmoothAlpha ) + signal * _params.energySmoothAlpha ;
548+ #if ENABLE_AUDIO_SMOOTHING
549+ // Weighted average: 0.25 * (2 * current + left + right)
550+ float left = (b > 0 ) ? _rawPrev[b - 1 ] : signal;
551+ float right = (b < NUM_BANDS - 1 ) ? _rawPrev[b + 1 ] : signal;
552+ float smoothed = 0 .125f * (6 .0f * signal + left + right);
524553 _rawPrev[b] = smoothed;
525554 _vPeaks[b] = smoothed;
526555 if (smoothed > frameMax)
527556 frameMax = smoothed;
557+ #else
558+ _vPeaks[b] = signal;
559+ if (signal > frameMax)
560+ frameMax = signal;
561+ #endif
528562 }
529563 if (frameMax > _energyMaxEnv)
530564 _energyMaxEnv = frameMax;
@@ -535,7 +569,7 @@ class SoundAnalyzer : public ISoundAnalyzer
535569
536570 // Raw SNR-based frame gate (pre-normalization) to suppress steady HVAC and similar backgrounfd noises
537571
538- float snrRaw = frameRawMax / (noiseMaxAll + 1e-9f );
572+ float snrRaw = frameSumRaw / (noiseMaxAll + 1e-9f );
539573 if (snrRaw < _params.frameSNRGate )
540574 {
541575 _frameGateHits++;
@@ -599,22 +633,8 @@ class SoundAnalyzer : public ISoundAnalyzer
599633 }
600634#endif
601635
636+ const float invEnv = 1 .0f / normDen;
602637 float sumNorm = 0 .0f ;
603- for (int b = 0 ; b < NUM_BANDS ; b++)
604- {
605- float v = _vPeaks[b] * invEnv;
606-
607- // Per-band normalized noise gate
608- if (v < _params.normNoiseGate )
609- {
610- v = 0 .0f ;
611- _bandGateHits++;
612- }
613- else
614- {
615- // Cube root compression for smooth flattening without spikes
616- v = sqrt (cbrtf (std::max (0 .0f , v)));
617- }
618638
619639 // Apply post-scale to improve visible band intensity and clamp to [0,1]
620640 v *= _params.postScale ;
@@ -628,15 +648,25 @@ class SoundAnalyzer : public ISoundAnalyzer
628648#if ENABLE_AUDIO_SMOOTHING
629649 // Spatially smooth live spectrum peaks by blending with neighbors.
630650 // This affects displayed bars (and VU after we recompute below).
651+ // Now that layout skips the lowest bins, emit all NUM_BANDS directly with no reindexing
652+ for (int b = 0 ; b < NUM_BANDS ; b++)
631653 {
632- float tmp[NUM_BANDS ];
633- for (int b = 0 ; b < NUM_BANDS ; ++b)
654+ float vTarget = _vPeaks[b] * invEnv;
655+ vTarget = cbrtf (std::max (0 .0f , vTarget));
656+ if (vTarget > 1 .0f ) vTarget = 1 .0f ;
657+ vTarget *= _gateEnv;
658+ vTarget *= kDisplayGain ;
659+ if (vTarget > 1 .0f ) vTarget = 1 .0f ;
660+ const float bandFloor = std::max (kBandFloorMin , kBandFloorScale * _vMaxRelEMA);
661+ if (vTarget < bandFloor) { vTarget = 0 .0f ; _bandGateHits++; }
662+ float vCurr = _livePeaks[b];
663+ float vNew = vTarget;
664+ if (vTarget > vCurr)
634665 {
635- float v = _Peaks._Level [b];
636- float l = (b > 0 ) ? _Peaks._Level [b - 1 ] : v;
637- float r = (b < NUM_BANDS - 1 ) ? _Peaks._Level [b + 1 ] : v;
638- tmp[b] = (v * 2 .0f + l + r) * 0 .25f ; // (2*center + left + right) / 4
666+ const float maxRise = kLiveAttackPerSec * dt;
667+ vNew = vCurr + std::min (vTarget - vCurr, maxRise);
639668 }
669+
640670 for (int b = 0 ; b < NUM_BANDS ; ++b)
641671 {
642672 float v = tmp[b];
@@ -646,9 +676,14 @@ class SoundAnalyzer : public ISoundAnalyzer
646676 }
647677 // Recompute VU from smoothed values
648678 sumNorm = std::accumulate (&_Peaks._Level [0 ], &_Peaks._Level [0 ] + NUM_BANDS , 0 .0f );
679+ if (vNew > 1 .0f ) vNew = 1 .0f ;
680+ if (vNew < 0 .0f ) vNew = 0 .0f ;
681+ _livePeaks[b] = vNew;
682+ _vPeaks[b] = vNew;
683+ _Peaks._Level [b] = vNew;
684+ sumNorm += vNew;
649685 }
650- #endif
651- UpdateVU ((float )(sumNorm / NUM_BANDS ));
686+ UpdateVU (sumNorm / (float )NUM_BANDS);
652687 return _Peaks;
653688 }
654689
@@ -794,8 +829,10 @@ class SoundAnalyzer : public ISoundAnalyzer
794829 _vReal = (float *)PreferPSRAMAlloc (MAX_SAMPLES * sizeof (_vReal[0 ]));
795830 _vImaginary = (float *)PreferPSRAMAlloc (MAX_SAMPLES * sizeof (_vImaginary[0 ]));
796831 _vPeaks = (float *)PreferPSRAMAlloc (NUM_BANDS * sizeof (_vPeaks[0 ]));
797- if (!_vReal || !_vImaginary || !_vPeaks)
832+ _livePeaks = (float *)PreferPSRAMAlloc (NUM_BANDS * sizeof (_livePeaks[0 ]));
833+ if (!_vReal || !_vImaginary || !_vPeaks || !_livePeaks)
798834 throw std::runtime_error (" Failed to allocate FFT buffers" );
835+ for (int i = 0 ; i < NUM_BANDS ; ++i) _livePeaks[i] = 0 .0f ;
799836 _oldVU = _oldPeakVU = _oldMinVU = 0 .0f ;
800837 _MicMode = PeakData::MESMERIZERMIC ;
801838 _params = ParamsFor (_MicMode);
@@ -812,6 +849,7 @@ class SoundAnalyzer : public ISoundAnalyzer
812849 free (_vReal);
813850 free (_vImaginary);
814851 free (_vPeaks);
852+ free (_livePeaks);
815853 }
816854
817855 // These functions allow access to the last-acquired sample buffer and its size so that
0 commit comments