tono-core 1.8.0

The pure, headless audio engine behind tono: synthesis-graph DSL, DSP, deterministic renderer, instruments, songs, and analysis — no I/O, no transport.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
//! The routing [`Mixer`]: input/FX buses with insert chains, faders, and
//! post-fader sends over any set of [`AudioSource`]s.

use super::source::AudioSource;
use crate::dsl::{ENGINE_VERSION, Node};
use crate::streaming::EffectChain;

/// Handle to a source added to a [`Mixer`].
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SourceId(u64);

/// Blanket-implemented for every source, so a [`Mixer`] can hand back a typed
/// `&mut` to a source it owns without forcing `Any` onto the public
/// [`AudioSource`] trait (every plain `fill`-only adapter stays unencumbered).
trait AnySource: AudioSource + std::any::Any {}
impl<T: AudioSource + 'static> AnySource for T {}

struct MixedSource {
    id: u64,
    source: Box<dyn AnySource + Send>,
    gain: f32,
    /// The bus this source feeds ([`BusId::MASTER`] by default).
    bus: BusId,
}

/// Handle to a bus in a [`Mixer`] — an input group or an FX/return bus. The
/// master bus is always [`BusId::MASTER`]. The handle's value is the bus's index
/// in the mixer, so it is stable for the mixer's life.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct BusId(u32);

impl BusId {
    /// The always-present master bus. Every source, dry bus output, and FX
    /// return sums here, and the master insert chain is the final stage.
    pub const MASTER: BusId = BusId(0);
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum BusKind {
    Master,
    Input,
    Fx,
}

/// Why a [`Mixer`] bus operation failed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MixerError {
    /// An effect node is outside the real-time-streamable subset.
    NotStreamable,
    /// The mixer was built without a sample rate ([`Mixer::new`]); effect chains
    /// need [`Mixer::new_at`].
    NoSampleRate,
}

impl std::fmt::Display for MixerError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MixerError::NotStreamable => {
                write!(f, "effect chain contains a non-streamable node")
            }
            MixerError::NoSampleRate => {
                write!(f, "mixer has no sample rate; build it with Mixer::new_at")
            }
        }
    }
}

impl std::error::Error for MixerError {}

/// One bus: a summing point with an optional insert chain, a fader, post-fader
/// sends to FX buses, and a dry level into master.
struct Bus {
    name: String,
    kind: BusKind,
    gain: f32,
    /// Dry level into master (0 = send-only). Unused by the master bus.
    to_master: f32,
    /// A stereo insert chain (identical coefficients, independent L/R state).
    inserts: Option<(EffectChain, EffectChain)>,
    /// Post-fader sends into FX buses, as `(target bus index, level)`.
    sends: Vec<(u32, f32)>,
}

/// A routing stereo mixer of audio sources — instruments, the SFX [`Engine`](super::Engine),
/// [`StreamSource`](super::StreamSource)s. Each source feeds a **bus**; buses carry live insert
/// chains (reverb / EQ / compressor / …) and post-fader **sends** into shared
/// FX/return buses, all summing through a **master** insert chain. It is itself
/// an [`AudioSource`], so it feeds one output callback (or nests).
///
/// With no buses or effects created, every source sits on the master bus at unity
/// and the output is a plain additive sum — byte-identical to a bare mixer. Live
/// effects need a sample rate: build with [`Mixer::new_at`].
pub struct Mixer {
    sources: Vec<MixedSource>,
    /// `buses[0]` is always the master bus; a bus's index equals its [`BusId`].
    buses: Vec<Bus>,
    next_id: u64,
    sample_rate: Option<u32>,
    // Reused planar scratch (grown lazily, never shrunk).
    scratch: Vec<f32>,
    master_l: Vec<f32>,
    master_r: Vec<f32>,
    bus_l: Vec<f32>,
    bus_r: Vec<f32>,
    /// Per-bus FX input accumulators (indexed by bus index; only FX slots used).
    fx_in: Vec<(Vec<f32>, Vec<f32>)>,
}

impl Default for Mixer {
    fn default() -> Self {
        Mixer::new()
    }
}

impl Mixer {
    /// An empty mixer with no sample rate. Sources sum additively; adding effect
    /// chains returns [`MixerError::NoSampleRate`] — use [`Mixer::new_at`] for FX.
    pub fn new() -> Self {
        Mixer::build(None)
    }

