Skip to main content

audiobox/synth/
prepared.rs

1//! Typed preparation for absolute note frames; arrangement syntax belongs to callers.
2use super::{Instrument, MAX_CHANNELS, Pitch, ScoreLimits};
3use crate::{Error, Limits, Result};
4
5/// Maximum authored tempo segments in a prepared map.
6pub const MAX_TEMPO_CHANGES: usize = 1024;
7
8/// A tempo beginning at an absolute tick. BPM counts quarter notes per minute.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct TempoChange {
11    pub tick: u64,
12    /// Finite 1..=1000 BPM; rounded to 0.001 BPM during preparation.
13    pub bpm: f64,
14}
15
16#[derive(Clone, Copy, Debug)]
17struct Rational {
18    num: u128,
19    den: u128,
20}
21fn gcd(mut a: u128, mut b: u128) -> u128 {
22    while b != 0 {
23        (a, b) = (b, a % b);
24    }
25    a
26}
27fn timing_overflow() -> Error {
28    Error::limit("Exact prepared timing exceeds checked u128/u64 bounds")
29}
30impl Rational {
31    fn new(num: u128, den: u128) -> Self {
32        let g = gcd(num, den);
33        Self {
34            num: num / g,
35            den: den / g,
36        }
37    }
38    fn duration(ticks: u64, rate: u32, bpm: u32, tpq: u16) -> Self {
39        // The bounded factors (u64 ticks, <=768000 Hz) fit in u128.
40        Self::new(
41            ticks as u128 * rate as u128 * 60000,
42            bpm as u128 * tpq as u128,
43        )
44    }
45    fn add(self, other: Self) -> Result<Self> {
46        let g = gcd(self.den, other.den);
47        let a = other.den / g;
48        let b = self.den / g;
49        let num = self
50            .num
51            .checked_mul(a)
52            .and_then(|n| n.checked_add(other.num.checked_mul(b)?))
53            .ok_or_else(timing_overflow)?;
54        let den = self.den.checked_mul(a).ok_or_else(timing_overflow)?;
55        Ok(Self::new(num, den))
56    }
57    fn frame(self) -> Result<u64> {
58        crate::timing::rounded_ratio(self.num, self.den).ok_or_else(timing_overflow)
59    }
60}
61
62#[derive(Clone, Debug)]
63struct Segment {
64    tick: u64,
65    bpm: u32,
66    start: Rational,
67    frame: u64,
68    beat_numerator: u128,
69}
70
71/// An immutable, prepared tick-to-frame map with 0.001-BPM tempo precision.
72///
73/// Changes must be strictly tick-ordered, start at tick zero, and contain
74/// 1..=1024 segments. TPQ is 1..=960; sample rate follows default `Limits`.
75/// Prefix durations are exact reduced u128 rationals. Every absolute position
76/// is rounded once, nearest frame with halves up, using Music::Transport's rule.
77/// Invalid tempo/order/TPQ is `InvalidArgument`; sample-rate limits, segment
78/// count, rational intermediate overflow or frames beyond u64 are `LimitExceeded`.
79/// This deliberately differs from legacy Score's integer-tempo floor rounding.
80#[derive(Clone, Debug)]
81pub struct TempoMap {
82    rate: u32,
83    tpq: u16,
84    segments: Vec<Segment>,
85}
86impl TempoMap {
87    pub fn new(sample_rate: u32, ticks_per_quarter: u16, changes: &[TempoChange]) -> Result<Self> {
88        Limits::default().check(0, 1, sample_rate)?;
89        if changes.len() > MAX_TEMPO_CHANGES {
90            return Err(Error::limit("Too many prepared tempo changes"));
91        }
92        if changes.is_empty()
93            || changes[0].tick != 0
94            || !(1..=960).contains(&ticks_per_quarter)
95            || changes.windows(2).any(|p| p[0].tick >= p[1].tick)
96        {
97            return Err(Error::invalid(
98                "Tempo changes must start at zero and increase; TPQ must be 1..=960",
99            ));
100        }
101        let mut segments: Vec<Segment> = Vec::new();
102        segments
103            .try_reserve_exact(changes.len())
104            .map_err(|_| Error::limit("Cannot reserve tempo map"))?;
105        for change in changes {
106            let bpm = crate::timing::milli_bpm(change.bpm)?;
107            let (start, beat_numerator) = if let Some(previous) = segments.last() {
108                let start = previous.start.add(Rational::duration(
109                    change.tick - previous.tick,
110                    sample_rate,
111                    previous.bpm,
112                    ticks_per_quarter,
113                ))?;
114                let frame = start.frame()?;
115                (
116                    start,
117                    previous.beat_numerator
118                        + (frame - previous.frame) as u128 * previous.bpm as u128,
119                )
120            } else {
121                (Rational::new(0, 1), 0)
122            };
123            segments.push(Segment {
124                tick: change.tick,
125                bpm,
126                start,
127                frame: start.frame()?,
128                beat_numerator,
129            });
130        }
131        Ok(Self {
132            rate: sample_rate,
133            tpq: ticks_per_quarter,
134            segments,
135        })
136    }
137    pub fn sample_rate(&self) -> u32 {
138        self.rate
139    }
140    pub fn ticks_per_quarter(&self) -> u16 {
141        self.tpq
142    }
143    /// Map an absolute tick once, including note-offs and the exclusive body end.
144    /// Map both boundaries of a duration and subtract; never add rounded lengths.
145    /// Exact rational intermediate overflow or frames beyond u64 are rejected.
146    pub fn frame_at(&self, tick: u64) -> Result<u64> {
147        let s = &self.segments[self.segments.partition_point(|s| s.tick <= tick) - 1];
148        s.start
149            .add(Rational::duration(
150                tick - s.tick,
151                self.rate,
152                s.bpm,
153                self.tpq,
154            ))?
155            .frame()
156    }
157    /// The normalized tempo in effect at an absolute tick, in BPM.
158    pub fn bpm_at(&self, tick: u64) -> f64 {
159        self.segments[self.segments.partition_point(|s| s.tick <= tick) - 1].bpm as f64 / 1000.0
160    }
161    pub(super) fn last_change_frame(&self) -> u64 {
162        self.segments.last().unwrap().frame
163    }
164    pub(super) fn clock(&self, frame: u64) -> BeatClock {
165        // Last authored change wins when several ticks round to the same frame.
166        let s = &self.segments[self.segments.partition_point(|s| s.frame <= frame) - 1];
167        BeatClock {
168            numerator: s.beat_numerator + (frame - s.frame) as u128 * s.bpm as u128,
169            denominator: self.rate as u128 * 60000,
170        }
171    }
172}
173
174/// Exact integrated quarter-note phase on the scheduled output-frame clock.
175#[derive(Clone, Copy, Debug)]
176pub(super) struct BeatClock {
177    pub numerator: u128,
178    pub denominator: u128,
179}
180
181/// One note's absolute onset and exclusive gate-off, in source output frames.
182/// A tie across a section/tempo change is one note with a later `off`, not a
183/// retrigger. Sorted equal-onset notes follow input order within each track.
184#[derive(Clone, Copy, Debug)]
185pub struct FrameNote {
186    pub on: u64,
187    pub off: u64,
188    pub pitch: Pitch,
189    pub instrument: usize,
190    /// 0..=15 channel volume, independent of the instrument's own volume.
191    pub volume: u8,
192    /// 0..=127 velocity, linear in amplitude.
193    pub velocity: u8,
194}
195impl FrameNote {
196    pub fn new(on: u64, off: u64, pitch: Pitch, instrument: usize) -> Self {
197        Self {
198            on,
199            off,
200            pitch,
201            instrument,
202            volume: 15,
203            velocity: 127,
204        }
205    }
206}
207
208/// Typed input for [`super::Synth::from_frames`] and its stereo constructor.
209/// Up to eight tracks and 256 existing instruments per source. Use multiple
210/// aligned sources for larger arrangements. Authored repeats are expanded to
211/// absolute events; implicit RenderOptions loops are rejected on this path.
212#[derive(Clone, Debug, Default)]
213pub struct FrameScore {
214    pub instruments: Vec<Instrument>,
215    pub tracks: Vec<Vec<FrameNote>>,
216    /// Exclusive musical body end, including authored trailing rests. Gates must
217    /// end by this frame. Active releases/drum tails drain after it.
218    pub end_frame: u64,
219}
220impl FrameScore {
221    pub(super) fn validate(&self, map: &TempoMap) -> Result<()> {
222        let limits = ScoreLimits::default();
223        if self.tracks.len() > MAX_CHANNELS || self.instruments.len() > 256 {
224            return Err(Error::limit(
225                "Prepared synth needs at most eight tracks and 256 instruments",
226            ));
227        }
228        if self.end_frame as u128 > map.rate as u128 * 1200 {
229            return Err(Error::limit("Prepared synth body exceeds 1200 seconds"));
230        }
231        if map.last_change_frame() > self.end_frame {
232            return Err(Error::invalid(
233                "Tempo changes extend beyond the prepared body",
234            ));
235        }
236        let mut notes = 0usize;
237        for track in &self.tracks {
238            notes = notes
239                .checked_add(track.len())
240                .ok_or_else(|| Error::limit("Prepared note count overflow"))?;
241            if notes > limits.max_notes {
242                return Err(Error::limit("Too many prepared notes"));
243            }
244            if track.windows(2).any(|p| p[0].on > p[1].on) {
245                return Err(Error::invalid("Prepared notes must be onset-ordered"));
246            }
247            for note in track {
248                if note.on >= note.off
249                    || note.off > self.end_frame
250                    || note.instrument >= self.instruments.len()
251                    || note.volume > 15
252                    || note.velocity > 127
253                {
254                    return Err(Error::invalid(
255                        "Invalid prepared note bounds, instrument, volume or velocity",
256                    ));
257                }
258            }
259        }
260        for instrument in &self.instruments {
261            instrument.validate()?;
262        }
263        Ok(())
264    }
265}