Skip to main content

forge_audio/
dsp.rs

1//! DSP primitives: load, effects, write.
2
3use std::collections::VecDeque;
4
5/// Deterministic xorshift64 RNG. No heap, no deps.
6struct SimpleRng(u64);
7impl SimpleRng {
8    fn new(seed: u64) -> Self { Self(seed.wrapping_add(1)) }
9    fn next_u64(&mut self) -> u64 {
10        self.0 ^= self.0 << 13;
11        self.0 ^= self.0 >> 7;
12        self.0 ^= self.0 << 17;
13        self.0
14    }
15    fn next_f32(&mut self) -> f32 { (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32 }
16}
17
18/// Mono or stereo audio buffer at a known sample rate.
19#[derive(Debug, Clone)]
20pub struct AudioBuffer {
21    pub samples: Vec<Vec<f32>>,
22    pub sample_rate: u32,
23}
24
25impl AudioBuffer {
26    pub fn channels(&self) -> usize { self.samples.len() }
27    pub fn len(&self) -> usize { self.samples.first().map(|c| c.len()).unwrap_or(0) }
28    pub fn duration_secs(&self) -> f32 { self.len() as f32 / self.sample_rate as f32 }
29    pub fn to_mono(&self) -> Vec<f32> {
30        if self.channels() == 1 { return self.samples[0].clone(); }
31        let n = self.len();
32        let mut mono = vec![0.0; n];
33        let scale = 1.0 / self.channels() as f32;
34        for ch in &self.samples { for (i, &s) in ch.iter().enumerate() { mono[i] += s * scale; } }
35        mono
36    }
37}
38
39// --- I/O ---
40
41pub fn load_wav(path: &str) -> Result<AudioBuffer, String> {
42    let reader = hound::WavReader::open(path).map_err(|e| format!("WAV read: {}", e))?;
43    let spec = reader.spec();
44    let channels = spec.channels as usize;
45    let raw: Vec<f32> = match spec.sample_format {
46        hound::SampleFormat::Float => reader.into_samples::<f32>().map(|s| s.unwrap_or(0.0)).collect(),
47        hound::SampleFormat::Int => {
48            let max = (1i64 << (spec.bits_per_sample - 1)) as f32;
49            reader.into_samples::<i32>().map(|s| s.unwrap_or(0) as f32 / max).collect()
50        }
51    };
52    let mut samples = vec![Vec::new(); channels];
53    for (i, &s) in raw.iter().enumerate() { samples[i % channels].push(s); }
54    Ok(AudioBuffer { samples, sample_rate: spec.sample_rate })
55}
56
57pub fn write_wav(path: &str, buf: &AudioBuffer) -> Result<(), String> {
58    let spec = hound::WavSpec { channels: buf.channels() as u16, sample_rate: buf.sample_rate, bits_per_sample: 32, sample_format: hound::SampleFormat::Float };
59    let mut w = hound::WavWriter::create(path, spec).map_err(|e| format!("WAV write: {}", e))?;
60    for i in 0..buf.len() { for ch in &buf.samples { w.write_sample(ch[i]).map_err(|e| format!("{}", e))?; } }
61    w.finalize().map_err(|e| format!("{}", e))
62}
63
64/// Write game-ready audio: 16-bit WAV.
65/// For OGG Vorbis export, use external tools (ffmpeg) or wait for a pure-Rust Vorbis encoder.
66// TODO: Add OGG Vorbis export when a pure-Rust encoder crate becomes available.
67pub fn write_game_audio(path: &str, buf: &AudioBuffer) -> Result<(), String> {
68    let spec = hound::WavSpec {
69        channels: buf.channels() as u16,
70        sample_rate: buf.sample_rate,
71        bits_per_sample: 16,
72        sample_format: hound::SampleFormat::Int,
73    };
74    let mut w = hound::WavWriter::create(path, spec).map_err(|e| format!("WAV write: {}", e))?;
75    for i in 0..buf.len() {
76        for ch in &buf.samples {
77            let sample = (ch[i].clamp(-1.0, 1.0) * 32767.0) as i16;
78            w.write_sample(sample).map_err(|e| format!("{}", e))?;
79        }
80    }
81    w.finalize().map_err(|e| format!("{}", e))
82}
83
84/// Universal audio loader using symphonia. Supports MP3, FLAC, OGG, WAV, and AIFF.
85pub fn load_audio(path: &str) -> Result<AudioBuffer, String> {
86    use symphonia::core::audio::SampleBuffer;
87    use symphonia::core::codecs::DecoderOptions;
88    use symphonia::core::formats::FormatOptions;
89    use symphonia::core::io::MediaSourceStream;
90    use symphonia::core::meta::MetadataOptions;
91    use symphonia::core::probe::Hint;
92
93    // 1. Open file and create MediaSourceStream
94    let file = std::fs::File::open(path).map_err(|e| format!("open {}: {}", path, e))?;
95    let mss = MediaSourceStream::new(Box::new(file), Default::default());
96
97    // 2. Probe format using file extension hint
98    let mut hint = Hint::new();
99    if let Some(ext) = std::path::Path::new(path).extension().and_then(|e| e.to_str()) {
100        hint.with_extension(ext);
101    }
102
103    let probed = symphonia::default::get_probe()
104        .format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())
105        .map_err(|e| format!("probe {}: {}", path, e))?;
106
107    let mut format_reader = probed.format;
108
109    // 3. Get default track, extract sample_rate and channel count
110    let track = format_reader
111        .default_track()
112        .ok_or_else(|| "no default track".to_string())?;
113    let sample_rate = track.codec_params.sample_rate.ok_or("no sample rate")?;
114    let n_channels = track
115        .codec_params
116        .channels
117        .map(|c| c.count())
118        .unwrap_or(1);
119    let track_id = track.id;
120
121    let mut decoder = symphonia::default::get_codecs()
122        .make(&track.codec_params, &DecoderOptions::default())
123        .map_err(|e| format!("decoder: {}", e))?;
124
125    // 4. Decode all packets into interleaved f32 samples
126    let mut interleaved = Vec::<f32>::new();
127    loop {
128        let packet = match format_reader.next_packet() {
129            Ok(p) => p,
130            Err(symphonia::core::errors::Error::IoError(ref e))
131                if e.kind() == std::io::ErrorKind::UnexpectedEof =>
132            {
133                break;
134            }
135            Err(_) => break,
136        };
137        if packet.track_id() != track_id {
138            continue;
139        }
140        let decoded = match decoder.decode(&packet) {
141            Ok(d) => d,
142            Err(_) => continue,
143        };
144        let spec = *decoded.spec();
145        let duration = decoded.capacity();
146        let mut sample_buf = SampleBuffer::<f32>::new(duration as u64, spec);
147        sample_buf.copy_interleaved_ref(decoded);
148        interleaved.extend_from_slice(sample_buf.samples());
149    }
150
151    // 5. De-interleave into per-channel Vec<f32>
152    let mut samples = vec![Vec::new(); n_channels];
153    for (i, &s) in interleaved.iter().enumerate() {
154        samples[i % n_channels].push(s);
155    }
156
157    // 6. Return AudioBuffer
158    Ok(AudioBuffer {
159        samples,
160        sample_rate,
161    })
162}
163
164// --- Original effects ---
165
166pub fn time_stretch(buf: &AudioBuffer, factor: f64) -> Result<AudioBuffer, String> {
167    let new_len = (buf.len() as f64 * factor) as usize;
168    let samples = buf.samples.iter().map(|ch| {
169        (0..new_len).map(|i| {
170            let src = i as f64 / factor;
171            let idx = src as usize;
172            let frac = (src - idx as f64) as f32;
173            let a = ch.get(idx).copied().unwrap_or(0.0);
174            let b = ch.get(idx + 1).copied().unwrap_or(a);
175            a + (b - a) * frac
176        }).collect()
177    }).collect();
178    Ok(AudioBuffer { samples, sample_rate: buf.sample_rate })
179}
180
181pub fn pitch_shift(buf: &AudioBuffer, semitones: f32) -> Result<AudioBuffer, String> {
182    let ratio = 2.0_f64.powf(semitones as f64 / 12.0);
183    let mut stretched = time_stretch(buf, ratio)?;
184    stretched.sample_rate = buf.sample_rate;
185    Ok(stretched)
186}
187
188pub fn reverb(mut buf: AudioBuffer, room_size: f32, damping: f32, mix: f32) -> AudioBuffer {
189    let sr = buf.sample_rate as f32;
190    let comb_lengths = [(0.0297 * room_size * sr) as usize, (0.0371 * room_size * sr) as usize,
191                        (0.0411 * room_size * sr) as usize, (0.0437 * room_size * sr) as usize];
192    let ap_lengths = [(0.0050 * sr) as usize, (0.0017 * sr) as usize];
193    for ch in &mut buf.samples {
194        let mut comb_out = vec![0.0f32; ch.len()];
195        for &len in &comb_lengths {
196            let mut delay = VecDeque::from(vec![0.0f32; len.max(1)]);
197            let mut fs = 0.0f32;
198            for i in 0..ch.len() {
199                let d = delay.pop_front().unwrap_or(0.0);
200                fs = d * (1.0 - damping) + fs * damping;
201                delay.push_back(ch[i] + fs * 0.7);
202                comb_out[i] += d;
203            }
204        }
205        for &len in &ap_lengths {
206            let mut delay = VecDeque::from(vec![0.0f32; len.max(1)]);
207            for i in 0..comb_out.len() {
208                let d = delay.pop_front().unwrap_or(0.0);
209                let inp = comb_out[i];
210                comb_out[i] = d - 0.5 * inp;
211                delay.push_back(inp + 0.5 * d);
212            }
213        }
214        for i in 0..ch.len() { ch[i] = ch[i] * (1.0 - mix) + comb_out[i] * mix * 0.25; }
215    }
216    buf
217}
218
219pub fn delay(mut buf: AudioBuffer, time_ms: f32, feedback: f32, mix: f32) -> AudioBuffer {
220    let dl = (time_ms * 0.001 * buf.sample_rate as f32) as usize;
221    let dl = dl.max(1);
222    for ch in &mut buf.samples {
223        let mut ring = vec![0.0f32; dl];
224        let mut pos = 0usize;
225        for i in 0..ch.len() {
226            let d = ring[pos % dl];
227            ring[pos % dl] = ch[i] + d * feedback;
228            ch[i] = ch[i] * (1.0 - mix) + d * mix;
229            pos += 1;
230        }
231    }
232    buf
233}
234
235pub fn lowpass(mut buf: AudioBuffer, cutoff_hz: f32) -> AudioBuffer {
236    let alpha = {
237        let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_hz);
238        let dt = 1.0 / buf.sample_rate as f32;
239        dt / (rc + dt)
240    };
241    for ch in &mut buf.samples { let mut p = 0.0f32; for s in ch.iter_mut() { *s = p + alpha * (*s - p); p = *s; } }
242    buf
243}
244
245pub fn highpass(mut buf: AudioBuffer, cutoff_hz: f32) -> AudioBuffer {
246    let alpha = {
247        let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_hz);
248        let dt = 1.0 / buf.sample_rate as f32;
249        rc / (rc + dt)
250    };
251    for ch in &mut buf.samples {
252        let (mut pi, mut po) = (0.0f32, 0.0f32);
253        for s in ch.iter_mut() { let i = *s; *s = alpha * (po + i - pi); pi = i; po = *s; }
254    }
255    buf
256}
257
258pub fn granular(buf: &AudioBuffer, grain_ms: f32, density: f32, scatter: f32, seed: u64) -> AudioBuffer {
259    let mut rng = SimpleRng::new(seed);
260    let gl = (grain_ms * 0.001 * buf.sample_rate as f32) as usize;
261    let n = buf.len();
262    let mut out = AudioBuffer { samples: vec![vec![0.0; n]; buf.channels()], sample_rate: buf.sample_rate };
263    let num = (density * n as f32 / gl.max(1) as f32) as usize;
264    for _ in 0..num {
265        let ss = (rng.next_f32() * n.saturating_sub(gl) as f32) as usize;
266        let ds = ((rng.next_f32() * n as f32) as usize + (rng.next_f32() * scatter * n as f32) as usize) % n;
267        for i in 0..gl.min(n - ds) {
268            let env = hann(i, gl);
269            for c in 0..buf.channels() { if ss + i < buf.samples[c].len() { out.samples[c][ds + i] += buf.samples[c][ss + i] * env; } }
270        }
271    }
272    out
273}
274
275pub fn reverse(buf: &AudioBuffer) -> AudioBuffer {
276    let mut out = buf.clone();
277    for ch in &mut out.samples { ch.reverse(); }
278    out
279}
280
281// --- 10 new effects ---
282
283/// Phase vocoder time stretch (simplified overlap-add with windowed segments).
284pub fn phase_vocoder(buf: &AudioBuffer, stretch_factor: f32) -> AudioBuffer {
285    let win = 2048usize;
286    let hop_in = win / 4;
287    let hop_out = (hop_in as f32 * stretch_factor) as usize;
288    let hop_out = hop_out.max(1);
289    let new_len = (buf.len() as f32 * stretch_factor) as usize;
290    let mut out_samples = vec![vec![0.0f32; new_len]; buf.channels()];
291    for (ci, ch) in buf.samples.iter().enumerate() {
292        let mut out_pos = 0usize;
293        let mut in_pos = 0usize;
294        while in_pos + win <= ch.len() && out_pos + win <= new_len {
295            for j in 0..win {
296                let env = hann(j, win);
297                out_samples[ci][out_pos + j] += ch[in_pos + j] * env;
298            }
299            in_pos += hop_in;
300            out_pos += hop_out;
301        }
302    }
303    AudioBuffer { samples: out_samples, sample_rate: buf.sample_rate }
304}
305
306/// Notch (band-reject) filter.
307pub fn notch_filter(buf: &AudioBuffer, freq_hz: f32, q: f32, _gain_db: f32) -> AudioBuffer {
308    let mut out = buf.clone();
309    let w0 = 2.0 * std::f32::consts::PI * freq_hz / buf.sample_rate as f32;
310    let alpha = w0.sin() / (2.0 * q);
311    let b0 = 1.0; let b1 = -2.0 * w0.cos(); let b2 = 1.0;
312    let a0 = 1.0 + alpha; let a1 = -2.0 * w0.cos(); let a2 = 1.0 - alpha;
313    for ch in &mut out.samples { biquad_stateless(ch, b0/a0, b1/a0, b2/a0, a1/a0, a2/a0); }
314    out
315}
316
317/// LFO amplitude modulation.
318pub fn lfo_modulate(buf: &AudioBuffer, rate_hz: f32, depth: f32) -> AudioBuffer {
319    let mut out = buf.clone();
320    let sr = buf.sample_rate as f32;
321    for ch in &mut out.samples {
322        for (i, s) in ch.iter_mut().enumerate() {
323            let lfo = (2.0 * std::f32::consts::PI * rate_hz * i as f32 / sr).sin();
324            *s *= 1.0 + lfo * depth;
325        }
326    }
327    out
328}
329
330/// Reverse preswell: slice tail, reverse, prepend.
331pub fn reverse_preswell(buf: &AudioBuffer, segment_ms: f32) -> AudioBuffer {
332    let seg_len = (segment_ms * 0.001 * buf.sample_rate as f32) as usize;
333    if seg_len == 0 || seg_len >= buf.len() { return buf.clone(); }
334    let mut out = AudioBuffer { samples: Vec::new(), sample_rate: buf.sample_rate };
335    for ch in &buf.samples {
336        let tail_start = ch.len() - seg_len;
337        let mut tail: Vec<f32> = ch[tail_start..].to_vec();
338        tail.reverse();
339        // Crossfade envelope on the reversed tail
340        for (i, s) in tail.iter_mut().enumerate() { *s *= hann(i, seg_len); }
341        let mut combined = tail;
342        combined.extend_from_slice(ch);
343        out.samples.push(combined);
344    }
345    out
346}
347
348/// Bitcrush: reduce bit depth and sample rate.
349pub fn bitcrush(buf: &AudioBuffer, bit_depth: u32, target_rate: u32) -> AudioBuffer {
350    let mut out = buf.clone();
351    let levels = 2.0f32.powi(bit_depth.clamp(1, 32) as i32);
352    let hold_every = (buf.sample_rate / target_rate.max(1)).max(1) as usize;
353    for ch in &mut out.samples {
354        let mut held = 0.0f32;
355        for (i, s) in ch.iter_mut().enumerate() {
356            if i % hold_every == 0 { held = (*s * levels).round() / levels; }
357            *s = held;
358        }
359    }
360    out
361}
362
363/// Bandpass filter (lowpass + highpass).
364pub fn bandpass(buf: &AudioBuffer, low_hz: f32, high_hz: f32) -> AudioBuffer {
365    highpass(lowpass(buf.clone(), high_hz), low_hz)
366}
367
368/// 3-band EQ using biquad shelving/peaking filters (no phase-splitting).
369/// Low shelf at 200 Hz, mid peak at 1 kHz (Q=0.7), high shelf at 3 kHz.
370/// Audio EQ Cookbook coefficients (Robert Bristow-Johnson).
371///
372/// `state` is `[low/mid/high][channel]` — persists across blocks for click-free filtering.
373pub fn eq_3band(buf: &mut AudioBuffer, low_db: f32, mid_db: f32, high_db: f32, state: &mut [[BiquadState; 2]; 3]) {
374    if low_db.abs() < 0.5 && mid_db.abs() < 0.5 && high_db.abs() < 0.5 {
375        return;
376    }
377
378    let sr = buf.sample_rate as f32;
379
380    // Low shelf at 200 Hz
381    if low_db.abs() >= 0.5 {
382        let c = biquad_low_shelf(200.0, low_db, 0.7, sr);
383        // Coeffs are pre-normalized (divided by a0), so a0 = 1.0
384        let coeffs = [c.0, c.1, c.2, 1.0, c.3, c.4];
385        for (ch_idx, ch) in buf.samples.iter_mut().enumerate() {
386            biquad(ch, &coeffs, &mut state[0][ch_idx.min(1)]);
387        }
388    }
389
390    // Mid peak at 1 kHz
391    if mid_db.abs() >= 0.5 {
392        let c = biquad_peaking(1000.0, mid_db, 0.7, sr);
393        let coeffs = [c.0, c.1, c.2, 1.0, c.3, c.4];
394        for (ch_idx, ch) in buf.samples.iter_mut().enumerate() {
395            biquad(ch, &coeffs, &mut state[1][ch_idx.min(1)]);
396        }
397    }
398
399    // High shelf at 3 kHz
400    if high_db.abs() >= 0.5 {
401        let c = biquad_high_shelf(3000.0, high_db, 0.7, sr);
402        let coeffs = [c.0, c.1, c.2, 1.0, c.3, c.4];
403        for (ch_idx, ch) in buf.samples.iter_mut().enumerate() {
404            biquad(ch, &coeffs, &mut state[2][ch_idx.min(1)]);
405        }
406    }
407}
408
409/// Offline 3-band EQ (stateless — for non-realtime use like file processing).
410pub fn eq_3band_offline(buf: &AudioBuffer, low_db: f32, mid_db: f32, high_db: f32) -> AudioBuffer {
411    let mut out = buf.clone();
412    let mut state: [[BiquadState; 2]; 3] = Default::default();
413    eq_3band(&mut out, low_db, mid_db, high_db, &mut state);
414    out
415}
416
417// --- Helpers ---
418
419/// Persistent biquad filter state — must survive across audio blocks.
420#[derive(Default, Clone)]
421pub struct BiquadState {
422    pub x1: f32, pub x2: f32,
423    pub y1: f32, pub y2: f32,
424}
425
426fn hann(i: usize, len: usize) -> f32 {
427    if len <= 1 { return 1.0; }
428    0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (len - 1) as f32).cos())
429}
430
431/// Biquad peaking EQ coefficients (Audio EQ Cookbook).
432fn biquad_peaking(freq: f32, db_gain: f32, q: f32, sr: f32) -> (f32, f32, f32, f32, f32) {
433    let a = 10.0_f32.powf(db_gain / 40.0);
434    let w0 = 2.0 * std::f32::consts::PI * freq / sr;
435    let alpha = w0.sin() / (2.0 * q);
436    let a0 = 1.0 + alpha / a;
437    ( (1.0 + alpha * a) / a0,
438      (-2.0 * w0.cos()) / a0,
439      (1.0 - alpha * a) / a0,
440      (-2.0 * w0.cos()) / a0,
441      (1.0 - alpha / a) / a0 )
442}
443
444/// Biquad low shelf coefficients (Audio EQ Cookbook).
445fn biquad_low_shelf(freq: f32, db_gain: f32, q: f32, sr: f32) -> (f32, f32, f32, f32, f32) {
446    let a = 10.0_f32.powf(db_gain / 40.0);
447    let w0 = 2.0 * std::f32::consts::PI * freq / sr;
448    let alpha = w0.sin() / (2.0 * q);
449    let two_sqrt_a_alpha = 2.0 * a.sqrt() * alpha;
450    let a0 = (a + 1.0) + (a - 1.0) * w0.cos() + two_sqrt_a_alpha;
451    ( (a * ((a + 1.0) - (a - 1.0) * w0.cos() + two_sqrt_a_alpha)) / a0,
452      (2.0 * a * ((a - 1.0) - (a + 1.0) * w0.cos())) / a0,
453      (a * ((a + 1.0) - (a - 1.0) * w0.cos() - two_sqrt_a_alpha)) / a0,
454      (-2.0 * ((a - 1.0) + (a + 1.0) * w0.cos())) / a0,
455      ((a + 1.0) + (a - 1.0) * w0.cos() - two_sqrt_a_alpha) / a0 )
456}
457
458/// Biquad high shelf coefficients (Audio EQ Cookbook).
459fn biquad_high_shelf(freq: f32, db_gain: f32, q: f32, sr: f32) -> (f32, f32, f32, f32, f32) {
460    let a = 10.0_f32.powf(db_gain / 40.0);
461    let w0 = 2.0 * std::f32::consts::PI * freq / sr;
462    let alpha = w0.sin() / (2.0 * q);
463    let two_sqrt_a_alpha = 2.0 * a.sqrt() * alpha;
464    let a0 = (a + 1.0) - (a - 1.0) * w0.cos() + two_sqrt_a_alpha;
465    ( (a * ((a + 1.0) + (a - 1.0) * w0.cos() + two_sqrt_a_alpha)) / a0,
466      (-2.0 * a * ((a - 1.0) + (a + 1.0) * w0.cos())) / a0,
467      (a * ((a + 1.0) + (a - 1.0) * w0.cos() - two_sqrt_a_alpha)) / a0,
468      (2.0 * ((a - 1.0) - (a + 1.0) * w0.cos())) / a0,
469      ((a + 1.0) - (a - 1.0) * w0.cos() - two_sqrt_a_alpha) / a0 )
470}
471
472/// Biquad filter with persistent state (for real-time block-by-block processing).
473pub fn biquad(ch: &mut [f32], coeffs: &[f32; 6], state: &mut BiquadState) {
474    let (b0, b1, b2, a0, a1, a2) = (coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5]);
475    for s in ch.iter_mut() {
476        let x0 = *s;
477        let y0 = (b0 * x0 + b1 * state.x1 + b2 * state.x2 - a1 * state.y1 - a2 * state.y2) / a0;
478        state.x2 = state.x1; state.x1 = x0;
479        state.y2 = state.y1; state.y1 = y0;
480        *s = y0;
481    }
482}
483
484/// Resample an AudioBuffer to the target sample rate using linear interpolation.
485/// Good enough for playback normalization — sinc resampler comes later.
486pub fn resample(buf: &AudioBuffer, target_rate: u32) -> AudioBuffer {
487    if buf.sample_rate == target_rate {
488        return buf.clone();
489    }
490    let ratio = target_rate as f64 / buf.sample_rate as f64;
491    let new_len = (buf.len() as f64 * ratio) as usize;
492    let mut out_samples = Vec::with_capacity(buf.channels());
493
494    for ch in &buf.samples {
495        let mut resampled = Vec::with_capacity(new_len);
496        for i in 0..new_len {
497            let src_pos = i as f64 / ratio;
498            let idx = src_pos as usize;
499            let frac = (src_pos - idx as f64) as f32;
500            let a = ch.get(idx).copied().unwrap_or(0.0);
501            let b = ch.get(idx + 1).copied().unwrap_or(a);
502            resampled.push(a + (b - a) * frac);
503        }
504        out_samples.push(resampled);
505    }
506
507    AudioBuffer {
508        samples: out_samples,
509        sample_rate: target_rate,
510    }
511}
512
513/// Biquad filter with internal (zeroed) state — for offline/full-buffer processing.
514fn biquad_stateless(ch: &mut [f32], b0: f32, b1: f32, b2: f32, a1: f32, a2: f32) {
515    let (mut x1, mut x2, mut y1, mut y2) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
516    for s in ch.iter_mut() {
517        let x0 = *s;
518        let y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
519        x2 = x1; x1 = x0; y2 = y1; y1 = y0;
520        *s = y0;
521    }
522}
523
524/// Compute 3-band waveform energy at `resolution`-sample windows.
525/// Returns Vec<[low, mid, high]> with each value normalized 0.0–1.0.
526/// Bands: low <200Hz, mid 200–4000Hz, high >4000Hz.
527pub fn compute_waveform_bands(buf: &AudioBuffer, resolution: usize) -> Vec<[f32; 3]> {
528    let mono = buf.to_mono();
529    if mono.is_empty() || resolution == 0 { return vec![]; }
530    let sr = buf.sample_rate as f32;
531    let chunk = resolution.max(1);
532    let n_windows = (mono.len() + chunk - 1) / chunk;
533
534    // Biquad coefficients: lowpass at 200Hz, bandpass 200-4000Hz (via highpass+lowpass), highpass at 4000Hz
535    // We use simple 2nd-order filters: LP@200, HP@200 then LP@4000 for mid, HP@4000 for high
536    let lp200 = biquad_lp_coeffs(200.0, 0.707, sr);
537    let hp200 = biquad_hp_coeffs(200.0, 0.707, sr);
538    let lp4000 = biquad_lp_coeffs(4000.0, 0.707, sr);
539    let hp4000 = biquad_hp_coeffs(4000.0, 0.707, sr);
540
541    // Process full signal through filters
542    let mut low_sig = mono.clone();
543    let mut mid_sig = mono.clone();
544    let mut high_sig = mono.clone();
545
546    biquad_apply(&mut low_sig, &lp200);
547
548    biquad_apply(&mut mid_sig, &hp200);
549    biquad_apply(&mut mid_sig, &lp4000);
550
551    biquad_apply(&mut high_sig, &hp4000);
552
553    // Compute RMS per window, track global max
554    let mut result = Vec::with_capacity(n_windows);
555    let mut max_e = [0.0f32; 3];
556    for w in 0..n_windows {
557        let start = w * chunk;
558        let end = (start + chunk).min(mono.len());
559        let n = (end - start) as f32;
560        if n <= 0.0 { result.push([0.0; 3]); continue; }
561        let lo: f32 = low_sig[start..end].iter().map(|s| s * s).sum::<f32>() / n;
562        let mi: f32 = mid_sig[start..end].iter().map(|s| s * s).sum::<f32>() / n;
563        let hi: f32 = high_sig[start..end].iter().map(|s| s * s).sum::<f32>() / n;
564        let e = [lo.sqrt(), mi.sqrt(), hi.sqrt()];
565        for b in 0..3 { if e[b] > max_e[b] { max_e[b] = e[b]; } }
566        result.push(e);
567    }
568    // Normalize to 0.0–1.0
569    for r in &mut result {
570        for b in 0..3 {
571            if max_e[b] > 0.0 { r[b] /= max_e[b]; }
572        }
573    }
574    result
575}
576
577/// Simple 2nd-order lowpass biquad coefficients.
578fn biquad_lp_coeffs(freq: f32, q: f32, sr: f32) -> [f32; 5] {
579    let w0 = 2.0 * std::f32::consts::PI * freq / sr;
580    let alpha = w0.sin() / (2.0 * q);
581    let cos_w0 = w0.cos();
582    let a0 = 1.0 + alpha;
583    let b0 = ((1.0 - cos_w0) / 2.0) / a0;
584    let b1 = (1.0 - cos_w0) / a0;
585    let b2 = b0;
586    let a1 = (-2.0 * cos_w0) / a0;
587    let a2 = (1.0 - alpha) / a0;
588    [b0, b1, b2, a1, a2]
589}
590
591/// Simple 2nd-order highpass biquad coefficients.
592fn biquad_hp_coeffs(freq: f32, q: f32, sr: f32) -> [f32; 5] {
593    let w0 = 2.0 * std::f32::consts::PI * freq / sr;
594    let alpha = w0.sin() / (2.0 * q);
595    let cos_w0 = w0.cos();
596    let a0 = 1.0 + alpha;
597    let b0 = ((1.0 + cos_w0) / 2.0) / a0;
598    let b1 = (-(1.0 + cos_w0)) / a0;
599    let b2 = b0;
600    let a1 = (-2.0 * cos_w0) / a0;
601    let a2 = (1.0 - alpha) / a0;
602    [b0, b1, b2, a1, a2]
603}
604
605/// Apply biquad filter in-place (stateless, single pass).
606fn biquad_apply(samples: &mut [f32], coeffs: &[f32; 5]) {
607    let (b0, b1, b2, a1, a2) = (coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4]);
608    let (mut x1, mut x2, mut y1, mut y2) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
609    for s in samples.iter_mut() {
610        let x0 = *s;
611        let y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
612        x2 = x1; x1 = x0;
613        y2 = y1; y1 = y0;
614        *s = y0;
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621
622    fn sine(freq: f32, dur: f32, sr: u32) -> AudioBuffer {
623        let n = (dur * sr as f32) as usize;
624        let s = (0..n).map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / sr as f32).sin()).collect();
625        AudioBuffer { samples: vec![s], sample_rate: sr }
626    }
627
628    #[test] fn reverb_len() { let b = sine(440.0, 0.5, 44100); let n = b.len(); assert_eq!(reverb(b, 1.0, 0.5, 0.3).len(), n); }
629    #[test] fn lp_attenuates() { let b = sine(8000.0, 0.1, 44100); let b2 = sine(8000.0, 0.1, 44100); let o = lowpass(b, 200.0);
630        let ei: f32 = b2.samples[0].iter().map(|s| s*s).sum(); let eo: f32 = o.samples[0].iter().map(|s| s*s).sum();
631        assert!(eo < ei * 0.5); }
632    #[test] fn rev_involution() { let b = sine(440.0, 0.1, 44100); assert_eq!(b.samples[0], reverse(&reverse(&b)).samples[0]); }
633    #[test] fn notch_removes_freq() { let b = sine(1000.0, 0.2, 44100); let o = notch_filter(&b, 1000.0, 10.0, 0.0);
634        let ei: f32 = b.samples[0].iter().map(|s| s*s).sum(); let eo: f32 = o.samples[0].iter().map(|s| s*s).sum();
635        assert!(eo < ei * 0.3, "notch should remove target freq"); }
636    #[test] fn bitcrush_quantizes() { let b = sine(440.0, 0.1, 44100); let o = bitcrush(&b, 4, 8000);
637        let unique: std::collections::HashSet<u32> = o.samples[0].iter().map(|s| s.to_bits()).collect();
638        assert!(unique.len() < b.len() / 2, "bitcrush should reduce unique values"); }
639    #[test] fn bandpass_works() { let b = sine(440.0, 0.1, 44100); let o = bandpass(&b, 200.0, 800.0);
640        assert_eq!(o.len(), b.len()); }
641    #[test] fn phase_vocoder_stretches() { let b = sine(440.0, 0.5, 44100); let o = phase_vocoder(&b, 2.0);
642        assert!(o.len() > b.len(), "vocoder should stretch"); }
643    #[test] fn preswell_prepends() { let b = sine(440.0, 0.5, 44100); let o = reverse_preswell(&b, 100.0);
644        assert!(o.len() > b.len(), "preswell should add samples"); }
645}