    /// An empty mixer that renders at `sample_rate`, so buses can carry live
    /// effect chains.
    pub fn new_at(sample_rate: u32) -> Self {
        Mixer::build(Some(sample_rate))
    }

    fn build(sample_rate: Option<u32>) -> Self {
        let master = Bus {
            name: "master".into(),
            kind: BusKind::Master,
            gain: 1.0,
            to_master: 1.0,
            inserts: None,
            sends: Vec::new(),
        };
        Mixer {
            sources: Vec::new(),
            buses: vec![master],
            next_id: 1,
            sample_rate,
            scratch: Vec::new(),
            master_l: Vec::new(),
            master_r: Vec::new(),
            bus_l: Vec::new(),
            bus_r: Vec::new(),
            fx_in: Vec::new(),
        }
    }

    /// Add a source to the master bus at unity gain; returns its handle.
    pub fn add(&mut self, source: impl AudioSource + Send + 'static) -> SourceId {
        self.add_to(BusId::MASTER, source)
    }

    /// Add a source to a specific bus at unity gain; returns its handle. An
    /// unknown bus falls back to master.
    pub fn add_to(&mut self, bus: BusId, source: impl AudioSource + Send + 'static) -> SourceId {
        let bus = if (bus.0 as usize) < self.buses.len() {
            bus
        } else {
            BusId::MASTER
        };
        let id = self.next_id;
        self.next_id += 1;
        self.sources.push(MixedSource {
            id,
            source: Box::new(source),
            gain: 1.0,
            bus,
        });
        SourceId(id)
    }

    /// Create an input bus (sources → inserts → master, with optional sends).
    pub fn bus(&mut self, name: impl Into<String>) -> BusId {
        self.push_bus(name.into(), BusKind::Input, None)
    }

    /// Create an FX/return bus with an insert chain fed only by sends. Returns
    /// [`MixerError`] if the effects aren't streamable or the mixer has no rate.
    pub fn fx_bus(
        &mut self,
        name: impl Into<String>,
        effects: Vec<Node>,
    ) -> Result<BusId, MixerError> {
        let inserts = self.build_chain(&effects)?;
        Ok(self.push_bus(name.into(), BusKind::Fx, inserts))
    }

    fn push_bus(
        &mut self,
        name: String,
        kind: BusKind,
        inserts: Option<(EffectChain, EffectChain)>,
    ) -> BusId {
        // A bus's id is its index — buses are only ever pushed, never removed.
        let id = self.buses.len() as u32;
        self.buses.push(Bus {
            name,
            kind,
            gain: 1.0,
            to_master: 1.0,
            inserts,
            sends: Vec::new(),
        });
        BusId(id)
    }

    /// Look up a bus by name.
    pub fn bus_named(&self, name: &str) -> Option<BusId> {
        self.buses
            .iter()
            .position(|b| b.name == name)
            .map(|i| BusId(i as u32))
    }

    /// Set (or clear, with an empty list) a bus's insert chain. Works on any bus,
    /// including master.
    pub fn set_bus_effects(&mut self, bus: BusId, effects: Vec<Node>) -> Result<(), MixerError> {
        let inserts = self.build_chain(&effects)?;
        if let Some(b) = self.buses.get_mut(bus.0 as usize) {
            b.inserts = inserts;
        }
        Ok(())
    }

    /// Set the master insert chain (a convenience for `set_bus_effects(MASTER, …)`).
    pub fn master_effects(&mut self, effects: Vec<Node>) -> Result<(), MixerError> {
        self.set_bus_effects(BusId::MASTER, effects)
    }

    /// Set a post-fader send from an input bus into an FX bus. A no-op unless
    /// `from` is an input bus and `to_fx` is an FX bus.
    pub fn set_send(&mut self, from: BusId, to_fx: BusId, level: f32) {
        let valid = matches!(
            self.buses.get(from.0 as usize).map(|b| b.kind),
            Some(BusKind::Input)
        ) && matches!(
            self.buses.get(to_fx.0 as usize).map(|b| b.kind),
            Some(BusKind::Fx)
        );
        if !valid {
            return;
        }
        let level = level.max(0.0);
        let bus = &mut self.buses[from.0 as usize];
        if let Some(s) = bus.sends.iter_mut().find(|s| s.0 == to_fx.0) {
            s.1 = level;
        } else {
            bus.sends.push((to_fx.0, level));
        }
    }

