Skip to main content

audiobox/mixer/
music.rs

1//! Music that follows a game: transitions, cues and intensity layers. Requires `music`.
2//!
3//! A [`Music`] controller uses a fixed [`Transport`] on the mixer's output-frame
4//! clock. Each operation is prepared on the control thread and sent as one command.
5//! The audio thread resolves its timing when it receives the command, then applies
6//! the whole operation at that frame. A full command queue changes no playing voice.
7//! With no room for all new layers, a transition is rejected without stopping the
8//! previous track. Poll [`MixerEvent::MusicApplied`] / [`MixerEvent::MusicRejected`]
9//! to distinguish application from submission; application starts a fade, not ends it.
10//!
11//! Every layer starts together, including silent ones. Intensity only changes gains,
12//! so a layer becomes audible at its current source position. Multilayer tracks use
13//! the same source sample rate and, where known, length. The caller supplies stems
14//! with matching musical timing and compatible loops; unknown-length streams and
15//! synthesizers must also maintain that timing. A cue is independent of transitions
16//! and is played once (supply a finite source).
17//!
18//! Music uses ordinary mixer voices at priority 255, without stealing on admission.
19//! Provision `max_voices` for the entire crossfade overlap and any cues/effects.
20//! Other priority-255 sounds can still steal music under the mixer's normal rules.
21//! With this feature the mixer reserves return storage for up to 16 layer voices per
22//! queued command, so its queue setting also controls this preparation-time memory.
23//! Poll events regularly to reclaim request containers and finished voices off audio.
24//! As with ordinary mixer voices, exhausting both return buffers without polling
25//! can free retired data on audio; the reservation covers a full render's worst case,
26//! not an unlimited number of renders without a consumer.
27//!
28//! The transport follows output frames, including while a bus is paused; it does not
29//! infer tempo from audio or follow per-voice rate/pitch changes. A shared bus pause
30//! freezes all its stems together but does not stop the beat grid. Limiter lookahead
31//! delays the audible result by [`MixerHandle::latency`] frames. Submit ahead of the
32//! desired boundary: a command delayed by a full pending queue is quantized when it
33//! is finally received. Operations at the same boundary follow submission order.
34//!
35//! ```
36//! use audiobox::music::{Music, TimeSignature, Timing, Track, Transport};
37//! use audiobox::mixer::{BusId, Fade, Mixer, MixerConfig};
38//! use audiobox::Audio;
39//! use std::sync::Arc;
40//! let (mut mixer, mut handle) = Mixer::new(MixerConfig::default())?;
41//! let transport = Transport::new(48_000, 120.0, TimeSignature::default(), 0)?;
42//! let music = Music::new(&mut handle, transport, BusId::MASTER)?;
43//! let source = Arc::new(Audio::new(vec![vec![0.1; 48_000]], 48_000)?);
44//! music.transition(&mut handle, Track::new(source), 0.0, Timing::NOW, Fade::NONE)?;
45//! music.cue(&mut handle, Arc::new(Audio::new(vec![vec![0.1; 2400]], 48_000)?),
46//!     0.5, Timing::BEAT)?;
47//! # mixer.render(&mut [0.0; 256]);
48//! # Ok::<(), audiobox::Error>(())
49//! ```
50use super::{
51    Action, BusId, Command, Fade, Mixer, MixerEvent, MixerHandle, Play, Returned, Sound, Voice,
52};
53use crate::{Error, ErrorCode, FadeCurve, Limits, Result};
54use std::sync::Arc;
55use std::sync::atomic::AtomicU64;
56
57/// Most stems in one track. Silent layers count toward the voice limit too.
58pub const MAX_LAYERS: usize = 16;
59
60/// A notated bar: `numerator` beats of `1 / denominator` whole notes.
61/// The transport accepts 1..=32 beats and denominators 1, 2, 4, 8, 16 or 32.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct TimeSignature {
64    pub numerator: u8,
65    pub denominator: u8,
66}
67
68impl Default for TimeSignature {
69    fn default() -> Self {
70        Self {
71            numerator: 4,
72            denominator: 4,
73        }
74    }
75}
76
77/// A fixed-tempo grid in output frames. BPM counts quarter notes per minute.
78/// Tempo is rounded to 0.001 BPM on construction; every boundary is computed
79/// directly with integer arithmetic, rounded to the nearest frame (halves up).
80#[derive(Clone, Copy, Debug)]
81pub struct Transport {
82    rate: u32,
83    milli_bpm: u32,
84    signature: TimeSignature,
85    origin: u64,
86}
87
88impl Transport {
89    /// `sample_rate` uses the crate's supported rate limits, BPM is finite 1..=1000,
90    /// and `origin` is the first bar's first output frame.
91    pub fn new(sample_rate: u32, bpm: f64, signature: TimeSignature, origin: u64) -> Result<Self> {
92        Limits::default().check(0, 1, sample_rate)?;
93        let milli_bpm = crate::timing::milli_bpm(bpm)?;
94        if !(1..=32).contains(&signature.numerator)
95            || !signature.denominator.is_power_of_two()
96            || signature.denominator > 32
97        {
98            return Err(Error::invalid(
99                "Music needs 1..=32 beats and a power-of-two beat unit up to 32",
100            ));
101        }
102        Ok(Self {
103            rate: sample_rate,
104            milli_bpm,
105            signature,
106            origin,
107        })
108    }
109
110    pub fn sample_rate(&self) -> u32 {
111        self.rate
112    }
113    pub fn bpm(&self) -> f64 {
114        self.milli_bpm as f64 / 1000.0
115    }
116    pub fn signature(&self) -> TimeSignature {
117        self.signature
118    }
119    pub fn origin(&self) -> u64 {
120        self.origin
121    }
122
123    /// Immediate returns `frame`. Beat/Bar returns the first grid boundary strictly
124    /// after `frame`, or `origin` when it is still in the future. An exact boundary
125    /// therefore advances one more beat/bar. Overflow returns `LimitExceeded`.
126    pub fn boundary_after(&self, frame: u64, quantize: Quantize) -> Result<u64> {
127        self.boundary(frame, quantize)
128            .ok_or_else(|| Error::limit("Music boundary exceeds the output clock"))
129    }
130
131    // The renderer uses this fallible arithmetic without constructing an Error/String.
132    fn boundary(&self, frame: u64, quantize: Quantize) -> Option<u64> {
133        if quantize == Quantize::Immediate {
134            return Some(frame);
135        }
136        if frame < self.origin {
137            return Some(self.origin);
138        }
139        let beats = if quantize == Quantize::Bar {
140            self.signature.numerator as u128
141        } else {
142            1
143        };
144        let numerator = self.rate as u128 * 240_000 * beats;
145        let denominator = self.milli_bpm as u128 * self.signature.denominator as u128;
146        let elapsed = (frame - self.origin) as u128;
147        let mut index = elapsed * denominator / numerator + 1;
148        loop {
149            let offset = crate::timing::rounded_ratio(index * numerator, denominator)?;
150            let boundary = self.origin.checked_add(offset)?;
151            if boundary > frame {
152                return Some(boundary);
153            }
154            index += 1;
155        }
156    }
157}
158
159/// Where an operation lands on a transport. Beat means the signature's beat unit;
160/// in 6/8 it is an eighth note, while BPM always counts quarter notes.
161#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
162pub enum Quantize {
163    #[default]
164    Immediate,
165    Beat,
166    Bar,
167}
168
169/// Resolve at audio-thread receipt, using `max(received_frame, not_before)`.
170/// Beat/Bar are strictly after that reference, even if it is already a boundary.
171#[derive(Clone, Copy, Debug, Default)]
172pub struct Timing {
173    pub not_before: u64,
174    pub quantize: Quantize,
175}
176
177impl Timing {
178    pub const NOW: Self = Self {
179        not_before: 0,
180        quantize: Quantize::Immediate,
181    };
182    pub const BEAT: Self = Self {
183        not_before: 0,
184        quantize: Quantize::Beat,
185    };
186    pub const BAR: Self = Self {
187        not_before: 0,
188        quantize: Quantize::Bar,
189    };
190}
191
192/// One stem of a track. It keeps playing even when its intensity gain is zero.
193pub struct Layer {
194    pub sound: Sound,
195    /// Full-level linear gain, finite and nonnegative. Default 1.
196    pub gain: f32,
197    /// Intensity at which gain starts rising and reaches full level, 0..=1 in order.
198    /// Equal endpoints give a step (on at/above the threshold). Default [0, 0], always on.
199    pub intensity: [f32; 2],
200}
201
202impl Layer {
203    pub fn new(sound: impl Into<Sound>) -> Self {
204        Self {
205            sound: sound.into(),
206            gain: 1.0,
207            intensity: [0.0, 0.0],
208        }
209    }
210}
211
212/// 1..=16 synchronized stems, with one loop policy. All source rates must match;
213/// all known frame lengths must match. Unknown lengths/timing remain the caller's
214/// responsibility. Single-stem tracks may use any supported source rate.
215pub struct Track {
216    pub layers: Vec<Layer>,
217    pub looping: super::Loop,
218}
219
220impl Track {
221    pub fn new(sound: impl Into<Sound>) -> Self {
222        Self {
223            layers: vec![Layer::new(sound)],
224            looping: super::Loop::Off,
225        }
226    }
227}
228
229/// Identifies a submitted operation in mixer events, not a voice or a controller.
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231pub struct MusicId(u64);
232
233/// A controller bound to one mixer and one fixed transport. It does not own sounds
234/// or mutate voices on the control thread; future operations target whichever track
235/// is active when they actually execute. Dropping it does not stop its track.
236pub struct Music {
237    owner: u64,
238    clock: Arc<AtomicU64>,
239    transport: Transport,
240    bus: BusId,
241}
242
243impl Music {
244    pub fn new(handle: &mut MixerHandle, transport: Transport, bus: BusId) -> Result<Self> {
245        if transport.rate != handle.rate || bus.0 > handle.buses.len() {
246            return Err(Error::invalid(
247                "Music transport rate and bus must match its mixer",
248            ));
249        }
250        let owner = handle.next_voice;
251        handle.next_voice += 1;
252        Ok(Self {
253            owner,
254            clock: handle.clock.clone(),
255            transport,
256            bus,
257        })
258    }
259
260    pub fn transport(&self) -> Transport {
261        self.transport
262    }
263
264    /// Replace this controller's track. All new stems start at source frame zero,
265    /// fading in while the previous track fades out over the same duration/curve.
266    /// Preparation/queue errors change no voice; audio capacity failures are events.
267    pub fn transition(
268        &self,
269        handle: &mut MixerHandle,
270        track: Track,
271        intensity: f32,
272        timing: Timing,
273        fade: Fade,
274    ) -> Result<MusicId> {
275        self.check(handle)?;
276        validate_intensity(intensity)?;
277        if track.layers.is_empty() || track.layers.len() > MAX_LAYERS {
278            return Err(Error::invalid("A music track needs 1..=16 layers"));
279        }
280        let (len, curve) = handle.frames(fade)?;
281        let mut rate = None;
282        let mut length = None;
283        let mut voices = Vec::with_capacity(track.layers.len());
284        for layer in track.layers {
285            let [low, high] = layer.intensity;
286            validate_intensity(low)?;
287            validate_intensity(high)?;
288            if low > high || !layer.gain.is_finite() || layer.gain < 0.0 {
289                return Err(Error::invalid(
290                    "A layer needs ordered intensity bounds and finite nonnegative gain",
291                ));
292            }
293            let (source_rate, frames) = shape(&layer.sound);
294            if rate.is_some_and(|rate| rate != source_rate)
295                || frames.zip(length).is_some_and(|(a, b)| a != b)
296            {
297                return Err(Error::invalid(
298                    "Music layers must share a source rate and known length",
299                ));
300            }
301            rate = Some(source_rate);
302            if frames.is_some() {
303                length = frames;
304            }
305            let state = LayerState {
306                owner: self.owner,
307                active: true,
308                gain: layer.gain as f64,
309                intensity: layer.intensity,
310            };
311            let mut voice = handle.prepare_voice(
312                layer.sound,
313                &Play {
314                    bus: self.bus,
315                    priority: 255,
316                    gain: state.gain_at(intensity) as f32,
317                    fade_in: fade,
318                    looping: track.looping,
319                    ..Play::default()
320                },
321            )?;
322            voice.music = Some(state);
323            voices.push(Some(voice));
324        }
325        self.submit(
326            handle,
327            timing,
328            len,
329            curve,
330            Operation::Transition,
331            intensity,
332            voices,
333        )
334    }
335
336    /// Fade the active track's layer gains without restarting or pausing any source.
337    pub fn set_intensity(
338        &self,
339        handle: &mut MixerHandle,
340        intensity: f32,
341        timing: Timing,
342        fade: Fade,
343    ) -> Result<MusicId> {
344        self.check(handle)?;
345        validate_intensity(intensity)?;
346        let (len, curve) = handle.frames(fade)?;
347        self.submit(
348            handle,
349            timing,
350            len,
351            curve,
352            Operation::Intensity,
353            intensity,
354            Vec::new(),
355        )
356    }
357
358    /// Layer a one-shot cue over music. Track transitions/stops do not stop cues.
359    pub fn cue(
360        &self,
361        handle: &mut MixerHandle,
362        sound: impl Into<Sound>,
363        gain: f32,
364        timing: Timing,
365    ) -> Result<MusicId> {
366        self.check(handle)?;
367        let voice = handle.prepare_voice(
368            sound.into(),
369            &Play {
370                bus: self.bus,
371                gain,
372                priority: 255,
373                ..Play::default()
374            },
375        )?;
376        self.submit(
377            handle,
378            timing,
379            0,
380            FadeCurve::Linear,
381            Operation::Cue,
382            0.0,
383            vec![Some(voice)],
384        )
385    }
386
387    /// Stop all this controller's track voices, including earlier crossfade tails.
388    /// Independently layered cues keep playing.
389    pub fn stop(&self, handle: &mut MixerHandle, timing: Timing, fade: Fade) -> Result<MusicId> {
390        self.check(handle)?;
391        let (len, curve) = handle.frames(fade)?;
392        self.submit(handle, timing, len, curve, Operation::Stop, 0.0, Vec::new())
393    }
394
395    fn check(&self, handle: &MixerHandle) -> Result<()> {
396        if Arc::ptr_eq(&self.clock, &handle.clock) {
397            Ok(())
398        } else {
399            Err(Error::invalid("Music controller belongs to another mixer"))
400        }
401    }
402
403    #[allow(clippy::too_many_arguments)]
404    fn submit(
405        &self,
406        handle: &mut MixerHandle,
407        timing: Timing,
408        len: u64,
409        curve: FadeCurve,
410        operation: Operation,
411        intensity: f32,
412        voices: Vec<Option<Box<Voice>>>,
413    ) -> Result<MusicId> {
414        // Validate the lower bound now; actual receipt can only move it later.
415        self.transport
416            .boundary_after(timing.not_before, timing.quantize)?
417            .checked_add(len)
418            .ok_or_else(|| Error::limit("Music fade exceeds the output clock"))?;
419        let id = MusicId(handle.next_voice);
420        handle.next_voice += 1;
421        let request = Box::new(Request {
422            id,
423            owner: self.owner,
424            transport: self.transport,
425            timing,
426            len,
427            curve,
428            operation,
429            intensity,
430            voices,
431        });
432        handle.send(None, Action::Music(request))?;
433        Ok(id)
434    }
435}
436
437fn validate_intensity(intensity: f32) -> Result<()> {
438    if (0.0..=1.0).contains(&intensity) {
439        Ok(())
440    } else {
441        Err(Error::invalid("Intensity must be finite and 0..=1"))
442    }
443}
444
445fn shape(sound: &Sound) -> (u32, Option<u64>) {
446    match sound {
447        Sound::Memory(audio) => (audio.sample_rate(), Some(audio.frames() as u64)),
448        Sound::Stream(stream) => (stream.sample_rate(), stream.frames()),
449    }
450}
451
452#[derive(Clone, Copy)]
453pub(super) struct LayerState {
454    owner: u64,
455    active: bool,
456    gain: f64,
457    intensity: [f32; 2],
458}
459
460impl LayerState {
461    fn gain_at(self, value: f32) -> f64 {
462        let [low, high] = self.intensity;
463        let amount = if value >= high {
464            1.0
465        } else if value <= low {
466            0.0
467        } else {
468            (value as f64 - low as f64) / (high as f64 - low as f64)
469        };
470        self.gain * amount
471    }
472}
473
474#[derive(Clone, Copy, PartialEq)]
475enum Operation {
476    Transition,
477    Cue,
478    Intensity,
479    Stop,
480}
481
482pub(super) struct Request {
483    id: MusicId,
484    owner: u64,
485    transport: Transport,
486    timing: Timing,
487    len: u64,
488    curve: FadeCurve,
489    operation: Operation,
490    intensity: f32,
491    voices: Vec<Option<Box<Voice>>>,
492}
493
494impl Mixer {
495    pub(super) fn quantize_music(&mut self, mut command: Command) -> Option<Command> {
496        if let Action::Music(request) = &command.action {
497            let reference = self.frame.max(request.timing.not_before);
498            let resolved = request
499                .transport
500                .boundary(reference, request.timing.quantize);
501            match resolved.and_then(|frame| frame.checked_add(request.len).map(|_| frame)) {
502                Some(frame) => command.frame = Some(frame),
503                None => {
504                    if let Action::Music(request) = command.action {
505                        let id = request.id;
506                        self.return_music(
507                            request,
508                            MixerEvent::MusicRejected(id, ErrorCode::LimitExceeded),
509                        );
510                    }
511                    return None;
512                }
513            }
514        }
515        Some(command)
516    }
517
518    pub(super) fn apply_music(&mut self, frame: u64, mut request: Box<Request>) {
519        self.retire_done();
520        let replacing = matches!(request.operation, Operation::Transition | Operation::Stop);
521        let targets = |layer: LayerState| {
522            layer.owner == request.owner && (layer.active || request.operation == Operation::Stop)
523        };
524        let incoming = request.voices.len();
525        let occupied = self.voices.iter().flatten().count();
526        let removed = if replacing {
527            self.voices
528                .iter()
529                .flatten()
530                .filter(|v| {
531                    v.music.is_some_and(targets)
532                        && (request.len == 0
533                            || v.paused()
534                            || self.buses[v.bus].paused
535                            || self.buses[0].paused)
536                })
537                .count()
538        } else {
539            0
540        };
541        if incoming > 0 && occupied - removed + incoming > self.voice_limit {
542            let id = request.id;
543            return self.return_music(
544                request,
545                MixerEvent::MusicRejected(id, ErrorCode::LimitExceeded),
546            );
547        }
548        if replacing {
549            for slot in 0..self.voices.len() {
550                if self.voices[slot]
551                    .as_ref()
552                    .is_some_and(|v| v.music.is_some_and(targets))
553                {
554                    self.voices[slot]
555                        .as_mut()
556                        .unwrap()
557                        .music
558                        .as_mut()
559                        .unwrap()
560                        .active = false;
561                    self.stop_slot(slot, frame, request.len, request.curve);
562                }
563            }
564            self.retire_done();
565        }
566        if request.operation == Operation::Intensity {
567            for voice in self.voices.iter_mut().flatten() {
568                if let Some(layer) = voice.music.filter(|m| m.owner == request.owner && m.active) {
569                    voice.gain.retarget(
570                        voice.time(frame),
571                        layer.gain_at(request.intensity),
572                        request.len,
573                        request.curve,
574                    );
575                }
576            }
577        }
578        for voice in &mut request.voices {
579            if let Some(voice) = voice.take() {
580                self.start(frame, voice);
581            }
582        }
583        let id = request.id;
584        self.return_music(request, MixerEvent::MusicApplied(id, frame));
585    }
586
587    fn return_music(&mut self, request: Box<Request>, event: MixerEvent) {
588        if self.retired.len() < self.retired.capacity() {
589            self.retired.push(Returned {
590                event: Some(event),
591                voice: None,
592                music: Some(request),
593            });
594        }
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn a_boundary_that_overflows_only_after_receipt_rejects_the_operation() {
604        let (mut mixer, mut handle) = Mixer::new(super::super::MixerConfig::default()).unwrap();
605        let transport = Transport::new(48_000, 120.0, TimeSignature::default(), 0).unwrap();
606        let music = Music::new(&mut handle, transport, BusId::MASTER).unwrap();
607        let request = music.stop(&mut handle, Timing::BEAT, Fade::NONE).unwrap();
608        mixer.frame = u64::MAX;
609        // Zero frames avoids advancing the exhausted clock. The arithmetic path
610        // returns only Option/ErrorCode, never constructs an allocating Error.
611        mixer.render(&mut []);
612        assert_eq!(
613            handle.poll_event(),
614            Some(MixerEvent::MusicRejected(request, ErrorCode::LimitExceeded))
615        );
616        assert!(mixer.pending.is_empty());
617    }
618}