Skip to main content

audiobox/mixer/events/
mod.rs

1//! Named sound events, parsed and prepared on the control thread (`events` feature).
2//!
3//! One definition per line: `event footstep sources=left,right choice=shuffle
4//! gain=0.8,1 pitch=-1,1 max-instances=2 min-gap=0.08 bus=effects`.
5//! Names are case-sensitive ASCII identifiers (letters, digits, `_`, `-`, `.`).
6//! `#` begins a comment. See [`EventBank::parse`] for the complete grammar.
7//!
8//! Register decoded sounds in [`Sources`]; optional helpers render SFX and scores
9//! once, before playback. [`EventBank::prepare`] resolves every source and bus and
10//! checks every loop region. No parsing, file access or synthesis runs on audio.
11//! A prepared bank belongs to one mixer. Its players share per-event instance
12//! limits, including across variants, but have independent seeded selection state.
13//!
14//! Selection/gain/pitch are deterministic per event name and seed, independent of
15//! other events and the mixer's RNG. Errors returned by `play` do not consume that
16//! state. A successfully queued request consumes it even if audio later reports
17//! [`super::MixerEvent::Limited`] or [`super::MixerEvent::Rejected`]. Voice IDs and
18//! admission depend on other mixer activity. Poll mixer events normally to reclaim
19//! finished voices and distinguish submission from application. All event methods
20//! are control-thread operations and may allocate.
21//!
22//! ```
23//! use audiobox::events::{EventBank, Sources};
24//! use audiobox::mixer::{Mixer, MixerConfig};
25//! use audiobox::Audio;
26//! let (mut mixer, mut handle) = Mixer::new(MixerConfig::default())?;
27//! let mut sources = Sources::default();
28//! sources.insert("click", Audio::new(vec![vec![0.1; 100]], 48_000)?)?;
29//! let bank = EventBank::parse("event ui.click sources=click gain=0.5,0.8")?;
30//! let mut player = bank.prepare(&handle, &sources)?.player(42);
31//! let started = player.play(&mut handle, "ui.click")?;
32//! # mixer.render(&mut [0.0; 256]);
33//! # Ok::<(), Box<dyn std::error::Error>>(())
34//! ```
35mod parse;
36use super::{BusId, Emitter, Loop, MixerHandle, Play, VoiceId};
37use crate::{Audio, Error, Result};
38pub use parse::ParseError;
39use std::collections::BTreeMap;
40use std::sync::{Arc, atomic::AtomicU64};
41
42/// Source registry. Insertion and optional synthesis happen on the control thread.
43/// Prepared banks retain `Arc`s, so the registry may be dropped after preparation.
44#[derive(Default)]
45pub struct Sources {
46    sounds: BTreeMap<String, Arc<Audio>>,
47}
48impl Sources {
49    /// Registers mono/stereo audio without copying its samples. Duplicate or invalid
50    /// names are errors, leaving the previous entry intact.
51    pub fn insert(&mut self, name: &str, audio: impl Into<Arc<Audio>>) -> Result<()> {
52        if !parse::name_ok(name) || self.sounds.contains_key(name) {
53            return Err(Error::invalid("Invalid or duplicate source name"));
54        }
55        let audio = audio.into();
56        if !(1..=2).contains(&audio.channels()) {
57            return Err(Error::invalid("Event sources must be mono or stereo"));
58        }
59        self.sounds.insert(name.to_owned(), audio);
60        Ok(())
61    }
62    /// Renders and caches an effect once; requires `sfx`.
63    #[cfg(feature = "sfx")]
64    pub fn insert_effect(
65        &mut self,
66        name: &str,
67        effect: &crate::sfx::Effect,
68        rate: u32,
69    ) -> Result<()> {
70        self.insert(name, effect.render(rate)?)
71    }
72    /// Renders and caches a finite score once; requires `synth`.
73    #[cfg(feature = "synth")]
74    pub fn insert_score(
75        &mut self,
76        name: &str,
77        score: &crate::synth::Score,
78        rate: u32,
79        options: &crate::synth::RenderOptions,
80    ) -> Result<()> {
81        self.insert(name, score.render_with(rate, options)?)
82    }
83}
84
85#[derive(Clone, Copy)]
86enum Choice {
87    Random,
88    Shuffle,
89    Sequence,
90}
91#[derive(Clone)]
92struct Definition {
93    name: String,
94    sources: Vec<String>,
95    choice: Choice,
96    gain: [f32; 2],
97    pitch: [f32; 2],
98    bus: String,
99    play: Play,
100    source_location: parse::Location,
101    bus_location: parse::Location,
102    loop_location: parse::Location,
103}
104
105/// Parsed text bank, independent of a mixer or source registry.
106#[derive(Clone)]
107pub struct EventBank {
108    definitions: Vec<Definition>,
109}
110impl EventBank {
111    /// Parses UTF-8 text (at most 1 MiB, 1024 events, 64 source entries per event).
112    /// Blank/comment lines are allowed; an empty bank is valid. Every other line is
113    /// `event NAME sources=A,B [key=value ...]`. No whitespace inside values.
114    /// Identifiers contain 1..=64 ASCII letters/digits/`_`/`-`/`.`.
115    /// Unknown/repeated keys and duplicate event names are errors.
116    ///
117    /// | Key | Values / default |
118    /// | --- | --- |
119    /// | `sources` | Required comma-separated registered names; duplicates weight entries |
120    /// | `choice` | `random` (default), `shuffle`, `sequence` |
121    /// | `gain` | Linear `min,max` or fixed value, finite >=0; default 1 |
122    /// | `pitch` | Semitones `min,max` or fixed, -48..=48; default 0 |
123    /// | `bus` | Named mixer bus or `master` (default) |
124    /// | `priority` | 0..=255; default 128 |
125    /// | `max-instances` | Concurrent voices across all variants; 0 unlimited (default) |
126    /// | `min-gap` | Minimum seconds between admitted starts, 0..=60; default 0 |
127    /// | `policy` | `refuse` (default), `oldest`, `quietest` |
128    /// | `loop` | `off` (default), `whole`, or `start,end` source frames |
129    /// | `pan` | -1..=1, default 0; replaced by spatial placement |
130    /// | `rate` | 1/16..=8; default 1 |
131    /// | `fade-in` | Linear fade seconds, 0..=60; default 0 |
132    /// | `position` | `x,y` relative 2D position using the mixer's `Space2d` |
133    /// | `emitter` | `x,y,z` world position, mutually exclusive with `position` |
134    /// | `velocity` | `x,y,z` (default zero), requires `emitter` |
135    /// | `attenuation` | `model,min,max,rolloff`; model is `linear`, `inverse` or `exponential`; requires `emitter`; mixer defaults |
136    ///
137    /// Sequence wraps in text order. Shuffle draws every entry once per cycle and
138    /// avoids the previous entry at cycle boundaries when there are >=2 entries.
139    /// Random samples with replacement. Distinct entries may name the same sound.
140    pub fn parse(text: &str) -> std::result::Result<Self, ParseError> {
141        parse::bank(text)
142    }
143    pub fn len(&self) -> usize {
144        self.definitions.len()
145    }
146    pub fn is_empty(&self) -> bool {
147        self.definitions.is_empty()
148    }
149    /// Event names in definition order.
150    pub fn names(&self) -> impl Iterator<Item = &str> {
151        self.definitions.iter().map(|d| d.name.as_str())
152    }
153    /// Resolves all references and validates loop regions against every candidate.
154    /// Resolution errors retain the line/column of the corresponding text token.
155    /// Preparing again creates independent instance groups; clone the resulting bank
156    /// or create multiple players from it to share limits.
157    pub fn prepare(
158        &self,
159        handle: &MixerHandle,
160        sources: &Sources,
161    ) -> std::result::Result<PreparedBank, ParseError> {
162        let mut events = BTreeMap::new();
163        for d in &self.definitions {
164            let mut play = d.play;
165            play.bus = if d.bus == "master" {
166                BusId::MASTER
167            } else {
168                handle
169                    .bus(&d.bus)
170                    .ok_or_else(|| d.bus_location.invalid("Unknown event bus"))?
171            };
172            let mut sounds = Vec::new();
173            for name in &d.sources {
174                let audio = sources.sounds.get(name).ok_or_else(|| {
175                    d.source_location
176                        .invalid(format!("Unknown event source: {name}"))
177                })?;
178                if let Loop::Region { end, .. } = play.looping {
179                    if end > audio.frames() as u64 {
180                        return Err(d
181                            .loop_location
182                            .invalid(format!("Loop exceeds source: {name}")));
183                    }
184                }
185                sounds.push(audio.clone());
186            }
187            if let Some(limit) = &mut play.instances {
188                if limit.max == 0 && limit.min_gap == 0.0 {
189                    play.instances = None;
190                } else {
191                    limit.key = crate::identity::next();
192                }
193            }
194            events.insert(
195                d.name.clone(),
196                PreparedEvent {
197                    sounds,
198                    choice: d.choice,
199                    gain: d.gain,
200                    pitch: d.pitch,
201                    play,
202                },
203            );
204        }
205        Ok(PreparedBank {
206            data: Arc::new(Prepared {
207                clock: handle.clock.clone(),
208                events,
209            }),
210        })
211    }
212}
213struct PreparedEvent {
214    sounds: Vec<Arc<Audio>>,
215    choice: Choice,
216    gain: [f32; 2],
217    pitch: [f32; 2],
218    play: Play,
219}
220struct Prepared {
221    clock: Arc<AtomicU64>,
222    events: BTreeMap<String, PreparedEvent>,
223}
224/// Resolved, immutable, mixer-bound events. Clones share sources and instance keys.
225#[derive(Clone)]
226pub struct PreparedBank {
227    data: Arc<Prepared>,
228}
229impl PreparedBank {
230    /// Creates independent selection state. Each event is seeded from its name and
231    /// `seed`; adding/reordering other events does not change its choices.
232    pub fn player(&self, seed: u64) -> EventPlayer {
233        EventPlayer {
234            bank: self.clone(),
235            states: self
236                .data
237                .events
238                .keys()
239                .map(|name| {
240                    let hash = name.bytes().fold(0xcbf29ce484222325u64, |h, b| {
241                        (h ^ b as u64).wrapping_mul(0x100000001b3)
242                    });
243                    (
244                        name.clone(),
245                        State {
246                            random: seed ^ hash,
247                            cursor: 0,
248                            bag: Vec::new(),
249                            last: None,
250                        },
251                    )
252                })
253                .collect(),
254        }
255    }
256}
257/// Optional per-request placement, replacing the event's spatial defaults.
258#[derive(Clone, Copy, Debug)]
259pub enum Placement {
260    /// Removes spatial placement; uses the event's pan.
261    Pan,
262    Position([f32; 2]),
263    Emitter(Emitter),
264}
265/// Per-request scheduling and spatial override.
266#[derive(Clone, Copy, Debug, Default)]
267pub struct Trigger {
268    /// Absolute output frame. `None` applies at the next render, in submission
269    /// order with other immediate mixer commands.
270    pub frame: Option<u64>,
271    pub placement: Option<Placement>,
272}
273/// Successfully queued request, with selected parameters for replay/debugging.
274#[derive(Clone, Copy, Debug, PartialEq)]
275pub struct Triggered {
276    pub voice: VoiceId,
277    /// Zero-based index in the event's `sources` list.
278    pub source: usize,
279    pub gain: f32,
280    pub pitch: f32,
281}
282/// Seeded event selection state. Use returned voice IDs with normal mixer commands.
283pub struct EventPlayer {
284    bank: PreparedBank,
285    states: BTreeMap<String, State>,
286}
287impl EventPlayer {
288    pub fn play(&mut self, handle: &mut MixerHandle, name: &str) -> Result<Triggered> {
289        self.play_with(handle, name, Trigger::default())
290    }
291    /// Queues a named event. Immediate errors (including queue full or wrong mixer)
292    /// preserve all event selection state. Later audio-side refusal consumes it.
293    pub fn play_with(
294        &mut self,
295        handle: &mut MixerHandle,
296        name: &str,
297        trigger: Trigger,
298    ) -> Result<Triggered> {
299        if !Arc::ptr_eq(&self.bank.data.clock, &handle.clock) {
300            return Err(Error::invalid("Event bank belongs to another mixer"));
301        }
302        let event = self
303            .bank
304            .data
305            .events
306            .get(name)
307            .ok_or_else(|| Error::invalid("Unknown sound event"))?;
308        let state = self.states.get_mut(name).expect("prepared event state");
309        let mut next = state.clone();
310        let source = next.choose(event.choice, event.sounds.len());
311        let mut play = event.play;
312        play.gain = next.range(event.gain);
313        play.pitch = next.range(event.pitch);
314        if let Some(placement) = trigger.placement {
315            play.position = None;
316            play.emitter = None;
317            match placement {
318                Placement::Pan => {}
319                Placement::Position(p) => play.position = Some(p),
320                Placement::Emitter(e) => play.emitter = Some(e),
321            }
322        }
323        let sound = event.sounds[source].clone();
324        let voice = match trigger.frame {
325            Some(frame) => handle.at(frame).play(sound, &play)?,
326            None => handle.play(sound, &play)?,
327        };
328        *state = next;
329        Ok(Triggered {
330            voice,
331            source,
332            gain: play.gain,
333            pitch: play.pitch,
334        })
335    }
336}
337#[derive(Clone)]
338struct State {
339    random: u64,
340    cursor: usize,
341    bag: Vec<usize>,
342    last: Option<usize>,
343}
344impl State {
345    fn next(&mut self) -> u64 {
346        self.random = self.random.wrapping_add(0x9e3779b97f4a7c15);
347        let mut z = self.random;
348        z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
349        z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
350        z ^ (z >> 31)
351    }
352    fn index(&mut self, n: usize) -> usize {
353        let n = n as u64;
354        let threshold = n.wrapping_neg() % n;
355        loop {
356            let r = self.next();
357            if r >= threshold {
358                return (r % n) as usize;
359            }
360        }
361    }
362    fn range(&mut self, [min, max]: [f32; 2]) -> f32 {
363        if min == max {
364            return min;
365        }
366        let unit = (self.next() >> 40) as f64 / 16_777_216.0;
367        (min as f64 + (max as f64 - min as f64) * unit) as f32
368    }
369    fn choose(&mut self, choice: Choice, n: usize) -> usize {
370        match choice {
371            Choice::Random => self.index(n),
372            Choice::Sequence => {
373                let result = self.cursor;
374                self.cursor = (self.cursor + 1) % n;
375                result
376            }
377            Choice::Shuffle => {
378                if self.cursor == self.bag.len() {
379                    self.bag = (0..n).collect();
380                    for i in (1..n).rev() {
381                        let j = self.index(i + 1);
382                        self.bag.swap(i, j);
383                    }
384                    if n > 1 && self.last == Some(self.bag[0]) {
385                        let j = 1 + self.index(n - 1);
386                        self.bag.swap(0, j);
387                    }
388                    self.cursor = 0;
389                }
390                let result = self.bag[self.cursor];
391                self.cursor += 1;
392                self.last = Some(result);
393                result
394            }
395        }
396    }
397}