    /// Set a bus fader (0 = silent). Applies to the bus's dry output and its sends.
    pub fn set_bus_gain(&mut self, bus: BusId, gain: f32) {
        if let Some(b) = self.buses.get_mut(bus.0 as usize) {
            b.gain = gain.max(0.0);
        }
    }

    /// Set a bus's dry level into master (0 = send-only). No-op for master.
    pub fn set_bus_dry(&mut self, bus: BusId, level: f32) {
        if bus != BusId::MASTER
            && let Some(b) = self.buses.get_mut(bus.0 as usize)
        {
            b.to_master = level.max(0.0);
        }
    }

    /// Build a paired L/R insert chain from effect nodes (empty → `None`).
    fn build_chain(
        &self,
        effects: &[Node],
    ) -> Result<Option<(EffectChain, EffectChain)>, MixerError> {
        if effects.is_empty() {
            return Ok(None);
        }
        let sr = self.sample_rate.ok_or(MixerError::NoSampleRate)?;
        let build = || EffectChain::try_new(effects, sr, ENGINE_VERSION);
        let l = build().ok_or(MixerError::NotStreamable)?;
        let r = build().ok_or(MixerError::NotStreamable)?;
        Ok(Some((l, r)))
    }

    /// Set a source's gain (no-op for an unknown handle).
    pub fn set_gain(&mut self, id: SourceId, gain: f32) {
        if let Some(s) = self.sources.iter_mut().find(|s| s.id == id.0) {
            s.gain = gain.max(0.0);
        }
    }

    /// Mutable access to an added source, downcast to its concrete type — e.g. to
    /// call [`Instrument::note_on`](crate::instrument::Instrument::note_on).
    pub fn get_mut<T: AudioSource + 'static>(&mut self, id: SourceId) -> Option<&mut T> {
        let s = self.sources.iter_mut().find(|s| s.id == id.0)?;
        let any: &mut dyn std::any::Any = s.source.as_mut();
        any.downcast_mut::<T>()
    }

    /// Remove a source.
    pub fn remove(&mut self, id: SourceId) {
        self.sources.retain(|s| s.id != id.0);
    }

    /// Number of sources in the mix.
    pub fn source_count(&self) -> usize {
        self.sources.len()
    }

    /// Whether `id` still refers to a source in the mix.
    pub fn contains(&self, id: SourceId) -> bool {
        self.sources.iter().any(|s| s.id == id.0)
    }
}

/// Grow `v` to at least `n` samples (never shrinks).
fn grow(v: &mut Vec<f32>, n: usize) {
    if v.len() < n {
        v.resize(n, 0.0);
    }
}

