Skip to content

Commit 0fda135

Browse files
committed
docs(audio): add inline comments throughout audio crate
Add concise, high-signal inline comments to function bodies across the audio processing implementation. Comments explain intent, edge cases, boundary math, ownership/cloning decisions, and performance trade-offs rather than restating code behavior. Key areas covered: - Audio input (CPAL device handling, sample format conversion, threading) - EQ pipeline (FFT processing, log-spaced bands, smoothing, normalization) - VAD gate (hysteresis logic, timing accumulation, state transitions) - SFX player (oscillator, envelope, double beep sequencing)
1 parent e9b6f16 commit 0fda135

6 files changed

Lines changed: 157 additions & 13 deletions

File tree

keyless-audio/src/eq_pipeline.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ pub struct EqState {
5858
impl 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.
8486
pub 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()

keyless-audio/src/input/api.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,14 @@ pub fn default_input_name() -> keyless_core::error::KeylessResult<String> {
6767
pub fn list_input_device_names() -> keyless_core::error::KeylessResult<Vec<String>> {
6868
let host = cpal::default_host();
6969
let mut out = Vec::new();
70+
// Iterate all input devices; skip devices where name() fails (may be unplugged/permission issue).
7071
for dev in host.input_devices().map_err(|e| {
7172
keyless_core::error::KeylessError::Audio(format!(
7273
"failed to enumerate input devices: {}",
7374
e
7475
))
7576
})? {
77+
// Silently skip devices that can't provide a name (defensive; better than failing entire list).
7678
if let Ok(name) = dev.name() {
7779
out.push(name);
7880
}
@@ -84,6 +86,7 @@ pub fn list_input_device_names() -> keyless_core::error::KeylessResult<Vec<Strin
8486
pub fn list_input_devices() -> keyless_core::error::KeylessResult<Vec<InputDevice>> {
8587
let host = cpal::default_host();
8688
let mut out = Vec::new();
89+
// Wrap each device in Arc for shared ownership (allows cloning InputDevice handles).
8790
for dev in host.input_devices().map_err(|e| {
8891
keyless_core::error::KeylessError::Audio(format!(
8992
"failed to enumerate input devices: {}",
@@ -100,9 +103,11 @@ pub fn list_input_devices() -> keyless_core::error::KeylessResult<Vec<InputDevic
100103
/// Get the system default input device as an opaque handle.
101104
pub fn default_input_device() -> keyless_core::error::KeylessResult<InputDevice> {
102105
let host = cpal::default_host();
106+
// OS chooses default device (usually the primary microphone or first available).
103107
let dev = host.default_input_device().ok_or_else(|| {
104108
keyless_core::error::KeylessError::Audio("no input device available".to_string())
105109
})?;
110+
// Wrap in Arc for shared ownership (allows cloning the handle).
106111
Ok(InputDevice {
107112
inner: Arc::new(dev),
108113
})

0 commit comments

Comments
 (0)