@@ -86,6 +86,10 @@ struct AudioInputParams {
8686 float energyMinEnv; // Min envelope to avoid div-by-zero
8787 float bandCompLow; // Low-band compensation scalar
8888 float bandCompHigh; // High-band compensation scalar
89+ float frameSilenceGate; // Gate entire frame if below this after norm
90+ float normNoiseGate; // Gate individual bands below this after norm
91+ float envFloorFromNoise; // Multiplier on mean noise floor to cap normalization
92+ float frameSNRGate; // Gate frame if raw SNR (max/noiseMax) is below this
8993};
9094
9195// Mesmerizer (default) tuning
@@ -97,33 +101,17 @@ static constexpr AudioInputParams kParamsMesmerizer{
97101 0 .90f , // energyEnvDecay
98102 0 .000001f , // energyMinEnv
99103 0 .25f , // bandCompLow
100- 10 .0f // bandCompHigh
104+ 10 .0f , // bandCompHigh
105+ 0 .00f , // frameSilenceGate
106+ 0 .00f , // normNoiseGate
107+ 3 .0f , // envFloorFromNoise (cap auto-gain at ~1/4 of pure-noise)
108+ 0 .0f // frameSNRGate (require ~3:1 SNR to show frame)
101109};
102110
103111// Copies for now; adjust per device as needed
104112static constexpr AudioInputParams kParamsPCRemote = kParamsMesmerizer ;
105-
106- static constexpr AudioInputParams kParamsM5 {
107- 4 .0f , // windowPowerCorrection
108- 0 .02f , // energyNoiseAdapt
109- 0 .98f , // energyNoiseDecay
110- 0 .25f , // energySmoothAlpha
111- 0 .90f , // energyEnvDecay
112- 0 .000001f , // energyMinEnv
113- 0 .25f , // bandCompLow
114- 10 .0f // bandCompHigh
115- };
116-
117- static constexpr AudioInputParams kParamsM5Plus2 {
118- 4 .0f , // windowPowerCorrection
119- 0 .02f , // energyNoiseAdapt
120- 0 .98f , // energyNoiseDecay
121- 0 .25f , // energySmoothAlpha
122- 0 .90f , // energyEnvDecay
123- 0 .000001f , // energyMinEnv
124- 0 .25f , // bandCompLow
125- 10 .0f // bandCompHigh
126- };
113+ static constexpr AudioInputParams kParamsM5 = kParamsMesmerizer ;
114+ static constexpr AudioInputParams kParamsM5Plus2 = kParamsMesmerizer ;
127115
128116// PeakData
129117//
@@ -334,8 +322,8 @@ class SoundAnalyzer : public ISoundAnalyzer
334322 size_t _clipCount = 0 ; // number of clipped samples seen
335323 float _dcOffsetEMA = 0 .0f ; // exponential moving average of DC offset (abs)
336324 float _envEMA = 0 .0f ; // EMA of _energyMaxEnv
337- size_t _frameGateHits = 0 ; // frames gated by gate
338- size_t _bandGateHits = 0 ; // total band gate hits (kept for telemetry)
325+ size_t _frameGateHits = 0 ; // frames gated by params.frameSilenceGate
326+ size_t _bandGateHits = 0 ; // total band gate hits
339327 size_t _framesProcessed = 0 ; // total processed frames
340328
341329 float _oldVU; // Old VU value for damping
@@ -358,33 +346,6 @@ class SoundAnalyzer : public ISoundAnalyzer
358346 PeakData::MicrophoneType _MicMode;
359347 AudioInputParams _params; // Active tuning params for current mic mode
360348
361- // Gate envelope (absolute-level, hysteresis, with hold)
362- float _gateEnv = 1 .0f ; // 0..1 applied to bands and VU
363- uint32_t _gateHoldUntil = 0 ; // ms timestamp until which gate stays closed
364- uint32_t _gateOpenHoldUntil = 0 ; // ms timestamp until which gate stays open
365- float _frameSumEMA = 0 .0f ; // smoothed frame energy sum
366- float _vMaxRelEMA = 0 .0f ; // smoothed normalized max-band level
367-
368- // Gate tuning constants
369- static constexpr float kGateEnterK = 0 .60f ; // kept (unused by new gate but retained for debug)
370- static constexpr float kGateExitK = 1 .20f ; // kept (unused by new gate but retained for debug)
371- static constexpr float kGateAttackPerSec = 20 .0f ; // ~50ms full open
372- static constexpr float kGateReleasePerSec = 2 .0f ; // ~500ms full close
373- static constexpr uint32_t kGateHoldMs = 100 ; // minimum close hold time
374- static constexpr uint32_t kGateOpenHoldMs = 150 ; // minimum open hold time (reduced to avoid lingering on noise)
375- static constexpr float kGateEnterVMax = 0 .035f ; // close if vMaxRelEMA <= this (raise to suppress ambient)
376- static constexpr float kGateExitVMax = 0 .090f ; // open if vMaxRelEMA >= this (raise to require stronger music)
377-
378- // Limit auto-gain in silence: anchor normalization to a multiple of mean noise
379- static constexpr float kEnvNoiseFloorK = 37 .0f ; // denominator >= noiseMean * K
380-
381- // Post-gate display floor to suppress shimmer from tiny values
382- static constexpr float kBandFloorMin = 0 .015f ; // absolute min floor after gating
383- static constexpr float kBandFloorScale = 0 .15f ; // floor scales with vMaxRelEMA (higher during music)
384-
385- // Display gain (post-normalization, post-compression). Increase to use more vertical range.
386- static constexpr float kDisplayGain = 1 .5f ;
387-
388349 // Energy spectrum processing (implemented inline below)
389350 //
390351 // Calculate a logarithmic scale for the bands like you would find on a graphic equalizer display
@@ -408,7 +369,6 @@ class SoundAnalyzer : public ISoundAnalyzer
408369 if (_vImaginary)
409370 _vImaginary[i] = 0 .0f ;
410371 }
411- _cSamples = 0 ;
412372 for (int i = 0 ; i < NUM_BANDS ; i++)
413373 _vPeaks[i] = 0 .0f ;
414374 }
@@ -521,8 +481,9 @@ class SoundAnalyzer : public ISoundAnalyzer
521481 PeakData ProcessPeaksEnergy ()
522482 {
523483 float frameMax = 0 .0f ;
524- float frameSumRaw = 0 .0f ; // sum of noise-subtracted, smoothed band power (pre-normalization )
484+ float frameRawMax = 0 .0f ; // max pre-subtraction band power (with compensation )
525485 float noiseSum = 0 .0f ; // accumulate noise floor for mean
486+ float noiseMaxAll = 0 .0f ; // track max noise floor across bands
526487 _framesProcessed++;
527488 const float binWidth = (float )SAMPLING_FREQUENCY / (MAX_SAMPLES / 2 .0f );
528489 for (int b = 0 ; b < NUM_BANDS ; b++)
@@ -544,102 +505,141 @@ class SoundAnalyzer : public ISoundAnalyzer
544505 float avgPower = (widthBins > 0 ) ? (sumPower / (float )widthBins) : 0 .0f ;
545506 avgPower *= _params.windowPowerCorrection ;
546507
547- // Frequency -dependent compensation
508+ // Apply frequency -dependent compensation
548509 float compensation = _params.bandCompLow + (_params.bandCompHigh - _params.bandCompLow ) * ((float )b / (NUM_BANDS - 1 ));
549510 avgPower *= compensation;
550511
551- // Adaptive noise floor
512+ // Track pre-subtraction max for SNR gating
513+ if (avgPower > frameRawMax)
514+ frameRawMax = avgPower;
515+
552516 if (avgPower > _noiseFloor[b])
553517 _noiseFloor[b] = _noiseFloor[b] * (1 .0f - _params.energyNoiseAdapt ) + (float )avgPower * _params.energyNoiseAdapt ;
554518 else
555519 _noiseFloor[b] *= _params.energyNoiseDecay ;
556520
521+ // Accumulate noise stats
557522 noiseSum += _noiseFloor[b];
523+ if (_noiseFloor[b] > noiseMaxAll)
524+ noiseMaxAll = _noiseFloor[b];
558525
559526 float signal = (float )avgPower - _noiseFloor[b];
560- if (signal < 0 .0f ) signal = 0 .0f ;
527+ if (signal < 0 .0f )
528+ signal = 0 .0f ;
561529 float smoothed = _rawPrev[b] * (1 .0f - _params.energySmoothAlpha ) + signal * _params.energySmoothAlpha ;
562530 _rawPrev[b] = smoothed;
563- _vPeaks[b] = smoothed; // store pre-normalized, smoothed signal
564- frameSumRaw += smoothed;
531+ _vPeaks[b] = smoothed;
565532 if (smoothed > frameMax)
566533 frameMax = smoothed;
567534 }
568-
569- // Update autoscale envelope
570535 if (frameMax > _energyMaxEnv)
571536 _energyMaxEnv = frameMax;
572537 else
573538 _energyMaxEnv = std::max (_params.energyMinEnv , _energyMaxEnv * _params.energyEnvDecay );
574539
575540 _envEMA = _envEMA * 0 .95f + _energyMaxEnv * 0 .05f ;
576541
577- // Timing and smoothing factors
578- const float dt = std::max (0 .0f , (float )g_Values.AppTime .LastFrameTime ());
579-
580- // Optional: keep frame-sum EMA for diagnostics (not used for gating now)
581- const float emaAlphaFrame = std::clamp (dt * 5 .0f , 0 .0f , 1 .0f ); // ~200ms time constant
582- _frameSumEMA = _frameSumEMA * (1 .0f - emaAlphaFrame) + frameSumRaw * emaAlphaFrame;
583-
584- // Gate based on normalized max band value relative to adaptive envelope,
585- // with auto-gain capped by noise-anchored floor
586- const float noiseMean = noiseSum / (float )NUM_BANDS ;
587- const float envFloor = std::max (_params.energyMinEnv , noiseMean * kEnvNoiseFloorK );
588- const float normDen = std::max (_energyMaxEnv, envFloor);
589- const float vMaxRel = frameMax / normDen;
590- const float emaAlphaV = std::clamp (dt * 6 .0f , 0 .0f , 1 .0f ); // ~167ms time constant
591- _vMaxRelEMA = _vMaxRelEMA * (1 .0f - emaAlphaV) + vMaxRel * emaAlphaV;
592-
593- const uint32_t now = millis ();
594- bool targetOpen = _gateEnv > 0 .0f ;
595- if (_vMaxRelEMA <= kGateEnterVMax ) {
596- targetOpen = false ;
542+ // Raw SNR-based frame gate (pre-normalization) to suppress steady HVAC and similar backgrounfd noises
543+
544+ float snrRaw = frameRawMax / (noiseMaxAll + 1e-9f );
545+ if (snrRaw < _params.frameSNRGate )
546+ {
597547 _frameGateHits++;
598- if (_gateHoldUntil < now + kGateHoldMs )
599- _gateHoldUntil = now + kGateHoldMs ;
600- } else if (_vMaxRelEMA >= kGateExitVMax ) {
601- targetOpen = true ;
602- if (_gateOpenHoldUntil < now + kGateOpenHoldMs )
603- _gateOpenHoldUntil = now + kGateOpenHoldMs ;
548+ for (int b = 0 ; b < NUM_BANDS ; ++b)
549+ {
550+ _vPeaks[b] = 0 .0f ;
551+ _Peaks._Level [b] = 0 .0f ;
552+ }
553+ UpdateVU (0 .0f );
554+ return _Peaks;
604555 }
605556
606- // Apply holds (closed-hold has priority to prevent flicker right after closing)
607- if (now < _gateHoldUntil)
608- targetOpen = false ;
609- else if (now < _gateOpenHoldUntil)
610- targetOpen = true ;
557+ // Anchor normalization to noise-derived floor to avoid auto-gain blow-up
558+ float noiseMean = noiseSum / ( float ) NUM_BANDS ;
559+ float envFloor = std::max (_params. energyMinEnv , noiseMean * _params. envFloorFromNoise ) ;
560+ float normDen = std::max (_energyMaxEnv, envFloor);
561+ float invEnv = 1 . 0f / normDen ;
611562
612- // Attack/release envelope on gate
613- if (targetOpen)
614- _gateEnv = std::min (1 .0f , _gateEnv + dt * kGateAttackPerSec );
615- else
616- _gateEnv = std::max (0 .0f , _gateEnv - dt * kGateReleasePerSec );
563+ // Frame-level silence gate on normalized values
564+ float vMaxNorm = 0 .0f ;
565+ for (int b = 0 ; b < NUM_BANDS ; ++b)
566+ {
567+ float vNorm = _vPeaks[b] * invEnv;
568+ if (vNorm > vMaxNorm)
569+ vMaxNorm = vNorm;
570+ }
571+ if (vMaxNorm < _params.frameSilenceGate )
572+ {
573+ _frameGateHits++;
574+ for (int b = 0 ; b < NUM_BANDS ; ++b)
575+ {
576+ _vPeaks[b] = 0 .0f ;
577+ _Peaks._Level [b] = 0 .0f ;
578+ }
579+ UpdateVU (0 .0f );
580+ return _Peaks;
581+ }
617582
618- // Simple normalization using autoscale envelope with noise-anchored floor
619- const float invEnv = 1 .0f / normDen;
583+ #if ENABLE_AUDIO_DEBUG
584+ // Debug HVAC issues: log when frame gate should trigger but doesn't
585+ static uint32_t lastDebugMs = 0 ;
586+ if (millis () - lastDebugMs > 1000 ) // once per second
587+ {
588+ Serial.printf (" FrameGate: vMaxNorm=%.3f, gate=%.3f, envelope=%.1f, hits=%zu\n " ,
589+ vMaxNorm, _params.frameSilenceGate , _energyMaxEnv, _frameGateHits);
590+ lastDebugMs = millis ();
591+ }
592+ #endif
620593
621594 float sumNorm = 0 .0f ;
622595 for (int b = 0 ; b < NUM_BANDS ; b++)
623596 {
624597 float v = _vPeaks[b] * invEnv;
625- // Soft compression for smooth visuals
626- v = cbrtf (std::max (0 .0f , v));
627- if (v > 1 .0f ) v = 1 .0f ;
628- v *= _gateEnv; // apply gate envelope
629598
630- // Apply additional display gain and clamp
631- v *= kDisplayGain ;
632- if (v > 1 .0f ) v = 1 .0f ;
633-
634- // Apply display floor to remove tiny ambient shimmer
635- const float bandFloor = std::max (kBandFloorMin , kBandFloorScale * _vMaxRelEMA);
636- if (v < bandFloor) { v = 0 .0f ; _bandGateHits++; }
599+ // Per-band normalized noise gate
600+ if (v < _params.normNoiseGate )
601+ {
602+ v = 0 .0f ;
603+ _bandGateHits++;
604+ }
605+ else
606+ {
607+ // Cube root compression for smooth flattening without spikes
608+ v = sqrt (cbrtf (std::max (0 .0f , v)));
609+ }
637610
611+ if (v > 1 .0f )
612+ v = 1 .0f ;
638613 _vPeaks[b] = v;
639614 _Peaks._Level [b] = v;
640615 sumNorm += v;
641616 }
642617
618+ #if ENABLE_AUDIO_SMOOTHING
619+ // Spatially smooth live spectrum peaks by blending with neighbors.
620+ // This affects displayed bars (and VU after we recompute below).
621+ {
622+ float tmp[NUM_BANDS ];
623+ for (int b = 0 ; b < NUM_BANDS ; ++b)
624+ {
625+ float v = _Peaks._Level [b];
626+ float l = (b > 0 ) ? _Peaks._Level [b - 1 ] : v;
627+ float r = (b < NUM_BANDS - 1 ) ? _Peaks._Level [b + 1 ] : v;
628+ tmp[b] = (v * 2 .0f + l + r) * 0 .25f ; // (2*center + left + right) / 4
629+ }
630+ for (int b = 0 ; b < NUM_BANDS ; ++b)
631+ {
632+ float v = tmp[b];
633+ if (v > 1 .0f ) v = 1 .0f ;
634+ _vPeaks[b] = v;
635+ _Peaks._Level [b] = v;
636+ }
637+ // Recompute VU from smoothed values
638+ sumNorm = 0 .0f ;
639+ for (int b = 0 ; b < NUM_BANDS ; ++b)
640+ sumNorm += _Peaks._Level [b];
641+ }
642+ #endif
643643 UpdateVU ((float )(sumNorm / NUM_BANDS ));
644644 return _Peaks;
645645 }
0 commit comments