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}