impl AudioSource for Mixer {
    fn fill(&mut self, out: &mut [f32]) -> usize {
        let frames = out.len() / 2;

        // Take the reusable planar buffers out so we can also borrow
        // self.sources / self.buses mutably during routing.
        let mut scratch = std::mem::take(&mut self.scratch);
        let mut master_l = std::mem::take(&mut self.master_l);
        let mut master_r = std::mem::take(&mut self.master_r);
        let mut bus_l = std::mem::take(&mut self.bus_l);
        let mut bus_r = std::mem::take(&mut self.bus_r);
        let mut fx_in = std::mem::take(&mut self.fx_in);

        grow(&mut scratch, frames * 2);
        grow(&mut master_l, frames);
        grow(&mut master_r, frames);
        grow(&mut bus_l, frames);
        grow(&mut bus_r, frames);
        if fx_in.len() < self.buses.len() {
            fx_in.resize_with(self.buses.len(), || (Vec::new(), Vec::new()));
        }
        for (l, r) in fx_in.iter_mut() {
            grow(l, frames);
            grow(r, frames);
        }

        master_l[..frames].fill(0.0);
        master_r[..frames].fill(0.0);
        for (l, r) in fx_in.iter_mut() {
            l[..frames].fill(0.0);
            r[..frames].fill(0.0);
        }

        let scr = &mut scratch[..frames * 2];

        // 1a. Master-direct sources sum straight into the master accumulator.
        for s in self.sources.iter_mut().filter(|s| s.bus == BusId::MASTER) {
            s.source.fill(scr);
            for f in 0..frames {
                master_l[f] += scr[f * 2] * s.gain;
                master_r[f] += scr[f * 2 + 1] * s.gain;
            }
        }

        // 1b. Input buses: sum sources → inserts → dry to master + post-fader sends.
        for bi in 1..self.buses.len() {
            if self.buses[bi].kind != BusKind::Input {
                continue;
            }
            bus_l[..frames].fill(0.0);
            bus_r[..frames].fill(0.0);
            let bus_id = bi as u32;
            for s in self.sources.iter_mut().filter(|s| s.bus.0 == bus_id) {
                s.source.fill(scr);
                for f in 0..frames {
                    bus_l[f] += scr[f * 2] * s.gain;
                    bus_r[f] += scr[f * 2 + 1] * s.gain;
                }
            }
            if let Some((cl, cr)) = &mut self.buses[bi].inserts {
                cl.process(&mut bus_l[..frames]);
                cr.process(&mut bus_r[..frames]);
            }
            let fader = self.buses[bi].gain;
            let dry = fader * self.buses[bi].to_master;
            for f in 0..frames {
                master_l[f] += bus_l[f] * dry;
                master_r[f] += bus_r[f] * dry;
            }
            for &(target, level) in &self.buses[bi].sends {
                let k = target as usize;
                if k < fx_in.len() {
                    let g = fader * level;
                    let (fl, fr) = &mut fx_in[k];
                    for f in 0..frames {
                        fl[f] += bus_l[f] * g;
                        fr[f] += bus_r[f] * g;
                    }
                }
            }
        }

        // 1c. Sources routed directly onto an FX bus are wet-only: sum them into
        // that bus's accumulator so its inserts process them like any send.
        // Without this a source added with `add_to(fx_bus, ..)` is never mixed and
        // its play head never advances.
        #[allow(clippy::needless_range_loop)]
        for bi in 1..self.buses.len() {
            if self.buses[bi].kind != BusKind::Fx {
                continue;
            }
            let bus_id = bi as u32;
            let (fl, fr) = &mut fx_in[bi];
            for s in self.sources.iter_mut().filter(|s| s.bus.0 == bus_id) {
                s.source.fill(scr);
                for f in 0..frames {
                    fl[f] += scr[f * 2] * s.gain;
                    fr[f] += scr[f * 2 + 1] * s.gain;
                }
            }
        }

        // 2. FX buses: run inserts on the accumulated sends, return to master.
        // `bi` indexes both self.buses and fx_in, so a range loop is clearest.
        #[allow(clippy::needless_range_loop)]
        for bi in 1..self.buses.len() {
            if self.buses[bi].kind != BusKind::Fx {
                continue;
            }
            let (fl, fr) = &mut fx_in[bi];
            if let Some((cl, cr)) = &mut self.buses[bi].inserts {
                cl.process(&mut fl[..frames]);
                cr.process(&mut fr[..frames]);
            }
            let ret = self.buses[bi].gain * self.buses[bi].to_master;
            for f in 0..frames {
                master_l[f] += fl[f] * ret;
                master_r[f] += fr[f] * ret;
            }
        }

        // 3. Master insert chain, then the master fader, then interleave out.
        if let Some((cl, cr)) = &mut self.buses[0].inserts {
            cl.process(&mut master_l[..frames]);
            cr.process(&mut master_r[..frames]);
        }
        let master_gain = self.buses[0].gain;
        for f in 0..frames {
            out[f * 2] = master_l[f] * master_gain;
            out[f * 2 + 1] = master_r[f] * master_gain;
        }

        self.scratch = scratch;
        self.master_l = master_l;
        self.master_r = master_r;
        self.bus_l = bus_l;
        self.bus_r = bus_r;
        self.fx_in = fx_in;
        frames
    }

    /// Rewind every source so a transport restart replays the mix from the top.
    /// Bus insert/send state (reverb tails, delay lines) is left ringing.
    fn reset(&mut self) {
        for s in self.sources.iter_mut() {
            s.source.reset();
        }
    }
}