simple_editor\engine/
mixer_fx.rs

1//! Audio filters and bus routing: the DSP behind the mixer panel.
2//!
3//! `BusGraph` resolves `Project.buses` (Main first) into an evaluation order and owns one `FilterState`
4//! per (bus, filter) so IIR/delay state survives across blocks. Clips sum into their bus
5//! (`Project::bus_of`), each bus runs its filter chain, then gain/pan/mono, then sums into its output bus.
6//! Interleaved stereo f32 @ 48 kHz, processed in place, block-continuous.
7//!
8//! Filters (`model::FilterKind`): Eq (5-band RBJ: low shelf, three peaks, high shelf — `EQ_BANDS` maps
9//! bands to parameter indices), HighPass/LowPass (RBJ),
10//! Reverb (Freeverb comb+allpass), Echo (delay line + feedback, optional ping-pong), Distortion (soft clip
11//! + tone), Compressor (peak detector, attack/release, makeup), NoiseGate, Noise (white/pink/tone), Gain.
12//!
13//! Parameters: every `AudioFilter` param is an `Animated` read **once per block** at the block-start
14//! timeline time `t` (`AudioFilter::at(i, t)`). Coefficients / delay lengths / thresholds are recomputed
15//! from those values and held constant for the block, so automation steps at block boundaries
16//! (≤ ~100 ms). The two params where a step is audible as a click — the Gain filter's gain and the Noise
17//! filter's level — are ramped linearly from the previous block's value across the block. All buffers
18//! (delay lines, comb/allpass memories, bus scratch) are sized at construction / first block, so steady
19//! state allocates nothing.
20
21use crate::media::SAMPLE_RATE;
22use crate::model::{AudioFilter, Bus, FilterKind, Id, Project};
23
24/// Linear amplitude of a dB value.
25pub fn db_to_lin(db: f32) -> f32 {
26    10f32.powf(db / 20.0)
27}
28
29/// dB of a linear amplitude (floored at -120 dB).
30pub fn lin_to_db(v: f32) -> f32 {
31    20.0 * v.abs().max(1e-6).log10()
32}
33
34/// One-pole smoothing coefficient for a time constant in ms.
35fn coef(ms: f32, sr: f32) -> f32 {
36    let n = (ms.max(0.01) * 0.001 * sr).max(1.0);
37    1.0 - (-1.0 / n).exp()
38}
39
40// ---------- biquad ----------
41
42/// RBJ band shapes used by the EQ and the pass filters.
43#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44pub enum Band {
45    LowPass,
46    HighPass,
47    Peak,
48    LowShelf,
49    HighShelf,
50}
51
52/// Normalised RBJ coefficients `[b0, b1, b2, a1, a2]` (a0 divided out). Shelves take Q like the peaks
53/// (Q = 0.707 is the classic slope-1 shelf).
54pub fn coeffs(band: Band, f0: f32, q: f32, gain_db: f32, sr: f32) -> [f32; 5] {
55    let f0 = f0.clamp(10.0, sr * 0.45);
56    let q = q.clamp(0.05, 20.0);
57    let w0 = std::f32::consts::TAU * f0 / sr;
58    let (sn, cs) = w0.sin_cos();
59    let alpha = sn / (2.0 * q);
60    let a = 10f32.powf(gain_db / 40.0);
61    let tsa = 2.0 * a.sqrt() * alpha;
62    let (b0, b1, b2, a0, a1, a2) = match band {
63        Band::LowPass => ((1.0 - cs) * 0.5, 1.0 - cs, (1.0 - cs) * 0.5, 1.0 + alpha, -2.0 * cs, 1.0 - alpha),
64        Band::HighPass => ((1.0 + cs) * 0.5, -(1.0 + cs), (1.0 + cs) * 0.5, 1.0 + alpha, -2.0 * cs, 1.0 - alpha),
65        Band::Peak => (1.0 + alpha * a, -2.0 * cs, 1.0 - alpha * a, 1.0 + alpha / a, -2.0 * cs, 1.0 - alpha / a),
66        Band::LowShelf => (
67            a * ((a + 1.0) - (a - 1.0) * cs + tsa),
68            2.0 * a * ((a - 1.0) - (a + 1.0) * cs),
69            a * ((a + 1.0) - (a - 1.0) * cs - tsa),
70            (a + 1.0) + (a - 1.0) * cs + tsa,
71            -2.0 * ((a - 1.0) + (a + 1.0) * cs),
72            (a + 1.0) + (a - 1.0) * cs - tsa,
73        ),
74        Band::HighShelf => (
75            a * ((a + 1.0) + (a - 1.0) * cs + tsa),
76            -2.0 * a * ((a - 1.0) + (a + 1.0) * cs),
77            a * ((a + 1.0) + (a - 1.0) * cs - tsa),
78            (a + 1.0) - (a - 1.0) * cs + tsa,
79            2.0 * ((a - 1.0) - (a + 1.0) * cs),
80            (a + 1.0) - (a - 1.0) * cs - tsa,
81        ),
82    };
83    if a0.abs() < 1e-12 {
84        return [1.0, 0.0, 0.0, 0.0, 0.0];
85    }
86    [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0]
87}
88
89/// |H(e^jw)| in dB at `freq` for one set of normalised coefficients.
90pub fn response_db(c: &[f32; 5], freq: f32, sr: f32) -> f32 {
91    let w = std::f32::consts::TAU * freq.clamp(1.0, sr * 0.5) / sr;
92    let (s1, c1) = (-w).sin_cos();
93    let (s2, c2) = (-2.0 * w).sin_cos();
94    let nr = c[0] + c[1] * c1 + c[2] * c2;
95    let ni = c[1] * s1 + c[2] * s2;
96    let dr = 1.0 + c[3] * c1 + c[4] * c2;
97    let di = c[3] * s1 + c[4] * s2;
98    let num = (nr * nr + ni * ni).sqrt();
99    let den = (dr * dr + di * di).sqrt().max(1e-12);
100    lin_to_db(num / den)
101}
102
103/// The EQ's five bands, low to high, as `(shape, gain param, freq param, Q param)`. The first seven
104/// `F_EQ` slots keep the meaning the 3-band EQ gave them, so projects saved before the extra peaks
105/// still load — hence the scattered indices.
106pub const EQ_BANDS: [(Band, usize, usize, usize); 5] = [
107    (Band::LowShelf, 0, 1, 7),
108    (Band::Peak, 9, 10, 11),
109    (Band::Peak, 2, 3, 4),
110    (Band::Peak, 12, 13, 14),
111    (Band::HighShelf, 5, 6, 8),
112];
113
114/// The bands an EQ / pass filter is made of at time `t`, as `(band, freq, q, gain_db)`.
115/// Empty for every other kind — the UI draws a response curve exactly when this is non-empty.
116pub fn filter_bands(f: &AudioFilter, t: f64) -> Vec<(Band, f32, f32, f32)> {
117    let p = |i: usize| f.at(i, t) as f32;
118    match f.kind {
119        FilterKind::Eq => EQ_BANDS.iter().map(|&(b, gi, fi, qi)| (b, p(fi), p(qi), p(gi))).collect(),
120        FilterKind::HighPass => vec![(Band::HighPass, p(0), p(1), 0.0)],
121        FilterKind::LowPass => vec![(Band::LowPass, p(0), p(1), 0.0)],
122        _ => Vec::new(),
123    }
124}
125
126/// Total response of an EQ / pass filter at `freq`, in dB (0 for other kinds).
127pub fn filter_response_db(f: &AudioFilter, t: f64, freq: f32) -> f32 {
128    let sr = SAMPLE_RATE as f32;
129    filter_bands(f, t).iter().map(|&(b, f0, q, g)| response_db(&coeffs(b, f0, q, g, sr), freq, sr)).sum()
130}
131
132/// Transposed direct-form II biquad, one per channel per band.
133#[derive(Clone, Copy, Default)]
134struct Biquad {
135    c: [f32; 5],
136    z1: f32,
137    z2: f32,
138}
139
140impl Biquad {
141    fn tick(&mut self, x: f32) -> f32 {
142        let y = self.c[0] * x + self.z1;
143        self.z1 = self.c[1] * x - self.c[3] * y + self.z2;
144        self.z2 = self.c[2] * x - self.c[4] * y;
145        if !y.is_finite() {
146            self.z1 = 0.0;
147            self.z2 = 0.0;
148            return 0.0;
149        }
150        y
151    }
152}
153
154// ---------- reverb (Freeverb) ----------
155
156const COMB_LEN: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
157const ALLP_LEN: [usize; 4] = [556, 441, 341, 225];
158/// Right-channel stereo spread, in samples at 44.1 kHz (Freeverb's `stereospread`).
159const SPREAD: usize = 23;
160/// Freeverb's `fixedgain`: how much of the dry signal is pushed into the comb bank.
161const REV_IN_GAIN: f32 = 0.015;
162
163struct Comb {
164    buf: Vec<f32>,
165    pos: usize,
166    store: f32,
167}
168
169impl Comb {
170    fn new(n: usize) -> Self {
171        Self { buf: vec![0.0; n.max(1)], pos: 0, store: 0.0 }
172    }
173    fn tick(&mut self, x: f32, fb: f32, damp: f32) -> f32 {
174        let y = self.buf[self.pos];
175        self.store = y * (1.0 - damp) + self.store * damp;
176        let v = x + self.store * fb;
177        self.buf[self.pos] = if v.is_finite() { v } else { 0.0 };
178        self.pos = (self.pos + 1) % self.buf.len();
179        y
180    }
181}
182
183struct Allpass {
184    buf: Vec<f32>,
185    pos: usize,
186}
187
188impl Allpass {
189    fn new(n: usize) -> Self {
190        Self { buf: vec![0.0; n.max(1)], pos: 0 }
191    }
192    fn tick(&mut self, x: f32) -> f32 {
193        let y = self.buf[self.pos];
194        let v = x + y * 0.5;
195        self.buf[self.pos] = if v.is_finite() { v } else { 0.0 };
196        self.pos = (self.pos + 1) % self.buf.len();
197        y - x
198    }
199}
200
201struct Freeverb {
202    combs: [[Comb; 8]; 2],
203    allp: [[Allpass; 4]; 2],
204    /// Interleaved stereo pre-delay line, sized for the 200 ms parameter maximum.
205    pre: Vec<f32>,
206    pre_pos: usize,
207}
208
209impl Freeverb {
210    fn new(sr: f32) -> Self {
211        let scale = sr / 44100.0;
212        let n = |base: usize, off: usize| ((base + off) as f32 * scale) as usize;
213        Self {
214            combs: [
215                std::array::from_fn(|i| Comb::new(n(COMB_LEN[i], 0))),
216                std::array::from_fn(|i| Comb::new(n(COMB_LEN[i], SPREAD))),
217            ],
218            allp: [
219                std::array::from_fn(|i| Allpass::new(n(ALLP_LEN[i], 0))),
220                std::array::from_fn(|i| Allpass::new(n(ALLP_LEN[i], SPREAD))),
221            ],
222            pre: vec![0.0; ((sr * 0.201) as usize + 2) * 2],
223            pre_pos: 0,
224        }
225    }
226
227    /// One frame: returns the wet (l, r).
228    fn tick(&mut self, l: f32, r: f32, fb: f32, damp: f32, w1: f32, w2: f32, pre_frames: usize) -> (f32, f32) {
229        let cap = self.pre.len() / 2;
230        let wp = self.pre_pos * 2;
231        self.pre[wp] = l;
232        self.pre[wp + 1] = r;
233        let d = pre_frames.min(cap - 1);
234        let rp = (self.pre_pos + cap - d) % cap;
235        let (dl, dr) = (self.pre[rp * 2], self.pre[rp * 2 + 1]);
236        self.pre_pos = (self.pre_pos + 1) % cap;
237
238        let input = (dl + dr) * REV_IN_GAIN;
239        let mut wet = [0.0f32; 2];
240        for ch in 0..2 {
241            let mut acc = 0.0;
242            for c in self.combs[ch].iter_mut() {
243                acc += c.tick(input, fb, damp);
244            }
245            for a in self.allp[ch].iter_mut() {
246                acc = a.tick(acc);
247            }
248            wet[ch] = acc;
249        }
250        (wet[0] * w1 + wet[1] * w2, wet[1] * w1 + wet[0] * w2)
251    }
252}
253
254// ---------- per-filter state ----------
255
256/// Interleaved-stereo delay line used by Echo.
257struct Delay {
258    buf: Vec<f32>,
259    pos: usize,
260}
261
262/// The DSP memory a filter kind needs. Sized once in `FilterState::new`.
263enum Dsp {
264    /// 1 band (pass filters) or `EQ_BANDS.len()` bands (EQ) × 2 channels, band-major.
265    Biquads(Vec<Biquad>),
266    Reverb(Box<Freeverb>),
267    Echo(Delay),
268    /// Distortion tone one-pole, per channel.
269    Tone([f32; 2]),
270    /// Compressor / gate: linked peak envelope + smoothed gain.
271    Dyn {
272        env: f32,
273        gain: f32,
274    },
275    Noise {
276        rng: u32,
277        pink: [[f32; 3]; 2],
278        phase: f32,
279        prev: f32,
280    },
281    Gain {
282        prev: f32,
283    },
284}
285
286/// Per-filter DSP state (biquad histories, delay lines, envelopes).
287pub struct FilterState {
288    kind: FilterKind,
289    sr: f32,
290    dsp: Dsp,
291}
292
293/// Longest Echo delay the parameter allows, in seconds (`F_ECHO`'s "Delay ms" max).
294const MAX_ECHO_S: f32 = 2.0;
295
296impl FilterState {
297    pub fn new(f: &AudioFilter, sample_rate: u32) -> Self {
298        let sr = if sample_rate == 0 { SAMPLE_RATE as f32 } else { sample_rate as f32 };
299        let dsp = match f.kind {
300            FilterKind::Eq => Dsp::Biquads(vec![Biquad::default(); EQ_BANDS.len() * 2]),
301            FilterKind::HighPass | FilterKind::LowPass => Dsp::Biquads(vec![Biquad::default(); 2]),
302            FilterKind::Reverb => Dsp::Reverb(Box::new(Freeverb::new(sr))),
303            FilterKind::Echo => Dsp::Echo(Delay { buf: vec![0.0; ((sr * MAX_ECHO_S) as usize + 2) * 2], pos: 0 }),
304            FilterKind::Distortion => Dsp::Tone([0.0; 2]),
305            FilterKind::Compressor | FilterKind::NoiseGate => Dsp::Dyn { env: 0.0, gain: 1.0 },
306            FilterKind::Noise => Dsp::Noise { rng: 0x1234_5678, pink: [[0.0; 3]; 2], phase: 0.0, prev: f32::NAN },
307            FilterKind::Gain => Dsp::Gain { prev: f32::NAN },
308        };
309        Self { kind: f.kind, sr, dsp }
310    }
311
312    /// The kind this state was built for — the graph rebuilds when the user changes a slot's filter.
313    pub fn kind(&self) -> FilterKind {
314        self.kind
315    }
316
317    /// Process one block in place. `t` = timeline seconds at the block start (keyframed params).
318    pub fn process(&mut self, f: &AudioFilter, t: f64, buf: &mut [f32]) {
319        if f.kind != self.kind || buf.len() < 2 {
320            return;
321        }
322        let sr = self.sr;
323        let p = |i: usize| f.at(i, t) as f32;
324        match (&mut self.dsp, f.kind) {
325            (Dsp::Biquads(bq), FilterKind::Eq) => {
326                for (b, &(band, gi, fi, qi)) in bq.chunks_exact_mut(2).zip(EQ_BANDS.iter()) {
327                    let c = coeffs(band, p(fi), p(qi).max(0.1), p(gi), sr);
328                    b[0].c = c;
329                    b[1].c = c;
330                }
331                for fr in buf.chunks_exact_mut(2) {
332                    for b in bq.chunks_exact_mut(2) {
333                        fr[0] = b[0].tick(fr[0]);
334                        fr[1] = b[1].tick(fr[1]);
335                    }
336                }
337            }
338            (Dsp::Biquads(bq), FilterKind::HighPass | FilterKind::LowPass) => {
339                let band = if f.kind == FilterKind::HighPass { Band::HighPass } else { Band::LowPass };
340                let c = coeffs(band, p(0), p(1), 0.0, sr);
341                bq[0].c = c;
342                bq[1].c = c;
343                for fr in buf.chunks_exact_mut(2) {
344                    fr[0] = bq[0].tick(fr[0]);
345                    fr[1] = bq[1].tick(fr[1]);
346                }
347            }
348            (Dsp::Reverb(rev), _) => {
349                let room = p(0).clamp(0.0, 1.0);
350                let damp = p(1).clamp(0.0, 1.0) * 0.4;
351                let width = p(2).clamp(0.0, 1.0);
352                let mix = p(3).clamp(0.0, 1.0);
353                let pre = ((p(4).clamp(0.0, 200.0) * 0.001) * sr) as usize;
354                let fb = room * 0.28 + 0.7;
355                let (w1, w2) = (width * 0.5 + 0.5, (1.0 - width) * 0.5);
356                for fr in buf.chunks_exact_mut(2) {
357                    let (wl, wr) = rev.tick(fr[0], fr[1], fb, damp, w1, w2, pre);
358                    fr[0] = fr[0] * (1.0 - mix) + wl * mix;
359                    fr[1] = fr[1] * (1.0 - mix) + wr * mix;
360                }
361            }
362            (Dsp::Echo(d), _) => {
363                let cap = d.buf.len() / 2;
364                let frames = ((p(0).clamp(1.0, MAX_ECHO_S * 1000.0) * 0.001) * sr) as usize;
365                let frames = frames.clamp(1, cap - 1);
366                let fb = p(1).clamp(0.0, 0.95);
367                let mix = p(2).clamp(0.0, 1.0);
368                let ping = p(3) >= 0.5;
369                for fr in buf.chunks_exact_mut(2) {
370                    let rp = (d.pos + cap - frames) % cap;
371                    let (dl, dr) = (d.buf[rp * 2], d.buf[rp * 2 + 1]);
372                    let (yl, yr) = if ping { (dr, dl) } else { (dl, dr) };
373                    d.buf[d.pos * 2] = fr[0] + yl * fb;
374                    d.buf[d.pos * 2 + 1] = fr[1] + yr * fb;
375                    d.pos = (d.pos + 1) % cap;
376                    fr[0] = fr[0] * (1.0 - mix) + yl * mix;
377                    fr[1] = fr[1] * (1.0 - mix) + yr * mix;
378                }
379            }
380            (Dsp::Tone(lp), _) => {
381                let drive = p(0).clamp(1.0, 50.0);
382                let tone = p(1).clamp(0.0, 1.0);
383                let mix = p(2).clamp(0.0, 1.0);
384                let norm = 1.0 / drive.tanh();
385                // one-pole low-pass at ~3 kHz; `tone` blends it back towards the raw clip
386                let a = coef(0.053, sr);
387                for fr in buf.chunks_exact_mut(2) {
388                    for ch in 0..2 {
389                        let x = fr[ch];
390                        let y = (x * drive).tanh() * norm;
391                        lp[ch] += (y - lp[ch]) * a;
392                        let y = lp[ch] + (y - lp[ch]) * tone;
393                        fr[ch] = x * (1.0 - mix) + y * mix;
394                    }
395                }
396            }
397            (Dsp::Dyn { env, gain }, FilterKind::Compressor) => {
398                let thr = db_to_lin(p(0).clamp(-60.0, 0.0));
399                let ratio = p(1).max(1.0);
400                let at = coef(p(2), sr);
401                let rl = coef(p(3), sr);
402                let makeup = db_to_lin(p(4));
403                let ex = 1.0 - 1.0 / ratio;
404                for fr in buf.chunks_exact_mut(2) {
405                    let peak = fr[0].abs().max(fr[1].abs());
406                    let c = if peak > *env { at } else { rl };
407                    *env += (peak - *env) * c;
408                    *gain = if *env > thr { (thr / *env).powf(ex) } else { 1.0 };
409                    fr[0] *= *gain * makeup;
410                    fr[1] *= *gain * makeup;
411                }
412            }
413            (Dsp::Dyn { env, gain }, FilterKind::NoiseGate) => {
414                let thr = db_to_lin(p(0).clamp(-80.0, 0.0));
415                let at = coef(p(1), sr);
416                let rl = coef(p(2), sr);
417                for fr in buf.chunks_exact_mut(2) {
418                    let peak = fr[0].abs().max(fr[1].abs());
419                    let c = if peak > *env { at } else { rl };
420                    *env += (peak - *env) * c;
421                    let target = if *env > thr { 1.0 } else { 0.0 };
422                    *gain += (target - *gain) * if target > *gain { at } else { rl };
423                    fr[0] *= *gain;
424                    fr[1] *= *gain;
425                }
426            }
427            (Dsp::Noise { rng, pink, phase, prev }, _) => {
428                let level = db_to_lin(p(0).clamp(-80.0, 0.0));
429                let ty = p(1).round().clamp(0.0, 2.0) as u8;
430                let step = std::f32::consts::TAU * p(2).clamp(20.0, sr * 0.45) / sr;
431                let n = (buf.len() / 2) as f32;
432                let g0 = if prev.is_finite() { *prev } else { level };
433                let dg = (level - g0) / n;
434                *prev = level;
435                for (i, fr) in buf.chunks_exact_mut(2).enumerate() {
436                    let g = g0 + dg * i as f32;
437                    match ty {
438                        0 => {
439                            fr[0] += white(rng) * g;
440                            fr[1] += white(rng) * g;
441                        }
442                        1 => {
443                            for ch in 0..2 {
444                                fr[ch] += pink_tick(&mut pink[ch], white(rng)) * g;
445                            }
446                        }
447                        _ => {
448                            let s = phase.sin() * g;
449                            fr[0] += s;
450                            fr[1] += s;
451                            *phase = (*phase + step) % std::f32::consts::TAU;
452                        }
453                    }
454                }
455            }
456            (Dsp::Gain { prev }, _) => {
457                let g1 = db_to_lin(p(0).clamp(-60.0, 24.0));
458                let n = (buf.len() / 2) as f32;
459                let g0 = if prev.is_finite() { *prev } else { g1 };
460                let dg = (g1 - g0) / n;
461                *prev = g1;
462                for (i, fr) in buf.chunks_exact_mut(2).enumerate() {
463                    let g = g0 + dg * i as f32;
464                    fr[0] *= g;
465                    fr[1] *= g;
466                }
467            }
468            _ => {}
469        }
470    }
471}
472
473/// xorshift32 white noise in [-1, 1).
474fn white(rng: &mut u32) -> f32 {
475    *rng ^= *rng << 13;
476    *rng ^= *rng >> 17;
477    *rng ^= *rng << 5;
478    (*rng >> 8) as f32 / 8_388_608.0 - 1.0
479}
480
481/// Paul Kellet's economy pink filter (~ -3 dB/oct over the audio band).
482fn pink_tick(s: &mut [f32; 3], w: f32) -> f32 {
483    s[0] = 0.99765 * s[0] + w * 0.0990460;
484    s[1] = 0.96300 * s[1] + w * 0.2965164;
485    s[2] = 0.57000 * s[2] + w * 1.0526913;
486    (s[0] + s[1] + s[2] + w * 0.1848) * 0.2
487}
488
489// ---------- bus graph ----------
490
491struct Slot {
492    id: Id,
493    /// Resolved output bus; `id` itself means terminal (Main / a broken cycle's root).
494    dest: Id,
495    /// Hops to Main — the evaluation order is this, descending.
496    depth: u32,
497    audible: bool,
498    buf: Vec<f32>,
499    states: Vec<FilterState>,
500    meter: (f32, f32),
501}
502
503/// Bus routing + per-bus filter state, rebuilt when the bus set changes.
504#[derive(Default)]
505pub struct BusGraph {
506    slots: Vec<Slot>,
507    order: Vec<Id>,
508    main: Id,
509    have_main: bool,
510}
511
512/// Deepest routing chain we follow before declaring a cycle.
513const MAX_HOPS: u32 = 64;
514
515impl BusGraph {
516    pub fn new() -> Self {
517        Self::default()
518    }
519
520    /// Rebuild for `project` (cheap when nothing changed). Cycles fall back to routing into Main.
521    pub fn sync(&mut self, project: &Project) {
522        let buses = &project.buses;
523        self.have_main = !buses.is_empty();
524        self.main = buses.first().map(|b| b.id).unwrap_or(0);
525        // drop slots for buses that are gone, add slots for new ones (keeping filter state otherwise)
526        self.slots.retain(|s| buses.iter().any(|b| b.id == s.id));
527        for b in buses {
528            if !self.slots.iter().any(|s| s.id == b.id) {
529                self.slots.push(Slot {
530                    id: b.id,
531                    dest: b.id,
532                    depth: 0,
533                    audible: true,
534                    buf: Vec::new(),
535                    states: Vec::new(),
536                    meter: (0.0, 0.0),
537                });
538            }
539        }
540        let any_solo = buses.iter().any(|b| b.solo);
541        for b in buses {
542            let dest = resolve(buses, b, self.main);
543            let depth = hops(buses, b.id, self.main);
544            // Main is the master: solo elsewhere must not silence the path everything sums into.
545            let audible = !b.muted && (b.id == self.main || !any_solo || b.solo);
546            if let Some(s) = self.slots.iter_mut().find(|s| s.id == b.id) {
547                s.dest = dest;
548                s.depth = depth;
549                s.audible = audible;
550            }
551        }
552        let Self { slots, order, .. } = self;
553        order.clear();
554        order.extend(buses.iter().map(|b| b.id));
555        order.sort_by_key(|id| std::cmp::Reverse(slots.iter().find(|s| s.id == *id).map(|s| s.depth).unwrap_or(0)));
556    }
557
558    /// Buses in evaluation order (leaves first, Main last).
559    pub fn order(&self) -> Vec<Id> {
560        self.order.clone()
561    }
562
563    /// The same order without the allocation, for the mixer's per-block loop.
564    pub fn order_ref(&self) -> &[Id] {
565        &self.order
566    }
567
568    /// Zero every bus buffer for a new block of `frames` frames.
569    pub fn begin(&mut self, frames: usize) {
570        for s in &mut self.slots {
571            s.buf.clear();
572            s.buf.resize(frames * 2, 0.0);
573        }
574    }
575
576    /// Scratch buffer for a bus (created on demand, zeroed for the block).
577    pub fn buffer(&mut self, bus: Id, frames: usize) -> &mut [f32] {
578        let i = match self.slots.iter().position(|s| s.id == bus) {
579            Some(i) => i,
580            None => {
581                self.slots.push(Slot {
582                    id: bus,
583                    dest: bus,
584                    depth: 0,
585                    audible: true,
586                    buf: Vec::new(),
587                    states: Vec::new(),
588                    meter: (0.0, 0.0),
589                });
590                self.slots.len() - 1
591            }
592        };
593        let b = &mut self.slots[i].buf;
594        if b.len() != frames * 2 {
595            b.clear();
596            b.resize(frames * 2, 0.0);
597        }
598        b
599    }
600
601    /// Run one bus's filters + gain/pan/mono and add the result into its output bus (or `out` for Main).
602    pub fn flush(&mut self, bus: &Bus, t: f64, out: &mut [f32]) {
603        let Some(i) = self.slots.iter().position(|s| s.id == bus.id) else { return };
604        if !self.slots[i].audible {
605            self.slots[i].buf.fill(0.0);
606            self.slots[i].meter = (0.0, 0.0);
607            return;
608        }
609        let mut buf = std::mem::take(&mut self.slots[i].buf);
610        let dest = self.slots[i].dest;
611        {
612            let states = &mut self.slots[i].states;
613            // keep one state per filter slot; rebuild a slot whose kind changed
614            if states.len() > bus.filters.len() {
615                states.truncate(bus.filters.len());
616            }
617            for (j, f) in bus.filters.iter().enumerate() {
618                match states.get(j) {
619                    Some(s) if s.kind() == f.kind => {}
620                    Some(_) => states[j] = FilterState::new(f, SAMPLE_RATE),
621                    None => states.push(FilterState::new(f, SAMPLE_RATE)),
622                }
623                if f.enabled {
624                    states[j].process(f, t, &mut buf);
625                }
626            }
627        }
628
629        let g = bus.gain.at(t) as f32;
630        let pan = bus.pan.at(t).clamp(-1.0, 1.0) as f32;
631        let (gl, gr) = (g * (1.0 - pan).min(1.0), g * (1.0 + pan).min(1.0));
632        let mut peak = (0.0f32, 0.0f32);
633        for fr in buf.chunks_exact_mut(2) {
634            let (mut l, mut r) = (fr[0] * gl, fr[1] * gr);
635            if bus.mono {
636                let m = (l + r) * 0.5;
637                l = m;
638                r = m;
639            }
640            fr[0] = l;
641            fr[1] = r;
642            peak.0 = peak.0.max(l.abs());
643            peak.1 = peak.1.max(r.abs());
644        }
645
646        let frames = buf.len() / 2;
647        let decay = (-(frames as f32) / (0.25 * SAMPLE_RATE as f32)).exp();
648        let m = self.slots[i].meter;
649        self.slots[i].meter = ((m.0 * decay).max(peak.0), (m.1 * decay).max(peak.1));
650
651        let terminal = dest == bus.id || !self.have_main;
652        if terminal {
653            add_into(out, &buf);
654        } else if let Some(j) = self.slots.iter().position(|s| s.id == dest) {
655            let mut dbuf = std::mem::take(&mut self.slots[j].buf);
656            if dbuf.len() < buf.len() {
657                dbuf.resize(buf.len(), 0.0);
658            }
659            add_into(&mut dbuf, &buf);
660            self.slots[j].buf = dbuf;
661        }
662        self.slots[i].buf = buf;
663    }
664
665    /// Peak level (L, R) of the last block, for the mixer meters.
666    pub fn meter(&self, bus: Id) -> (f32, f32) {
667        self.slots.iter().find(|s| s.id == bus).map(|s| s.meter).unwrap_or((0.0, 0.0))
668    }
669}
670
671fn add_into(dst: &mut [f32], src: &[f32]) {
672    for (d, s) in dst.iter_mut().zip(src) {
673        *d += *s;
674    }
675}
676
677/// Where `b` really sends: its own id when terminal (Main, dangling or cyclic output).
678fn resolve(buses: &[Bus], b: &Bus, main: Id) -> Id {
679    if b.id == main || b.output == 0 || b.output == b.id || !buses.iter().any(|o| o.id == b.output) {
680        return if b.id == main { b.id } else { main };
681    }
682    // walk the chain; anything that does not reach Main within MAX_HOPS is a cycle → Main
683    let mut cur = b.output;
684    for _ in 0..MAX_HOPS {
685        if cur == main {
686            return b.output;
687        }
688        match buses.iter().find(|o| o.id == cur) {
689            Some(o) if o.output != 0 && o.output != o.id && buses.iter().any(|x| x.id == o.output) => cur = o.output,
690            _ => return b.output, // dangling further down: that bus becomes terminal-into-Main itself
691        }
692    }
693    main
694}
695
696/// Hops from `id` to Main along resolved outputs (Main = 0, a broken cycle = 1).
697fn hops(buses: &[Bus], id: Id, main: Id) -> u32 {
698    let mut cur = id;
699    for n in 0..MAX_HOPS {
700        if cur == main {
701            return n;
702        }
703        let Some(b) = buses.iter().find(|o| o.id == cur) else { return n + 1 };
704        let next = resolve(buses, b, main);
705        if next == cur {
706            return n + 1;
707        }
708        cur = next;
709    }
710    MAX_HOPS
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use crate::model::AudioFilter;
717
718    const SR: u32 = SAMPLE_RATE;
719
720    fn filt(kind: FilterKind, params: &[(usize, f64)]) -> AudioFilter {
721        let mut f = AudioFilter::new(kind);
722        for &(i, v) in params {
723            f.params[i].value = v;
724        }
725        f
726    }
727
728    /// n frames of a sine at `hz`, amplitude `amp`, interleaved stereo.
729    fn sine(hz: f32, amp: f32, n: usize) -> Vec<f32> {
730        let mut v = vec![0.0; n * 2];
731        for (i, fr) in v.chunks_exact_mut(2).enumerate() {
732            let s = amp * (std::f32::consts::TAU * hz * i as f32 / SR as f32).sin();
733            fr[0] = s;
734            fr[1] = s;
735        }
736        v
737    }
738
739    fn rms(buf: &[f32]) -> f32 {
740        if buf.is_empty() {
741            return 0.0;
742        }
743        (buf.iter().map(|s| s * s).sum::<f32>() / buf.len() as f32).sqrt()
744    }
745
746    fn peak(buf: &[f32]) -> f32 {
747        buf.iter().fold(0.0f32, |a, s| a.max(s.abs()))
748    }
749
750    /// Steady-state RMS of `hz` through `f` (first half discarded so the IIR has settled).
751    fn thru_rms(f: &AudioFilter, hz: f32) -> f32 {
752        let mut st = FilterState::new(f, SR);
753        let mut buf = sine(hz, 0.5, 9600);
754        st.process(f, 0.0, &mut buf);
755        rms(&buf[buf.len() / 2..])
756    }
757
758    #[test]
759    fn low_pass_attenuates_highs() {
760        let f = filt(FilterKind::LowPass, &[(0, 500.0), (1, 0.707)]);
761        let low = thru_rms(&f, 100.0);
762        let high = thru_rms(&f, 10_000.0);
763        assert!(low > 0.3, "100 Hz should pass: {low}");
764        assert!(high < low * 0.05, "10 kHz {high} vs 100 Hz {low}");
765    }
766
767    #[test]
768    fn high_pass_attenuates_lows() {
769        let f = filt(FilterKind::HighPass, &[(0, 2000.0), (1, 0.707)]);
770        let low = thru_rms(&f, 100.0);
771        let high = thru_rms(&f, 10_000.0);
772        assert!(high > 0.3, "10 kHz should pass: {high}");
773        assert!(low < high * 0.05, "100 Hz {low} vs 10 kHz {high}");
774    }
775
776    #[test]
777    fn eq_boost_raises_that_band() {
778        // +12 dB peak at 1 kHz, everything else flat
779        let f = filt(FilterKind::Eq, &[(2, 12.0), (3, 1000.0), (4, 1.0)]);
780        let flat = AudioFilter::new(FilterKind::Eq);
781        let boosted = thru_rms(&f, 1000.0);
782        let plain = thru_rms(&flat, 1000.0);
783        let ratio = boosted / plain;
784        assert!((ratio - 4.0).abs() < 0.4, "+12 dB ≈ ×4, got ×{ratio}");
785        // a far-away band is untouched
786        let far = thru_rms(&f, 60.0) / thru_rms(&flat, 60.0);
787        assert!((far - 1.0).abs() < 0.15, "60 Hz should be flat, got ×{far}");
788        // and the analytic response agrees with the measured one
789        let db = filter_response_db(&f, 0.0, 1000.0);
790        assert!((db - 12.0).abs() < 0.6, "response {db} dB");
791        assert!(filter_response_db(&flat, 0.0, 1000.0).abs() < 0.01);
792    }
793
794    #[test]
795    fn eq_shelves() {
796        let low = filt(FilterKind::Eq, &[(0, -12.0), (1, 200.0)]);
797        assert!(filter_response_db(&low, 0.0, 40.0) < -10.0);
798        assert!(filter_response_db(&low, 0.0, 8000.0).abs() < 0.5);
799        let high = filt(FilterKind::Eq, &[(5, 6.0), (6, 4000.0)]);
800        assert!(filter_response_db(&high, 0.0, 16000.0) > 5.0);
801        assert!(filter_response_db(&high, 0.0, 100.0).abs() < 0.5);
802    }
803
804    #[test]
805    fn eq_five_bands() {
806        let flat = AudioFilter::new(FilterKind::Eq);
807        for hz in [20.0, 100.0, 1000.0, 10_000.0, 20_000.0] {
808            assert!(filter_response_db(&flat, 0.0, hz).abs() < 0.01, "default EQ must be flat at {hz} Hz");
809        }
810        // every band boosts around its own default corner (a shelf reaches full gain past the knee)
811        for &(band, gi, fi, _) in &EQ_BANDS {
812            let f0 = FilterKind::Eq.params()[fi].default as f32;
813            let probe = match band {
814                Band::LowShelf => f0 * 0.2,
815                Band::HighShelf => f0 * 3.0,
816                _ => f0,
817            };
818            let db = filter_response_db(&filt(FilterKind::Eq, &[(gi, 12.0)]), 0.0, probe);
819            assert!(db > 9.0, "{band:?} at {probe} Hz: {db} dB");
820        }
821        // one of the added peaks measures what the analytic curve promises
822        let ratio = thru_rms(&filt(FilterKind::Eq, &[(9, 12.0)]), 400.0) / thru_rms(&flat, 400.0);
823        assert!((ratio - 4.0).abs() < 0.5, "+12 dB at 400 Hz ≈ ×4, got ×{ratio}");
824        // shelves take a Q now (0.707 = the slope-1 shelf they were pinned to): it reshapes the knee
825        // — a resonant shelf dips on the far side of the corner — without touching the plateau
826        let tight = filt(FilterKind::Eq, &[(0, 12.0), (7, 2.0)]);
827        let wide = filt(FilterKind::Eq, &[(0, 12.0), (7, 0.4)]);
828        let (t, w) = (filter_response_db(&tight, 0.0, 240.0), filter_response_db(&wide, 0.0, 240.0));
829        assert!((t - w).abs() > 2.0, "shelf Q must reshape the knee: {t} vs {w}");
830        for f in [&tight, &wide] {
831            let plateau = filter_response_db(f, 0.0, 20.0);
832            assert!((plateau - 12.0).abs() < 1.0, "the shelf still reaches +12 dB: {plateau}");
833        }
834        // a project saved by the 3-band EQ is seven params long and still means the same thing
835        let mut old = filt(FilterKind::Eq, &[(0, -12.0), (1, 200.0)]);
836        old.params.truncate(7);
837        assert!(filter_response_db(&old, 0.0, 40.0) < -10.0, "old low shelf");
838        assert!(filter_response_db(&old, 0.0, 8000.0).abs() < 0.5, "old EQ flat up top");
839    }
840
841    #[test]
842    fn gain_db_is_exact() {
843        let f = filt(FilterKind::Gain, &[(0, -6.0)]);
844        let mut st = FilterState::new(&f, SR);
845        let mut buf = vec![1.0f32; 4800 * 2];
846        st.process(&f, 0.0, &mut buf);
847        // first block ramps from the target (no previous value) → constant
848        let want = db_to_lin(-6.0);
849        assert!(buf.iter().all(|s| (s - want).abs() < 1e-6), "{} vs {want}", buf[0]);
850        // a second block at the same value stays put
851        let mut buf2 = vec![1.0f32; 512 * 2];
852        st.process(&f, 0.0, &mut buf2);
853        assert!((buf2[0] - want).abs() < 1e-6);
854        // and a changed value ramps instead of stepping
855        let f2 = filt(FilterKind::Gain, &[(0, 0.0)]);
856        let mut buf3 = vec![1.0f32; 512 * 2];
857        st.process(&f2, 0.0, &mut buf3);
858        assert!((buf3[0] - want).abs() < 1e-3, "ramp starts at the old gain: {}", buf3[0]);
859        assert!(buf3[buf3.len() - 1] > 0.99, "ramp ends at the new gain: {}", buf3[buf3.len() - 1]);
860    }
861
862    #[test]
863    fn gate_silences_below_threshold() {
864        let f = filt(FilterKind::NoiseGate, &[(0, -45.0), (1, 2.0), (2, 50.0)]);
865        // -60 dBFS tone: below the threshold → gated to silence
866        let mut st = FilterState::new(&f, SR);
867        let mut quiet = sine(440.0, db_to_lin(-60.0), 48000);
868        st.process(&f, 0.0, &mut quiet);
869        assert!(peak(&quiet[quiet.len() / 2..]) < 1e-6, "{}", peak(&quiet[quiet.len() / 2..]));
870        // -6 dBFS tone: above → passes untouched
871        let mut st = FilterState::new(&f, SR);
872        let mut loud = sine(440.0, db_to_lin(-6.0), 48000);
873        st.process(&f, 0.0, &mut loud);
874        let p = peak(&loud[loud.len() / 2..]);
875        assert!((p - db_to_lin(-6.0)).abs() < 1e-3, "{p}");
876    }
877
878    #[test]
879    fn compressor_reduces_peaks_by_the_ratio() {
880        // 0 dBFS in, threshold -18 dB, ratio 4 → -18 + 18/4 = -13.5 dBFS out.
881        // Fast attack / slow release so the peak detector actually settles on the sine's peak.
882        let f = filt(FilterKind::Compressor, &[(0, -18.0), (1, 4.0), (2, 1.0), (3, 1000.0), (4, 0.0)]);
883        let mut st = FilterState::new(&f, SR);
884        let mut buf = sine(440.0, 1.0, 48000);
885        st.process(&f, 0.0, &mut buf);
886        let out_db = lin_to_db(peak(&buf[buf.len() / 2..]));
887        assert!((out_db + 13.5).abs() < 1.0, "{out_db} dB");
888        // below the threshold nothing happens
889        let mut st = FilterState::new(&f, SR);
890        let mut quiet = sine(440.0, db_to_lin(-30.0), 48000);
891        st.process(&f, 0.0, &mut quiet);
892        let q = lin_to_db(peak(&quiet[quiet.len() / 2..]));
893        assert!((q + 30.0).abs() < 0.5, "{q} dB");
894        // makeup is exact on top
895        let g = filt(FilterKind::Compressor, &[(0, -18.0), (1, 4.0), (2, 1.0), (3, 1000.0), (4, 6.0)]);
896        let mut st = FilterState::new(&g, SR);
897        let mut buf = sine(440.0, 1.0, 48000);
898        st.process(&g, 0.0, &mut buf);
899        let with_makeup = lin_to_db(peak(&buf[buf.len() / 2..]));
900        assert!((with_makeup - out_db - 6.0).abs() < 0.6, "{with_makeup} vs {out_db}");
901    }
902
903    #[test]
904    fn echo_delays_by_the_right_offset() {
905        // 100 ms delay, no feedback, fully wet → an impulse reappears exactly 4800 frames later
906        let f = filt(FilterKind::Echo, &[(0, 100.0), (1, 0.0), (2, 1.0), (3, 0.0)]);
907        let mut st = FilterState::new(&f, SR);
908        let mut buf = vec![0.0f32; 48000 * 2];
909        buf[0] = 1.0;
910        buf[1] = 1.0;
911        st.process(&f, 0.0, &mut buf);
912        assert!(buf[0].abs() < 1e-6, "dry is gone at mix=1: {}", buf[0]);
913        assert!((buf[4800 * 2] - 1.0).abs() < 1e-6, "echo at 100 ms: {}", buf[4800 * 2]);
914        assert!(buf[4799 * 2].abs() < 1e-6);
915        assert!(buf[4801 * 2].abs() < 1e-6);
916        // feedback repeats it, quieter
917        let f = filt(FilterKind::Echo, &[(0, 100.0), (1, 0.5), (2, 1.0), (3, 0.0)]);
918        let mut st = FilterState::new(&f, SR);
919        let mut buf = vec![0.0f32; 48000 * 2];
920        buf[0] = 1.0;
921        st.process(&f, 0.0, &mut buf);
922        assert!((buf[9600 * 2] - 0.5).abs() < 1e-6, "second tap: {}", buf[9600 * 2]);
923        // ping-pong puts the first echo on the other channel
924        let f = filt(FilterKind::Echo, &[(0, 100.0), (1, 0.0), (2, 1.0), (3, 1.0)]);
925        let mut st = FilterState::new(&f, SR);
926        let mut buf = vec![0.0f32; 48000 * 2];
927        buf[0] = 1.0; // left only
928        st.process(&f, 0.0, &mut buf);
929        assert!(buf[4800 * 2].abs() < 1e-6, "L stays empty: {}", buf[4800 * 2]);
930        assert!((buf[4800 * 2 + 1] - 1.0).abs() < 1e-6, "R gets it: {}", buf[4800 * 2 + 1]);
931    }
932
933    #[test]
934    fn reverb_tail_decays() {
935        let f = filt(FilterKind::Reverb, &[(0, 0.8), (1, 0.2), (2, 1.0), (3, 1.0), (4, 0.0)]);
936        let mut st = FilterState::new(&f, SR);
937        // 50 ms of noise, then 2 s of silence
938        let mut buf = sine(500.0, 0.5, 2400);
939        buf.resize(2400 * 2 + 48000 * 2 * 2, 0.0);
940        st.process(&f, 0.0, &mut buf);
941        let tail = |from: usize, to: usize| rms(&buf[from * 2..to * 2]);
942        let early = tail(4800, 9600);
943        let late = tail(48000, 52800);
944        assert!(early > 1e-4, "tail after the input stops: {early}");
945        assert!(late < early, "tail decays: {early} → {late}");
946        assert!(buf.iter().all(|s| s.is_finite()));
947        // pre-delay pushes the onset later
948        let f = filt(FilterKind::Reverb, &[(0, 0.8), (1, 0.2), (2, 1.0), (3, 1.0), (4, 100.0)]);
949        let mut st = FilterState::new(&f, SR);
950        let mut buf = vec![0.0f32; 48000 * 2];
951        buf[0] = 1.0;
952        buf[1] = 1.0;
953        st.process(&f, 0.0, &mut buf);
954        assert!(rms(&buf[..2400 * 2]) < 1e-7, "silent before the pre-delay");
955        assert!(rms(&buf[4800 * 2..9600 * 2]) > 1e-6, "audible after it");
956    }
957
958    #[test]
959    fn distortion_clips_and_mixes() {
960        let f = filt(FilterKind::Distortion, &[(0, 20.0), (1, 1.0), (2, 1.0)]);
961        let mut st = FilterState::new(&f, SR);
962        let mut buf = sine(220.0, 0.2, 4800);
963        st.process(&f, 0.0, &mut buf);
964        // a 0.2 sine driven ×20 saturates: the peak climbs towards full scale
965        assert!(peak(&buf[buf.len() / 2..]) > 0.9, "{}", peak(&buf));
966        // mix 0 is a bypass
967        let f = filt(FilterKind::Distortion, &[(0, 20.0), (1, 1.0), (2, 0.0)]);
968        let mut st = FilterState::new(&f, SR);
969        let mut buf = sine(220.0, 0.2, 480);
970        let orig = buf.clone();
971        st.process(&f, 0.0, &mut buf);
972        assert!(buf.iter().zip(&orig).all(|(a, b)| (a - b).abs() < 1e-6));
973    }
974
975    #[test]
976    fn noise_kinds() {
977        for (ty, name) in [(0.0, "white"), (1.0, "pink"), (2.0, "tone")] {
978            let f = filt(FilterKind::Noise, &[(0, -6.0), (1, ty), (2, 1000.0)]);
979            let mut st = FilterState::new(&f, SR);
980            let mut buf = vec![0.0f32; 4800 * 2];
981            st.process(&f, 0.0, &mut buf);
982            let r = rms(&buf);
983            assert!(r > 1e-3, "{name} produced nothing");
984            assert!(buf.iter().all(|s| s.is_finite() && s.abs() < 4.0), "{name} blew up");
985        }
986        // the tone sits at the requested frequency: one period of 1 kHz is 48 samples
987        let f = filt(FilterKind::Noise, &[(0, 0.0), (1, 2.0), (2, 1000.0)]);
988        let mut st = FilterState::new(&f, SR);
989        let mut buf = vec![0.0f32; 4800 * 2];
990        st.process(&f, 0.0, &mut buf);
991        let left: Vec<f32> = buf.chunks_exact(2).map(|f| f[0]).collect();
992        let zero_crossings = left.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
993        assert!((zero_crossings as i32 - 200).abs() <= 4, "{zero_crossings} crossings in 100 ms");
994    }
995
996    #[test]
997    fn disabled_and_mismatched_kinds_are_noops() {
998        let f = filt(FilterKind::Gain, &[(0, -60.0)]);
999        let mut st = FilterState::new(&f, SR);
1000        let other = AudioFilter::new(FilterKind::LowPass);
1001        let mut buf = vec![1.0f32; 64];
1002        st.process(&other, 0.0, &mut buf);
1003        assert!(buf.iter().all(|s| *s == 1.0), "a kind mismatch must not touch the block");
1004    }
1005
1006    // ---------- bus graph ----------
1007
1008    fn bus_project() -> Project {
1009        let mut p = Project::new();
1010        p.main_bus();
1011        p
1012    }
1013
1014    #[test]
1015    fn order_is_leaves_first_main_last() {
1016        let mut p = bus_project();
1017        let main = p.buses[0].id;
1018        let a = p.add_bus("A");
1019        let b = p.add_bus("B");
1020        p.bus_mut(b).unwrap().output = a; // B → A → Main
1021        let mut g = BusGraph::new();
1022        g.sync(&p);
1023        let order = g.order();
1024        assert_eq!(order.len(), 3);
1025        assert_eq!(*order.last().unwrap(), main, "Main is last");
1026        let pos = |id| order.iter().position(|&x| x == id).unwrap();
1027        assert!(pos(b) < pos(a), "B before A");
1028    }
1029
1030    #[test]
1031    fn cycles_fall_back_to_main() {
1032        let mut p = bus_project();
1033        let main = p.buses[0].id;
1034        let a = p.add_bus("A");
1035        let b = p.add_bus("B");
1036        p.bus_mut(a).unwrap().output = b;
1037        p.bus_mut(b).unwrap().output = a; // A ↔ B
1038        let mut g = BusGraph::new();
1039        g.sync(&p);
1040        assert_eq!(g.order().len(), 3);
1041        assert_eq!(*g.order().last().unwrap(), main);
1042        // a signal put on A still reaches the output instead of looping forever
1043        g.begin(4);
1044        g.buffer(a, 4).fill(0.5);
1045        let mut out = vec![0.0f32; 8];
1046        for id in g.order() {
1047            let bus = p.bus(id).unwrap().clone();
1048            g.flush(&bus, 0.0, &mut out);
1049        }
1050        assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-6), "{out:?}");
1051    }
1052
1053    #[test]
1054    fn flush_gain_pan_mono_and_meter() {
1055        let mut p = bus_project();
1056        let main = p.buses[0].id;
1057        let a = p.add_bus("A");
1058        p.bus_mut(a).unwrap().gain.value = 0.5;
1059        let mut g = BusGraph::new();
1060        g.sync(&p);
1061        let mut out = vec![0.0f32; 8];
1062        g.begin(4);
1063        g.buffer(a, 4).fill(1.0);
1064        for id in g.order() {
1065            let bus = p.bus(id).unwrap().clone();
1066            g.flush(&bus, 0.0, &mut out);
1067        }
1068        assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-6), "gain: {out:?}");
1069        assert!((g.meter(a).0 - 0.5).abs() < 1e-6);
1070        assert!((g.meter(main).0 - 0.5).abs() < 1e-6);
1071
1072        // mono folds L and R
1073        p.bus_mut(a).unwrap().gain.value = 1.0;
1074        p.bus_mut(a).unwrap().mono = true;
1075        g.sync(&p);
1076        g.begin(2);
1077        let buf = g.buffer(a, 2);
1078        buf[0] = 1.0;
1079        buf[1] = 0.0;
1080        buf[2] = 1.0;
1081        buf[3] = 0.0;
1082        let mut out = vec![0.0f32; 4];
1083        for id in g.order() {
1084            let bus = p.bus(id).unwrap().clone();
1085            g.flush(&bus, 0.0, &mut out);
1086        }
1087        assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-6), "mono: {out:?}");
1088
1089        // pan hard right
1090        p.bus_mut(a).unwrap().mono = false;
1091        p.bus_mut(a).unwrap().pan.value = 1.0;
1092        g.sync(&p);
1093        g.begin(2);
1094        g.buffer(a, 2).fill(1.0);
1095        let mut out = vec![0.0f32; 4];
1096        for id in g.order() {
1097            let bus = p.bus(id).unwrap().clone();
1098            g.flush(&bus, 0.0, &mut out);
1099        }
1100        assert!(out[0].abs() < 1e-6 && (out[1] - 1.0).abs() < 1e-6, "pan: {out:?}");
1101    }
1102
1103    #[test]
1104    fn mute_and_solo() {
1105        let mut p = bus_project();
1106        let a = p.add_bus("A");
1107        let b = p.add_bus("B");
1108        let run = |g: &mut BusGraph, p: &Project| {
1109            g.begin(2);
1110            g.buffer(a, 2).fill(1.0);
1111            g.buffer(b, 2).fill(1.0);
1112            let mut out = vec![0.0f32; 4];
1113            for id in g.order() {
1114                if let Some(bus) = p.bus(id) {
1115                    let bus = bus.clone();
1116                    g.flush(&bus, 0.0, &mut out);
1117                }
1118            }
1119            out[0]
1120        };
1121        let mut g = BusGraph::new();
1122        g.sync(&p);
1123        assert!((run(&mut g, &p) - 2.0).abs() < 1e-6);
1124        // mute A → only B
1125        p.bus_mut(a).unwrap().muted = true;
1126        g.sync(&p);
1127        assert!((run(&mut g, &p) - 1.0).abs() < 1e-6);
1128        assert_eq!(g.meter(a), (0.0, 0.0));
1129        p.bus_mut(a).unwrap().muted = false;
1130        // solo B → only B, Main still passes
1131        p.bus_mut(b).unwrap().solo = true;
1132        g.sync(&p);
1133        assert!((run(&mut g, &p) - 1.0).abs() < 1e-6, "solo B");
1134    }
1135
1136    #[test]
1137    fn filter_state_survives_removal_and_kind_change() {
1138        let mut p = bus_project();
1139        let a = p.add_bus("A");
1140        p.bus_mut(a).unwrap().filters.push(filt(FilterKind::Gain, &[(0, -6.0)]));
1141        let mut g = BusGraph::new();
1142        g.sync(&p);
1143        let mut out = vec![0.0f32; 4];
1144        g.begin(2);
1145        g.buffer(a, 2).fill(1.0);
1146        for id in g.order() {
1147            let bus = p.bus(id).unwrap().clone();
1148            g.flush(&bus, 0.0, &mut out);
1149        }
1150        assert!((out[0] - db_to_lin(-6.0)).abs() < 1e-5, "{}", out[0]);
1151        // swap the filter for another kind in the same slot: the state must be rebuilt, not reused
1152        p.bus_mut(a).unwrap().filters[0] = filt(FilterKind::LowPass, &[(0, 20000.0), (1, 0.707)]);
1153        g.sync(&p);
1154        let mut out = vec![0.0f32; 4];
1155        g.begin(2);
1156        g.buffer(a, 2).fill(1.0);
1157        for id in g.order() {
1158            let bus = p.bus(id).unwrap().clone();
1159            g.flush(&bus, 0.0, &mut out);
1160        }
1161        assert!(out[0] > 0.5, "low-pass at 20 kHz passes DC: {}", out[0]);
1162        // a disabled filter is skipped
1163        p.bus_mut(a).unwrap().filters[0] = filt(FilterKind::Gain, &[(0, -60.0)]);
1164        p.bus_mut(a).unwrap().filters[0].enabled = false;
1165        g.sync(&p);
1166        let mut out = vec![0.0f32; 4];
1167        g.begin(2);
1168        g.buffer(a, 2).fill(1.0);
1169        for id in g.order() {
1170            let bus = p.bus(id).unwrap().clone();
1171            g.flush(&bus, 0.0, &mut out);
1172        }
1173        assert!((out[0] - 1.0).abs() < 1e-6, "{}", out[0]);
1174    }
1175
1176    #[test]
1177    fn removing_a_bus_drops_its_slot() {
1178        let mut p = bus_project();
1179        let a = p.add_bus("A");
1180        let mut g = BusGraph::new();
1181        g.sync(&p);
1182        assert_eq!(g.order().len(), 2);
1183        p.remove_bus(a);
1184        g.sync(&p);
1185        assert_eq!(g.order().len(), 1);
1186        assert_eq!(g.meter(a), (0.0, 0.0));
1187    }
1188}