Skip to main content

audiobox/mixer/
effects.rs

1//! Prepared bus effect chains. All times and parameter ramps are sample based.
2//!
3//! Install with [`MixerHandle::set_effects`], then update a matching slot with
4//! [`MixerHandle::set_effect`]. Slots are zero based and follow the slice order.
5//! Structural edits are immediate and reset the new chain's state; fade the bus
6//! around them when needed. Parameter ramps preserve filter/delay/reverb history.
7//! A delay-time ramp moves the read head and intentionally changes pitch.
8//! Chorus, flanger and phaser share retained-phase [`ModulationRate`] automation.
9//! [`Bitcrusher`] deliberately quantizes and holds samples; [`Saturation`] applies
10//! a cubic soft curve. These colour effects do not oversample or suppress aliasing.
11//! Parameter smoothing does not remove their intentional waveform discontinuities.
12//!
13//! Named buses run in combined audio/sidechain dependency order, effects before the bus fader.
14//! Detectors hear the key bus after its effects, gain and pause fade, in the same
15//! frame, including routing mute/solo gating when enabled. By default all named
16//! buses feed master; [`super::BusRouting`] replaces those audio paths explicitly.
17//! Master effects, gain and the limiter still run last.
18//! Self sidechains, master as a key, and cycles are refused. Application-time
19//! failures arrive as [`MixerEvent::EffectsRejected`]; the previous chain survives.
20//!
21//! A paused bus freezes its effect buffers and parameter clock. Tails continue
22//! after its voices end, until the bus is paused or the chain is removed. Sidechain
23//! attack/release are exponential time constants (63.2% after one time constant).
24//! Stereo detectors use the louder channel and apply one gain to both channels.
25//!
26//! Buffers are prepared on the handle. At most `MixerConfig::queue` chain edits may
27//! await reclamation: poll events to release old chains and permit more edits.
28//! Even if events are not polled or the handle is dropped, rendering retains these
29//! buffers rather than freeing them. Dropping the mixer itself frees its resources.
30
31use std::collections::VecDeque;
32use std::sync::Arc;
33use std::sync::atomic::{AtomicUsize, Ordering};
34
35use super::queue::{self, Consumer, Producer, PushError};
36use super::voice::Ramp;
37use super::{Action, Bus, BusId, Commands, Fade, Mixer, MixerEvent, MixerHandle};
38use crate::math::{cos_turns, db_to_gain, exp, log2, sin_turns};
39use crate::{Error, ErrorCode, FadeCurve, Result};
40
41mod colour;
42pub use colour::{Bitcrusher, Chorus, Flanger, ModulationRate, Phaser, Saturation};
43
44/// Maximum effects per bus, including the master.
45pub const MAX_EFFECTS: usize = 16;
46
47/// Delay time in seconds or beats with an explicit tempo. To follow a tempo change,
48/// send updated settings with the new BPM; the time follows the requested ramp.
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub enum EffectTime {
51    Seconds(f64),
52    Beats { beats: f64, bpm: f64 },
53}
54impl EffectTime {
55    fn seconds(self) -> Result<f64> {
56        let s = match self {
57            Self::Seconds(s) => s,
58            Self::Beats { beats, bpm } => {
59                if !(1.0..=1000.0).contains(&bpm) || !beats.is_finite() || beats <= 0.0 {
60                    return Err(Error::invalid(
61                        "Effect beats must be positive, BPM 1..=1000",
62                    ));
63                }
64                beats * 60.0 / bpm
65            }
66        };
67        if !s.is_finite() || s <= 0.0 {
68            return Err(Error::invalid("Effect time must be finite and positive"));
69        }
70        Ok(s)
71    }
72}
73
74/// Hard-knee peak compressor. `sidechain: None` detects this effect's input.
75/// `depth` blends gain reduction with unity; 0 disables reduction, 1 is full depth.
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub struct Compressor {
78    /// -96..=0 dBFS.
79    pub threshold_db: f32,
80    /// 1..=100.
81    pub ratio: f32,
82    /// 0..=5000 ms; zero is instantaneous.
83    pub attack_ms: f32,
84    /// 0..=5000 ms; zero is instantaneous.
85    pub release_ms: f32,
86    /// -24..=24 dB.
87    pub makeup_db: f32,
88    /// 0..=1.
89    pub depth: f32,
90    pub sidechain: Option<BusId>,
91}
92impl Default for Compressor {
93    fn default() -> Self {
94        Self {
95            threshold_db: -18.0,
96            ratio: 4.0,
97            attack_ms: 10.0,
98            release_ms: 100.0,
99            makeup_db: 0.0,
100            depth: 1.0,
101            sidechain: None,
102        }
103    }
104}
105impl Compressor {
106    /// A strong sidechain compressor, suitable for music under dialogue.
107    pub fn ducking(key: BusId) -> Self {
108        Self {
109            threshold_db: -30.0,
110            ratio: 20.0,
111            attack_ms: 5.0,
112            release_ms: 250.0,
113            sidechain: Some(key),
114            ..Self::default()
115        }
116    }
117}
118
119/// Fractional-sample delay with linear interpolation and optional negative feedback.
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct Delay {
122    pub time: EffectTime,
123    /// Capacity allocated at installation, 1 sample..=10 seconds. Updates may use
124    /// this capacity but cannot enlarge it without installing a new chain.
125    pub max_seconds: f64,
126    /// -0.99..=0.99.
127    pub feedback: f32,
128    /// Linear dry/wet blend, 0..=1.
129    pub mix: f32,
130}
131impl Default for Delay {
132    fn default() -> Self {
133        Self {
134            time: EffectTime::Seconds(0.25),
135            max_seconds: 2.0,
136            feedback: 0.35,
137            mix: 0.25,
138        }
139    }
140}
141
142/// Four parallel feedback combs and two allpass diffusers per channel.
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct Reverb {
145    /// Nominal -60 dB decay time, 0.05..=20 seconds, measured with damping at zero.
146    pub decay_seconds: f32,
147    /// Feedback low-pass memory, 0..=0.99; higher values shorten high-frequency tails.
148    pub damping: f32,
149    /// Linear dry/wet blend, 0..=1.
150    pub mix: f32,
151}
152impl Default for Reverb {
153    fn default() -> Self {
154        Self {
155            decay_seconds: 1.5,
156            damping: 0.25,
157            mix: 0.2,
158        }
159    }
160}
161
162/// A chain slot's typed settings. New variants can share the same preparation,
163/// automation, sidechain and lifetime machinery.
164#[derive(Clone, Copy, Debug, PartialEq)]
165#[non_exhaustive]
166pub enum Effect {
167    /// First-order topology-preserving low-pass, -3 dB at cutoff, 6 dB/octave.
168    LowPass {
169        cutoff_hz: f32,
170    },
171    /// First-order topology-preserving high-pass, -3 dB at cutoff, 6 dB/octave.
172    HighPass {
173        cutoff_hz: f32,
174    },
175    Compressor(Compressor),
176    Delay(Delay),
177    Reverb(Reverb),
178    Chorus(Chorus),
179    Flanger(Flanger),
180    Phaser(Phaser),
181    Bitcrusher(Bitcrusher),
182    Saturation(Saturation),
183}
184
185#[derive(Clone, Copy)]
186pub(super) struct Settings {
187    kind: u8,
188    values: [f64; 6],
189    key: Option<usize>,
190    capacity: f64,
191}
192impl Effect {
193    fn settings(self, rate: u32, bus: BusId, buses: usize) -> Result<Settings> {
194        let mut s = Settings {
195            kind: 0,
196            values: [0.0; 6],
197            key: None,
198            capacity: 0.0,
199        };
200        match self {
201            Self::LowPass { cutoff_hz } | Self::HighPass { cutoff_hz } => {
202                within(cutoff_hz as f64, 1.0, rate as f64 * 0.45)?;
203                s.kind = u8::from(matches!(self, Self::HighPass { .. }));
204                s.values[0] = cutoff_hz as f64;
205            }
206            Self::Compressor(c) => {
207                s.kind = 2;
208                for (v, lo, hi) in [
209                    (c.threshold_db, -96.0, 0.0),
210                    (c.ratio, 1.0, 100.0),
211                    (c.attack_ms, 0.0, 5000.0),
212                    (c.release_ms, 0.0, 5000.0),
213                    (c.makeup_db, -24.0, 24.0),
214                    (c.depth, 0.0, 1.0),
215                ] {
216                    within(v as f64, lo, hi)?;
217                }
218                s.values = [
219                    c.threshold_db as f64,
220                    c.ratio as f64,
221                    c.attack_ms as f64,
222                    c.release_ms as f64,
223                    c.makeup_db as f64,
224                    c.depth as f64,
225                ];
226                if let Some(key) = c.sidechain {
227                    if key.0 == 0 || key == bus || key.0 >= buses {
228                        return Err(Error::invalid("Sidechain needs another named bus"));
229                    }
230                    s.key = Some(key.0);
231                }
232            }
233            Self::Delay(d) => {
234                s.kind = 3;
235                within(d.max_seconds, 1.0 / rate as f64, 10.0)?;
236                let time = d.time.seconds()?;
237                within(time, 1.0 / rate as f64, d.max_seconds)?;
238                within(d.feedback as f64, -(0.99f32 as f64), 0.99f32 as f64)?;
239                within(d.mix as f64, 0.0, 1.0)?;
240                s.values = [
241                    time * rate as f64,
242                    d.feedback as f64,
243                    d.mix as f64,
244                    0.0,
245                    0.0,
246                    0.0,
247                ];
248                s.capacity = d.max_seconds * rate as f64;
249            }
250            Self::Reverb(r) => {
251                s.kind = 4;
252                within(r.decay_seconds as f64, 0.05, 20.0)?;
253                within(r.damping as f64, 0.0, 0.99f32 as f64)?;
254                within(r.mix as f64, 0.0, 1.0)?;
255                s.values = [
256                    r.decay_seconds as f64,
257                    r.damping as f64,
258                    r.mix as f64,
259                    0.0,
260                    0.0,
261                    0.0,
262                ];
263            }
264            Self::Chorus(c) => {
265                s.kind = 5;
266                s.values = c.values(rate)?;
267            }
268            Self::Flanger(f) => {
269                s.kind = 6;
270                s.values = f.values(rate)?;
271            }
272            Self::Phaser(p) => {
273                s.kind = 7;
274                s.values = p.values(rate)?;
275            }
276            Self::Bitcrusher(b) => {
277                s.kind = 8;
278                s.values = b.values()?;
279            }
280            Self::Saturation(d) => {
281                s.kind = 9;
282                s.values = d.values()?;
283            }
284        }
285        Ok(s)
286    }
287}
288fn within(value: f64, lo: f64, hi: f64) -> Result<()> {
289    if !(lo..=hi).contains(&value) {
290        Err(Error::invalid(
291            "Effect parameter is non-finite or outside its documented range",
292        ))
293    } else {
294        Ok(())
295    }
296}
297
298pub(super) struct Chain {
299    nodes: Vec<Node>,
300}
301struct Node {
302    settings: Settings,
303    ramps: [Ramp; 6],
304    state: State,
305}
306enum State {
307    Colour(colour::Processor),
308    Filter {
309        memory: [f64; 8],
310        cutoff: f64,
311        coefficient: f64,
312    },
313    Compressor {
314        gain: f64,
315        threshold: Cache,
316        makeup: Cache,
317        attack: Cache,
318        release: Cache,
319    },
320    Delay {
321        ring: Vec<f64>,
322        at: usize,
323    },
324    Reverb {
325        channels: Vec<ReverbChannel>,
326    },
327}
328struct Line {
329    ring: Vec<f64>,
330    at: usize,
331    memory: f64,
332}
333impl Line {
334    fn new(len: usize) -> Self {
335        Self {
336            ring: vec![0.0; len.max(1)],
337            at: 0,
338            memory: 0.0,
339        }
340    }
341    fn get(&self) -> f64 {
342        self.ring[self.at]
343    }
344    fn put(&mut self, value: f64) {
345        self.ring[self.at] = value;
346        self.at = (self.at + 1) % self.ring.len();
347    }
348}
349struct ReverbChannel {
350    combs: [Line; 4],
351    diffusers: [Line; 2],
352    decay: f64,
353    feedback: [f64; 4],
354}
355struct Cache {
356    input: f64,
357    output: f64,
358}
359impl Cache {
360    fn new() -> Self {
361        Self {
362            input: f64::NAN,
363            output: 0.0,
364        }
365    }
366    fn get(&mut self, input: f64, compute: impl FnOnce(f64) -> f64) -> f64 {
367        if self.input != input {
368            self.input = input;
369            self.output = compute(input);
370        }
371        self.output
372    }
373}
374
375impl Chain {
376    fn new(effects: &[Effect], rate: u32, ch: usize, bus: BusId, buses: usize) -> Result<Self> {
377        if effects.len() > MAX_EFFECTS {
378            return Err(Error::limit("Too many effects on a bus"));
379        }
380        // Validate the whole chain before allocating any DSP buffers.
381        let settings: Vec<_> = effects
382            .iter()
383            .map(|e| e.settings(rate, bus, buses))
384            .collect::<Result<_>>()?;
385        let nodes = settings
386            .into_iter()
387            .map(|s| {
388                let state = match s.kind {
389                    0 | 1 => State::Filter {
390                        memory: [0.0; 8],
391                        cutoff: 0.0,
392                        coefficient: 0.0,
393                    },
394                    2 => State::Compressor {
395                        gain: 1.0,
396                        threshold: Cache::new(),
397                        makeup: Cache::new(),
398                        attack: Cache::new(),
399                        release: Cache::new(),
400                    },
401                    3 => State::Delay {
402                        ring: vec![0.0; (s.capacity.ceil() as usize + 2) * ch],
403                        at: 0,
404                    },
405                    4 => State::Reverb {
406                        channels: (0..ch)
407                            .map(|c| {
408                                let line = |seconds: f64| {
409                                    Line::new(((seconds + c as f64 * 0.0011) * rate as f64).round()
410                                        as usize)
411                                };
412                                ReverbChannel {
413                                    combs: [line(0.0297), line(0.0371), line(0.0411), line(0.0437)],
414                                    diffusers: [line(0.005), line(0.0017)],
415                                    decay: 0.0,
416                                    feedback: [0.0; 4],
417                                }
418                            })
419                            .collect(),
420                    },
421                    _ => State::Colour(colour::Processor::new(s.kind, rate, ch)),
422                };
423                Node {
424                    settings: s,
425                    ramps: s.values.map(Ramp::constant),
426                    state,
427                }
428            })
429            .collect();
430        Ok(Self { nodes })
431    }
432    fn accepts(&self, slot: usize, settings: Settings) -> bool {
433        self.nodes.get(slot).is_some_and(|n| {
434            n.settings.kind == settings.kind
435                && (settings.kind != 3 || settings.capacity <= n.settings.capacity)
436        })
437    }
438    fn update(&mut self, slot: usize, settings: Settings, clock: u64, len: u64, curve: FadeCurve) {
439        let n = &mut self.nodes[slot];
440        // Delay capacity belongs to preparation, not parameter automation.
441        n.settings.key = settings.key;
442        for (r, value) in n.ramps.iter_mut().zip(settings.values) {
443            r.retarget(clock, value, len, curve);
444        }
445    }
446}
447
448impl Node {
449    fn process(&mut self, clock: u64, rate: u32, ch: usize, out: &mut [f32], key: Option<&[f32]>) {
450        let kind = self.settings.kind;
451        for (i, frame) in out.chunks_exact_mut(ch).enumerate() {
452            let p = self.ramps.map(|r| r.value(clock + i as u64));
453            let mut x = [0.0; 8];
454            for c in 0..ch {
455                x[c] = finite(frame[c] as f64);
456            }
457            match &mut self.state {
458                State::Colour(processor) => processor.process(&mut x, p, rate, ch),
459                State::Filter {
460                    memory,
461                    cutoff,
462                    coefficient,
463                } => {
464                    if *cutoff != p[0] {
465                        let turns = p[0] / (2.0 * rate as f64);
466                        let g = sin_turns(turns) / cos_turns(turns);
467                        *coefficient = g / (1.0 + g);
468                        *cutoff = p[0];
469                    }
470                    for c in 0..ch {
471                        let v = (x[c] - memory[c]) * *coefficient;
472                        let low = v + memory[c];
473                        memory[c] = low + v;
474                        x[c] = if kind == 0 { low } else { x[c] - low };
475                    }
476                }
477                State::Compressor {
478                    gain,
479                    threshold,
480                    makeup,
481                    attack,
482                    release,
483                } => {
484                    let peak = if let Some(key) = key {
485                        key[i * ch..i * ch + ch]
486                            .iter()
487                            .map(|&v| finite(v as f64).abs())
488                            .fold(0.0, f64::max)
489                    } else {
490                        x[..ch].iter().map(|v| v.abs()).fold(0.0, f64::max)
491                    };
492                    let threshold = threshold.get(p[0], db_to_gain);
493                    let reduction = if peak > threshold {
494                        crate::math::exp2(log2(threshold / peak) * (1.0 - 1.0 / p[1]))
495                    } else {
496                        1.0
497                    };
498                    let target = 1.0 - p[5] * (1.0 - reduction);
499                    let coefficient = |ms| {
500                        if ms == 0.0 {
501                            0.0
502                        } else {
503                            exp(-1000.0 / (ms * rate as f64))
504                        }
505                    };
506                    let a = if target < *gain {
507                        attack.get(p[2], coefficient)
508                    } else {
509                        release.get(p[3], coefficient)
510                    };
511                    *gain = target + (*gain - target) * a;
512                    let g = *gain * makeup.get(p[4], db_to_gain);
513                    for s in &mut x[..ch] {
514                        *s *= g;
515                    }
516                }
517                State::Delay { ring, at } => {
518                    let frames = ring.len() / ch;
519                    let mut pos = *at as f64 - p[0];
520                    if pos < 0.0 {
521                        pos += frames as f64;
522                    }
523                    let a = pos.floor() as usize;
524                    let b = (a + 1) % frames;
525                    let f = pos - a as f64;
526                    for c in 0..ch {
527                        let delayed = ring[a * ch + c] * (1.0 - f) + ring[b * ch + c] * f;
528                        ring[*at * ch + c] = finite(x[c] + delayed * p[1]);
529                        x[c] = x[c] * (1.0 - p[2]) + delayed * p[2];
530                    }
531                    *at = (*at + 1) % frames;
532                }
533                State::Reverb { channels } => {
534                    for c in 0..ch {
535                        let channel = &mut channels[c];
536                        if channel.decay != p[0] {
537                            channel.decay = p[0];
538                            for (feedback, line) in channel.feedback.iter_mut().zip(&channel.combs)
539                            {
540                                *feedback = db_to_gain(
541                                    -60.0 * line.ring.len() as f64 / (rate as f64 * p[0]),
542                                );
543                            }
544                        }
545                        let mut wet = 0.0;
546                        for (line, &feedback) in channel.combs.iter_mut().zip(&channel.feedback) {
547                            let d = line.get();
548                            line.memory = d * (1.0 - p[1]) + line.memory * p[1];
549                            line.put(finite(x[c] + line.memory * feedback));
550                            wet += d * 0.25;
551                        }
552                        for line in &mut channel.diffusers {
553                            let value = line.get() - wet * 0.5;
554                            line.put(wet + value * 0.5);
555                            wet = value;
556                        }
557                        x[c] = x[c] * (1.0 - p[2]) + wet * p[2];
558                    }
559                }
560            }
561            for c in 0..ch {
562                frame[c] = finite(x[c]) as f32;
563            }
564        }
565    }
566}
567fn finite(x: f64) -> f64 {
568    if x.is_nan() {
569        0.0
570    } else {
571        x.clamp(-(f32::MAX as f64), f32::MAX as f64)
572    }
573}
574
575// Each owning edit has one credit until the handle destroys the returned box.
576// Installing swaps its chain with the bus's; the SAME box comes back, carrying the
577// old chain (or the refused new chain). Thus even endless unpolled renders cannot
578// overflow retirement storage or destroy DSP memory in render.
579pub(super) struct Edit {
580    bus: usize,
581    chain: Option<Box<Chain>>,
582    event: Option<MixerEvent>,
583    credits: Arc<AtomicUsize>,
584}
585impl Drop for Edit {
586    fn drop(&mut self) {
587        self.credits.fetch_sub(1, Ordering::Release);
588    }
589}
590pub(super) struct AudioEffects {
591    returns: Producer<Box<Edit>>,
592    retired: VecDeque<Box<Edit>>,
593    pub(super) order: [usize; 256],
594}
595pub(super) struct ControlEffects {
596    returns: Consumer<Box<Edit>>,
597    credits: Arc<AtomicUsize>,
598    capacity: usize,
599}
600pub(super) fn create(capacity: usize, buses: usize) -> (AudioEffects, ControlEffects) {
601    let (tx, rx) = queue::queue(capacity);
602    let mut order = [0; 256];
603    for (i, b) in order[..buses - 1].iter_mut().enumerate() {
604        *b = i + 1;
605    }
606    (
607        AudioEffects {
608            returns: tx,
609            retired: VecDeque::with_capacity(capacity),
610            order,
611        },
612        ControlEffects {
613            returns: rx,
614            credits: Arc::new(AtomicUsize::new(0)),
615            capacity,
616        },
617    )
618}
619impl AudioEffects {
620    pub(super) fn send_returns(&mut self) {
621        while let Some(edit) = self.retired.pop_front() {
622            match self.returns.push(edit) {
623                Ok(()) => (),
624                Err(PushError::Full(edit) | PushError::Disconnected(edit)) => {
625                    self.retired.push_front(edit);
626                    break;
627                }
628            }
629        }
630    }
631}
632impl ControlEffects {
633    pub(super) fn poll(&mut self) -> Option<MixerEvent> {
634        let edit = self.returns.pop()?;
635        edit.event
636        // The box and its old DSP storage die here, on the control thread.
637    }
638}
639
640impl MixerHandle {
641    /// Replaces a bus's chain. Preparation may allocate; installation is atomic and
642    /// sample scheduled. An empty slice removes the chain and its tail immediately.
643    pub fn set_effects(&mut self, bus: BusId, effects: &[Effect]) -> Result<()> {
644        self.now().set_effects(bus, effects)
645    }
646    /// Ramps all numeric settings of an existing matching slot in place. Type or
647    /// capacity mismatches and dependency cycles are reported asynchronously.
648    /// Changing the sidechain key is immediate; gain reduction uses attack/release.
649    pub fn set_effect(
650        &mut self,
651        bus: BusId,
652        slot: usize,
653        effect: Effect,
654        fade: Fade,
655    ) -> Result<()> {
656        self.now().set_effect(bus, slot, effect, fade)
657    }
658}
659impl Commands<'_> {
660    pub fn set_effects(self, bus: BusId, effects: &[Effect]) -> Result<()> {
661        if bus.0 > self.handle.buses.len() {
662            return Err(Error::invalid("Unknown bus"));
663        }
664        let fx = &self.handle.effects;
665        if fx.credits.load(Ordering::Acquire) >= fx.capacity {
666            return Err(Error::limit(
667                "Poll mixer events before sending more effect chain edits",
668            ));
669        }
670        let chain = Chain::new(
671            effects,
672            self.handle.rate,
673            self.handle.channels,
674            bus,
675            self.handle.buses.len() + 1,
676        )?;
677        let chain = if effects.is_empty() {
678            None
679        } else {
680            Some(Box::new(chain))
681        };
682        fx.credits.fetch_add(1, Ordering::Relaxed);
683        let edit = Box::new(Edit {
684            bus: bus.0,
685            chain,
686            event: None,
687            credits: fx.credits.clone(),
688        });
689        self.handle.send(self.frame, Action::Effects(edit))
690    }
691    pub fn set_effect(self, bus: BusId, slot: usize, effect: Effect, fade: Fade) -> Result<()> {
692        if bus.0 > self.handle.buses.len() || slot >= MAX_EFFECTS {
693            return Err(Error::invalid("Unknown bus or effect slot"));
694        }
695        let settings = effect.settings(self.handle.rate, bus, self.handle.buses.len() + 1)?;
696        let (len, curve) = self.handle.frames(fade)?;
697        self.handle.send(
698            self.frame,
699            Action::Effect(bus.0, slot, settings, len, curve),
700        )
701    }
702}
703
704pub(super) enum Change<'a> {
705    Current,
706    Chain(usize, Option<&'a Chain>),
707    Parameter(usize, usize, Settings),
708}
709// Kahn sort with fixed storage, evaluated against actual application-time chains.
710// Master is always last and cannot be a source or detector key. Named audio and
711// detector dependencies are both checked against actual application-time state.
712pub(super) fn topology(
713    buses: &[Bus],
714    change: Change<'_>,
715    graph: Option<&super::routing::Graph>,
716) -> Option<[usize; 256]> {
717    let mut done = [false; 256];
718    let mut order = [0; 256];
719    for entry in &mut order[..buses.len() - 1] {
720        let ready = (1..buses.len()).find(|&b| {
721            if done[b] {
722                return false;
723            }
724            if graph.is_some_and(|g| g.incoming[b].iter().any(|&source| !done[source])) {
725                return false;
726            }
727            let chain = match change {
728                Change::Chain(target, chain) if target == b => chain,
729                _ => buses[b].effects.as_deref(),
730            };
731            chain.is_none_or(|chain| {
732                chain.nodes.iter().enumerate().all(|(slot, node)| {
733                    let key = match change {
734                        Change::Parameter(target, index, settings)
735                            if target == b && index == slot =>
736                        {
737                            settings.key
738                        }
739                        _ => node.settings.key,
740                    };
741                    key.is_none_or(|key| done[key])
742                })
743            })
744        })?;
745        *entry = ready;
746        done[ready] = true;
747    }
748    Some(order)
749}
750
751impl Mixer {
752    /// Opt-in audio graph: inserts run once on summed input, then pre/post-fader
753    /// routes feed destinations in the combined audio/detector topological order.
754    pub(super) fn mix_routing(&mut self, first: u64, out: &mut [f32]) {
755        use super::routing::SendPosition;
756        let graph = self.routing.graph.as_mut().unwrap();
757        let ch = self.channels;
758        let frames = out.len() / ch;
759        let master_paused = self.buses[0].paused;
760        // At this point buffers contain direct voices only. Sources keep their
761        // clocks even when solo discards their contribution at this input stage.
762        for (b, bus) in self.buses.iter_mut().enumerate() {
763            if !graph.input[b] {
764                bus.buffer[..out.len()].fill(0.0);
765            }
766        }
767        for index in 0..self.buses.len() {
768            let b = self.effects.order[index];
769            let node = &graph.nodes[b];
770            let pre = node
771                .edges
772                .iter()
773                .any(|e| e.enabled && e.position == SendPosition::PreFader);
774            let (before, rest) = self.buses.split_at_mut(b);
775            let (bus, after) = rest.split_first_mut().unwrap();
776            let buffer = &mut bus.buffer[..out.len()];
777            if master_paused || bus.paused {
778                buffer.fill(0.0);
779                if pre {
780                    graph.pre[..out.len()].fill(0.0);
781                }
782            } else {
783                if let Some(chain) = &mut bus.effects {
784                    for effect in &mut chain.nodes {
785                        let key = effect.settings.key.map(|key| {
786                            if key < b {
787                                &before[key].buffer[..out.len()]
788                            } else {
789                                &after[key - b - 1].buffer[..out.len()]
790                            }
791                        });
792                        effect.process(bus.effect_clock, self.rate, ch, buffer, key);
793                    }
794                }
795                bus.effect_clock += frames as u64;
796                for (i, frame) in buffer.chunks_exact_mut(ch).enumerate() {
797                    let at = first + i as u64;
798                    let pause = bus.pause.value(at);
799                    let gain = bus.gain.value(at) * pause;
800                    for (c, sample) in frame.iter_mut().enumerate() {
801                        if pre {
802                            graph.pre[i * ch + c] = (*sample as f64 * pause) as f32;
803                        }
804                        let pan = if node.pan == 0.0 {
805                            1.0
806                        } else if c == 0 {
807                            1.0 - node.pan.max(0.0)
808                        } else {
809                            1.0 + node.pan.min(0.0)
810                        };
811                        *sample = (*sample as f64 * (gain * pan)) as f32;
812                    }
813                }
814                if node.muted || !node.audible {
815                    buffer.fill(0.0);
816                    if pre {
817                        graph.pre[..out.len()].fill(0.0);
818                    }
819                }
820            }
821            for edge in node.edges.iter().filter(|e| e.enabled) {
822                let source = if edge.position == SendPosition::PreFader {
823                    &graph.pre[..out.len()]
824                } else {
825                    &buffer[..]
826                };
827                let to = edge.destination;
828                let destination = if to < b {
829                    &mut before[to].buffer[..out.len()]
830                } else {
831                    &mut after[to - b - 1].buffer[..out.len()]
832                };
833                for (dest, &sample) in destination.iter_mut().zip(source) {
834                    *dest += (sample as f64 * edge.gain) as f32;
835                }
836            }
837            if b == 0 {
838                out.copy_from_slice(buffer);
839            }
840        }
841    }
842    pub(super) fn apply_effects(&mut self, frame: u64, mut edit: Box<Edit>) {
843        let bus = edit.bus;
844        if let Some(order) = topology(
845            &self.buses,
846            Change::Chain(bus, edit.chain.as_deref()),
847            self.routing.graph.as_deref(),
848        ) {
849            self.effects.order = order;
850            std::mem::swap(&mut self.buses[bus].effects, &mut edit.chain);
851            edit.event = Some(MixerEvent::EffectsApplied(BusId(bus), frame));
852        } else {
853            edit.event = Some(MixerEvent::EffectsRejected(
854                BusId(bus),
855                ErrorCode::InvalidArgument,
856            ));
857        }
858        self.effects.retired.push_back(edit);
859    }
860    pub(super) fn apply_effect(
861        &mut self,
862        bus: usize,
863        slot: usize,
864        settings: Settings,
865        len: u64,
866        curve: FadeCurve,
867    ) {
868        let valid = self.buses[bus]
869            .effects
870            .as_ref()
871            .is_some_and(|c| c.accepts(slot, settings));
872        let order = valid
873            .then(|| {
874                topology(
875                    &self.buses,
876                    Change::Parameter(bus, slot, settings),
877                    self.routing.graph.as_deref(),
878                )
879            })
880            .flatten();
881        if let Some(order) = order {
882            self.effects.order = order;
883            let bus = &mut self.buses[bus];
884            bus.effects
885                .as_mut()
886                .unwrap()
887                .update(slot, settings, bus.effect_clock, len, curve);
888        } else {
889            self.event(MixerEvent::EffectsRejected(
890                BusId(bus),
891                ErrorCode::InvalidArgument,
892            ));
893        }
894    }
895
896    /// Effects and routing for a segment, using post-effect/post-fader key buses.
897    pub(super) fn mix_effects(&mut self, first: u64, out: &mut [f32]) {
898        let ch = self.channels;
899        let frames = out.len() / ch;
900        let master_paused = self.buses[0].paused;
901        for index in 0..self.buses.len() {
902            let b = self.effects.order[index];
903            if b == 0 {
904                for bus in &self.buses[1..] {
905                    for (o, &s) in out.iter_mut().zip(&bus.buffer) {
906                        *o += s;
907                    }
908                }
909                for (o, &s) in out.iter_mut().zip(&self.buses[0].buffer) {
910                    *o += s;
911                }
912                self.buses[0].buffer[..out.len()].copy_from_slice(out);
913            }
914            // Split borrowing around the target, then supply key buffers through a
915            // small stack view rather than copying any audio or allocating a Vec.
916            let (before, rest) = self.buses.split_at_mut(b);
917            let (bus, after) = rest.split_first_mut().unwrap();
918            let buffer = &mut bus.buffer[..out.len()];
919            if master_paused || bus.paused {
920                buffer.fill(0.0);
921            } else {
922                if let Some(chain) = &mut bus.effects {
923                    // Each node only needs its own key; splitting gives disjoint borrows.
924                    for node in &mut chain.nodes {
925                        let key = node.settings.key.map(|key| {
926                            if key < b {
927                                &before[key].buffer[..out.len()]
928                            } else {
929                                &after[key - b - 1].buffer[..out.len()]
930                            }
931                        });
932                        node.process(bus.effect_clock, self.rate, ch, buffer, key);
933                    }
934                }
935                bus.effect_clock += frames as u64;
936                for (i, frame) in buffer.chunks_exact_mut(ch).enumerate() {
937                    let at = first + i as u64;
938                    let gain = bus.gain.value(at) * bus.pause.value(at);
939                    for sample in frame {
940                        *sample = (*sample as f64 * gain) as f32;
941                    }
942                }
943            }
944            if b == 0 {
945                out.copy_from_slice(buffer);
946            }
947        }
948    }
949}