@@ -58,9 +58,10 @@ pub struct EqState {
5858impl EqState {
5959 /// Create a new state with cached FFT/buffers for the given layout.
6060 pub fn new ( bands : usize , nfft : usize ) -> Self {
61- // Initialize FFT planner and plan once
61+ // Initialize FFT planner and plan once (expensive operation, cached for reuse).
6262 let mut planner = RealFftPlanner :: < f32 > :: new ( ) ;
6363 let fft_plan = planner. plan_fft_forward ( nfft) ;
64+ // Real FFT produces N/2+1 complex bins (second half is mirror image).
6465 let spectrum_size = fft_plan. complex_len ( ) ;
6566
6667 let scratch_size = fft_plan. get_scratch_len ( ) ;
@@ -71,6 +72,7 @@ impl EqState {
7172 input_buf : vec ! [ 0.0 ; nfft] ,
7273 spectrum_buf : vec ! [ Complex32 :: new( 0.0 , 0.0 ) ; spectrum_size] ,
7374 scratch_buf : vec ! [ Complex32 :: new( 0.0 , 0.0 ) ; scratch_size] ,
75+ // Use with_capacity (not fixed size) since mags_buf length = spectrum_size (varies by FFT).
7476 mags_buf : Vec :: with_capacity ( spectrum_size) ,
7577 band_mags_buf : vec ! [ 0.0 ; bands] ,
7678 bars_buf : vec ! [ 0.0 ; bands] ,
@@ -82,6 +84,8 @@ impl EqState {
8284/// Uses auto-sensitivity (max normalization) for consistent visualization.
8385/// All buffers are reused from state for zero allocations.
8486pub fn compute_bars ( frame : & [ f32 ] , cfg : & EqConfig , state : & mut EqState ) -> Vec < u16 > {
87+ // Early return if frame too short: can't fill FFT window, reuse previous smoothed values.
88+ // This handles startup/edge cases where buffer hasn't accumulated enough samples yet.
8589 if frame. len ( ) < cfg. nfft {
8690 // Not enough samples; keep the current smooth values
8791 return state
@@ -100,8 +104,11 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
100104 // Fix: Multiply each sample by a "window" that fades to zero at both ends (bell curve shape).
101105 // Hann window formula: w[i] = 0.5 - 0.5 * cos(2π * i / N)
102106 // This gives smooth transitions and cleaner frequency bins.
107+ // Take last nfft samples (most recent audio); ensures we analyze latest data.
103108 let start = frame. len ( ) - cfg. nfft ;
104109 for ( i, s) in frame[ start..] . iter ( ) . enumerate ( ) {
110+ // Hann window: symmetric bell curve (peaks at center, fades to 0 at edges).
111+ // Formula: 0.5 - 0.5*cos(2π*i/N) produces values in [0, 1] range.
105112 let w = 0.5 - 0.5 * ( ( 2.0 * std:: f32:: consts:: PI * i as f32 ) / ( cfg. nfft as f32 ) ) . cos ( ) ;
106113 state. input_buf [ i] = * s * w;
107114 }
@@ -113,6 +120,7 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
113120 //
114121 // Output: Complex numbers (real + imaginary). Each complex number represents one frequency bin.
115122 // We only care about the first half because the second half is a mirror (real FFT property).
123+ // Input buffer is modified in-place; spectrum_buf receives frequency-domain output.
116124 let _ = state. fft_plan . process_with_scratch (
117125 & mut state. input_buf ,
118126 & mut state. spectrum_buf ,
@@ -126,9 +134,12 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
126134 //
127135 // Analogy: Think of (re, im) as a 2D point. The magnitude is its distance from origin.
128136 // This gives us a single "loudness" value per frequency bin.
137+ // Real FFT produces N/2+1 bins (mirror property); we only need first half for visualization.
129138 let half = state. spectrum_buf . len ( ) ;
139+ // Clear and reuse mags_buf (with_capacity avoids realloc, clear resets length to 0).
130140 state. mags_buf . clear ( ) ;
131141 for c in & state. spectrum_buf [ 0 ..half] {
142+ // Magnitude = sqrt(re² + im²); represents amplitude/loudness of this frequency bin.
132143 state. mags_buf . push ( ( c. re * c. re + c. im * c. im ) . sqrt ( ) ) ;
133144 }
134145
@@ -144,19 +155,27 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
144155 // Math: f(i) = start_hz * (nyquist / 50)^(i / (bands - 1))
145156 // This spreads bands from ~120 Hz (low bass) to Nyquist (half the sample rate) on a log scale.
146157 let nyquist = cfg. sample_rate_hz / 2.0 ;
158+ // Reset band magnitudes to zero before accumulation (reuse buffer).
147159 state. band_mags_buf . fill ( 0.0 ) ;
148160 for ( i, band_mag) in state. band_mags_buf . iter_mut ( ) . enumerate ( ) {
149- // Compute frequency range for this band (exponential growth)
161+ // Compute frequency range for this band (exponential growth).
162+ // Formula spreads bands logarithmically from start_hz to nyquist.
163+ // Division by (bands - 1) ensures last band ends at nyquist (i = bands - 1).
150164 let f0 = cfg. start_hz * ( nyquist / 50.0 ) . powf ( i as f32 / ( cfg. bands - 1 ) as f32 ) ;
151165 let f1 = cfg. start_hz * ( nyquist / 50.0 ) . powf ( ( i + 1 ) as f32 / ( cfg. bands - 1 ) as f32 ) ;
152166
153- // Map frequency range to FFT bin indices
167+ // Map frequency range to FFT bin indices (linear mapping: freq → bin).
168+ // Clamp to valid bin range [0, half-1] to prevent OOB access.
169+ // half-1 because bins are 0-indexed; last valid bin is at index half-1.
154170 let bin0 = ( ( f0 / nyquist) * ( half as f32 - 1.0 ) ) . clamp ( 0.0 , half as f32 - 1.0 ) as usize ;
155171 let bin1 = ( ( f1 / nyquist) * ( half as f32 - 1.0 ) ) . clamp ( 0.0 , half as f32 - 1.0 ) as usize ;
156172
157- // Find the loudest frequency in this band
173+ // Find the loudest frequency in this band (peak detection, not average).
174+ // Max preserves sharp peaks better than averaging (important for visualization).
158175 let mut max_v = 0.0f32 ;
176+ // Inclusive range bin0..=bin1; max(bin0) handles edge case where bin1 < bin0 (shouldn't happen).
159177 for b in bin0..=bin1. max ( bin0) {
178+ // Bounds check: get() returns None if index OOB (defensive).
160179 if let Some ( & m) = state. mags_buf . get ( b)
161180 && m > max_v
162181 {
@@ -173,13 +192,15 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
173192 //
174193 // Fix: Find the loudest band right now, and normalize all bands relative to that max.
175194 // This ensures bars fill the screen consistently regardless of absolute volume.
195+ // Fold finds maximum magnitude across all bands (O(n) scan).
176196 let max_mag = state. band_mags_buf . iter ( ) . copied ( ) . fold ( 0.0f32 , f32:: max) ;
177197
178198 // Step 6: Shape the bars for clean visualization
179199 for ( i, bar) in state. bars_buf . iter_mut ( ) . enumerate ( ) {
180200 let mag = state. band_mags_buf [ i] ;
181201
182- // Normalize against the loudest band (0.0 = silence, 1.0 = loudest right now)
202+ // Normalize against the loudest band (0.0 = silence, 1.0 = loudest right now).
203+ // Check max_mag > 1e-12 to avoid division by zero (silence case).
183204 let normalized = if max_mag > 1e-12 { mag / max_mag } else { 0.0 } ;
184205
185206 // Apply noise reduction: filter out low-level background noise
@@ -188,18 +209,24 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
188209 // the range (threshold..1.0) back to (0..1.0) so the visualizer stays smooth.
189210 //
190211 // Example: threshold=0.3, band=0.5 → (0.5 - 0.3) / (1.0 - 0.3) = 0.29 (rescaled)
212+ // Rescaling preserves relative differences above threshold while suppressing noise below.
191213 let filtered = if normalized < cfg. noise_reduction {
192214 0.0
193215 } else {
216+ // Linear rescale: maps [threshold, 1.0] → [0.0, 1.0].
194217 ( normalized - cfg. noise_reduction ) / ( 1.0 - cfg. noise_reduction )
195218 } ;
196219
197220 // Apply perceptual shaping (window_db and gamma curve)
198221 //
199222 // window_db: Compresses dynamic range (makes quiet sounds more visible)
200223 // gamma: Power curve for perceptual brightness (like monitor gamma correction)
224+ // Clamp filtered*window_db to [0, window_db] then normalize back to [0, 1].
225+ // This effectively applies window_db as a scaling factor.
201226 let scaled = ( filtered * cfg. window_db ) . min ( cfg. window_db ) / cfg. window_db ;
227+ // Gamma curve: pow(scaled, gamma) darkens (gamma>1) or brightens (gamma<1) the visualization.
202228 let curved = scaled. powf ( cfg. gamma ) ;
229+ // Convert to 0..100 range for final bar height; clamp prevents overflow.
203230 * bar = ( curved * 100.0 ) . clamp ( 0.0 , 100.0 ) ;
204231 }
205232
@@ -216,13 +243,19 @@ pub fn compute_bars(frame: &[f32], cfg: &EqConfig, state: &mut EqState) -> Vec<u
216243 // Formula: new_value = old_value + α * (target - old_value)
217244 // α = attack (if rising) or decay (if falling); typical values 0.35 attack, 0.12 decay
218245 for i in 0 ..cfg. bands {
246+ // Bounds check: get_mut() returns None if index OOB (defensive, shouldn't happen).
219247 if let Some ( s) = state. smooth_bars . get_mut ( i) {
248+ // Get target bar value; fallback to 0.0 if index OOB (defensive).
220249 let target = state. bars_buf . get ( i) . copied ( ) . unwrap_or ( 0.0 ) ;
250+ // Choose smoothing factor: attack for rising, decay for falling (asymmetric response).
221251 let alpha = if target > * s { cfg. attack } else { cfg. decay } ;
252+ // Exponential smoothing: move fraction α of the way toward target each frame.
222253 * s = * s + alpha * ( target - * s) ;
223254 }
224255 }
225256
257+ // Convert smoothed bars to u16 (0..100 range) for final output.
258+ // Clamp to [0, 100] to handle any floating-point precision issues or smoothing overshoot.
226259 state
227260 . smooth_bars
228261 . iter ( )
0 commit comments