Skip to main content

audiobox/synth/
render.rs

1use super::chip::{ChipPlan, ChipVoice};
2use super::controls::{
3    self, ControlEvent, ControlId, ControlParameter, ControlReport, ControlState, ControlTarget,
4    MAX_SYNTH_CONTROLS, Resolved, Values,
5};
6use super::drum::{DrumPlan, DrumVoice};
7use super::prepared::{BeatClock, FrameScore, TempoMap};
8use super::score::{Instrument, Score};
9use super::synthwave::{WidePlan, WideVoice};
10use super::wave::{Aliasing, Duty, Oscillator, Waveform};
11use crate::math::cents_to_hz;
12use crate::{Audio, Error, Limits, Result};
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct RenderOptions {
16    pub aliasing: Aliasing,
17    /// Times the loop section repeats after the first pass through the song, or
18    /// `None` to loop forever (incremental rendering only). The default is `Some(0)`.
19    pub loops: Option<u32>,
20    /// Linear gain applied to the sum of the channels. The default, 0.25, lets
21    /// four channels at full volume reach 1.0.
22    pub gain: f32,
23}
24impl Default for RenderOptions {
25    fn default() -> Self {
26        Self {
27            aliasing: Aliasing::BandLimited,
28            loops: Some(0),
29            gain: 0.25,
30        }
31    }
32}
33
34impl Score {
35    /// Renders the song once through, followed by the release of its last notes.
36    pub fn render(&self, sample_rate: u32) -> Result<Audio> {
37        self.render_with(sample_rate, &RenderOptions::default())
38    }
39    /// Renders the song into mono audio. Equal to concatenating every chunk of a [`Synth`].
40    pub fn render_with(&self, sample_rate: u32, options: &RenderOptions) -> Result<Audio> {
41        self.render_channels(sample_rate, options, false)
42    }
43    /// Renders stereo, preserving synthwave unison spread and pan. Legacy channels
44    /// are duplicated to left/right. No automatic limiting is applied.
45    pub fn render_stereo(&self, sample_rate: u32) -> Result<Audio> {
46        self.render_stereo_with(sample_rate, &RenderOptions::default())
47    }
48    pub fn render_stereo_with(&self, sample_rate: u32, options: &RenderOptions) -> Result<Audio> {
49        self.render_channels(sample_rate, options, true)
50    }
51    fn render_channels(
52        &self,
53        sample_rate: u32,
54        options: &RenderOptions,
55        stereo: bool,
56    ) -> Result<Audio> {
57        let mut synth = if stereo {
58            Synth::new_stereo(self, sample_rate, options)?
59        } else {
60            Synth::new(self, sample_rate, options)?
61        };
62        let frames = synth
63            .max_frames()
64            .ok_or_else(|| Error::invalid("Cannot render an endless loop offline"))?;
65        let frames = usize::try_from(frames).map_err(|_| Error::limit("Song is too long"))?;
66        let channels = synth.channels();
67        Limits::default().check(frames, channels, sample_rate)?;
68        let mut samples = vec![0.0; frames * channels];
69        let written = synth.fill(&mut samples);
70        samples.truncate(written * channels);
71        if channels == 1 {
72            Audio::new(vec![samples], sample_rate)
73        } else {
74            Audio::new(
75                (0..channels)
76                    .map(|c| samples.iter().skip(c).step_by(channels).copied().collect())
77                    .collect(),
78                sample_rate,
79            )
80        }
81    }
82}
83
84#[derive(Clone, Debug)]
85struct Plan {
86    wide: Option<WidePlan>,
87    /// Size of the track's voice pool for this instrument's notes: synthwave voices,
88    /// or the opt-in voices of a wavetable source. Zero uses the single legacy voice.
89    voices: usize,
90    chip: Option<ChipPlan>,
91    drum: Option<DrumPlan>,
92    waveform: Waveform,
93    attack: u64,
94    decay: u64,
95    sustain: f64,
96    release: u64,
97    volume: f64,
98    slide: f64,
99    vibrato: Option<(f64, u64, u64)>,
100    arpeggio: Option<(Vec<i8>, u64)>,
101    duty_sweep: Option<(Vec<Duty>, u64)>,
102}
103impl Plan {
104    fn control_base(&self, parameter: ControlParameter) -> Option<f64> {
105        use ControlParameter::*;
106        match parameter {
107            Expression => Some(1.0),
108            Pan => Some(
109                self.wide
110                    .as_ref()
111                    .map_or(0.0, |p| p.settings.pan as f64 / 100.0),
112            ),
113            _ => {
114                let s = self.wide.as_ref()?.settings;
115                Some(match parameter {
116                    CutoffHz => s.filter?.cutoff_hz as f64,
117                    ResonanceQ => s.filter?.resonance as f64 / 100.0,
118                    PitchCents => 0.0,
119                    PitchLfoDepth => s.pitch_lfo?.depth as f64,
120                    CutoffLfoDepth => s.cutoff_lfo?.depth as f64,
121                    AmplitudeLfoDepth => s.amplitude_lfo?.depth as f64,
122                    PanLfoDepth => s.pan_lfo?.depth as f64,
123                    _ => unreachable!(),
124                })
125            }
126        }
127    }
128    fn new(instrument: &Instrument, rate: u32, tempo: u16, ticks: u16) -> Self {
129        let samples = |ms: u32| (ms as u64 * rate as u64 + 500) / 1000;
130        let step = |ms: u32| samples(ms).max(1);
131        let e = instrument.envelope;
132        Self {
133            // A wavetable source with synthwave `voices` keeps its chip rendering and
134            // only borrows the voice pool; validation admits no other combination.
135            wide: instrument
136                .synthwave
137                .filter(|_| instrument.chip.is_none())
138                .map(|s| WidePlan::new(s, rate, tempo)),
139            voices: instrument.synthwave.map_or(0, |s| s.voices as usize),
140            chip: instrument
141                .chip
142                .as_ref()
143                .map(|chip| ChipPlan::new(chip, rate, tempo, ticks)),
144            drum: instrument.drum.map(|d| DrumPlan::new(d, rate)),
145            waveform: instrument.waveform,
146            attack: samples(e.attack_ms),
147            decay: samples(e.decay_ms),
148            sustain: e.sustain as f64 / 100.0,
149            release: samples(e.release_ms),
150            volume: instrument.volume as f64 / 15.0,
151            slide: instrument.slide as f64 / rate as f64,
152            vibrato: instrument
153                .vibrato
154                .map(|v| (v.depth_cents as f64, step(v.period_ms), samples(v.delay_ms))),
155            arpeggio: instrument
156                .arpeggio
157                .as_ref()
158                .map(|a| (a.offsets.clone(), step(a.step_ms))),
159            duty_sweep: instrument
160                .duty_sweep
161                .as_ref()
162                .map(|d| (d.duties.clone(), step(d.step_ms))),
163        }
164    }
165    fn modulated(&self) -> bool {
166        self.slide != 0.0 || self.vibrato.is_some() || self.arpeggio.is_some()
167    }
168    fn cents(&self, base: f64, elapsed: u64) -> f64 {
169        let mut cents = base + self.slide * elapsed as f64;
170        if let Some((depth, period, delay)) = self.vibrato {
171            if elapsed >= delay {
172                let phase = ((elapsed - delay) % period) as f64 / period as f64;
173                cents += depth * crate::math::sin_turns(phase);
174            }
175        }
176        if let Some((offsets, step)) = &self.arpeggio {
177            cents += offsets[(elapsed / step) as usize % offsets.len()] as f64 * 100.0;
178        }
179        cents
180    }
181    fn waveform(&self, elapsed: u64) -> Waveform {
182        match (&self.duty_sweep, self.waveform) {
183            (Some((duties, step)), Waveform::Pulse(_)) if elapsed >= *step => {
184                let index = (elapsed / step - 1).min(duties.len() as u64 - 1) as usize;
185                Waveform::Pulse(duties[index])
186            }
187            (_, waveform) => waveform,
188        }
189    }
190}
191
192/// A note scheduled in samples.
193#[derive(Clone, Copy, Debug)]
194struct Event {
195    note: usize,
196    on: u64,
197    off: u64,
198    cents: f64,
199    instrument: usize,
200    amplitude: f64,
201}
202
203#[derive(Clone, Copy, Debug, PartialEq)]
204enum Stage {
205    Idle,
206    Attack,
207    Decay,
208    Sustain,
209    Release,
210}
211
212#[derive(Clone, Debug)]
213struct Voice {
214    wide: WideVoice,
215    started: u64,
216    chip: ChipVoice,
217    drum: DrumVoice,
218    drumming: bool,
219    oscillator: Oscillator,
220    event: Option<Event>,
221    held: bool,
222    stage: Stage,
223    position: u64,
224    from: f64,
225    level: f64,
226    elapsed: u64,
227    frequency: f64,
228    beat_start: Option<u128>,
229}
230impl Voice {
231    fn new(aliasing: Aliasing) -> Self {
232        Self {
233            wide: WideVoice::new(),
234            started: 0,
235            chip: ChipVoice::new(aliasing),
236            drum: DrumVoice::new(aliasing),
237            drumming: false,
238            oscillator: Oscillator::new(aliasing),
239            event: None,
240            held: false,
241            stage: Stage::Idle,
242            position: 0,
243            from: 0.0,
244            level: 0.0,
245            elapsed: 0,
246            frequency: 0.0,
247            beat_start: None,
248        }
249    }
250    fn start(&mut self, event: Event, plans: &[Plan]) {
251        let active = self.active();
252        let legato = plans[event.instrument]
253            .wide
254            .as_ref()
255            .is_some_and(|p| p.settings.legato)
256            && active
257            && self.held
258            && self
259                .event
260                .is_some_and(|old| old.instrument == event.instrument && old.off == event.on);
261        // A pooled voice that is free starts from silence, even if a release without
262        // a tail left its last level behind. A pooled wavetable note always attacks
263        // from silence, also on a stolen voice: stealing cuts the old note.
264        let plan = &plans[event.instrument];
265        if plan.voices > 0 && (!active || plan.wide.is_none()) {
266            self.level = 0.0;
267        }
268        if let Some(plan) = &plans[event.instrument].wide {
269            // Glide starts from the synthwave pitch, which only a synthwave note sets:
270            // a voice last used by a pooled wavetable note has none to glide from.
271            let gliding = active
272                && self
273                    .event
274                    .is_some_and(|old| plans[old.instrument].wide.is_some());
275            self.wide.trigger(plan, event.cents, gliding, legato);
276        }
277        self.event = Some(event);
278        self.held = true;
279        if legato {
280            return;
281        }
282        self.stage = Stage::Attack;
283        self.position = 0;
284        self.from = self.level;
285        self.elapsed = 0;
286        self.beat_start = None;
287        self.frequency = cents_to_hz(event.cents);
288        if plans[event.instrument].chip.is_some() {
289            self.chip.reset();
290        }
291        self.drumming = plans[event.instrument].drum.is_some();
292        if let Some(drum) = &plans[event.instrument].drum {
293            self.level = 0.0;
294            self.drum
295                .trigger(drum, crate::math::exp2((event.cents + 900.0) / 1200.0));
296        }
297    }
298    fn release(&mut self, plans: &[Plan]) {
299        self.held = false;
300        self.wide.release();
301        if self.drumming {
302            return;
303        }
304        if self.stage != Stage::Idle {
305            let release = self.event.map_or(0, |e| plans[e.instrument].release);
306            self.stage = if release == 0 {
307                Stage::Idle
308            } else {
309                Stage::Release
310            };
311            self.position = 0;
312            self.from = self.level;
313        }
314    }
315    fn envelope(&mut self, plan: &Plan) -> f64 {
316        let level = loop {
317            let p = self.position as f64;
318            match self.stage {
319                Stage::Attack if self.position < plan.attack => {
320                    break self.from + (1.0 - self.from) * p / plan.attack as f64;
321                }
322                Stage::Attack => (self.stage, self.position) = (Stage::Decay, 0),
323                Stage::Decay if self.position < plan.decay => {
324                    break 1.0 - (1.0 - plan.sustain) * p / plan.decay as f64;
325                }
326                Stage::Decay => self.stage = Stage::Sustain,
327                Stage::Sustain => break plan.sustain,
328                Stage::Release if self.position < plan.release => {
329                    break self.from * (1.0 - p / plan.release as f64);
330                }
331                Stage::Release | Stage::Idle => break 0.0,
332            }
333        };
334        self.position += 1;
335        if self.stage == Stage::Release && self.position >= plan.release {
336            self.stage = Stage::Idle;
337            self.level = 0.0;
338        } else {
339            self.level = level;
340        }
341        level
342    }
343    fn sample(
344        &mut self,
345        plans: &[Plan],
346        rate: u32,
347        musical: Option<BeatClock>,
348        controls: Option<Values>,
349    ) -> f64 {
350        let Some(event) = self.event else {
351            return 0.0;
352        };
353        let plan = &plans[event.instrument];
354        let expression = controls.and_then(|c| c.get(ControlParameter::Expression));
355        let express = |sample| expression.map_or(sample, |gain| sample * gain);
356        if let Some(drum) = &plan.drum {
357            return express(self.drum.sample(drum) * event.amplitude * plan.volume);
358        }
359        if self.stage == Stage::Idle {
360            return 0.0;
361        }
362        let level = self.envelope(plan);
363        let frequency = if plan.modulated() {
364            cents_to_hz(plan.cents(event.cents, self.elapsed))
365        } else {
366            self.frequency
367        };
368        let waveform = plan.waveform(self.elapsed);
369        if let Some(chip) = &plan.chip {
370            let relative = musical.map(|clock| BeatClock {
371                numerator: clock.numerator - *self.beat_start.get_or_insert(clock.numerator),
372                ..clock
373            });
374            let frequency = chip.frequency(frequency, self.elapsed, relative);
375            let (sample, finished) =
376                self.chip
377                    .sample(chip, waveform, frequency, self.elapsed, relative);
378            let gain = chip.gain(
379                level * event.amplitude * plan.volume,
380                self.elapsed,
381                relative,
382            );
383            self.elapsed += 1;
384            if finished {
385                self.stage = Stage::Idle;
386                self.level = 0.0;
387            }
388            return express(sample * gain);
389        }
390        self.elapsed += 1;
391        express(
392            self.oscillator.next(waveform, frequency, rate) * level * event.amplitude * plan.volume,
393        )
394    }
395    fn active(&self) -> bool {
396        if self.drumming {
397            self.drum.active()
398        } else {
399            self.stage != Stage::Idle
400        }
401    }
402    fn sample_stereo(
403        &mut self,
404        plans: &[Plan],
405        rate: u32,
406        clock: u64,
407        musical: Option<BeatClock>,
408        controls: Option<Values>,
409    ) -> [f64; 2] {
410        let Some(event) = self.event else {
411            return [0.0; 2];
412        };
413        let plan = &plans[event.instrument];
414        let Some(wide) = &plan.wide else {
415            let sample = self.sample(plans, rate, musical, controls);
416            return controls
417                .and_then(|c| c.get(ControlParameter::Pan))
418                .map_or([sample; 2], |pan| {
419                    [sample * (1.0 - pan.max(0.0)), sample * (1.0 + pan.min(0.0))]
420                });
421        };
422        if self.stage == Stage::Idle {
423            return [0.0; 2];
424        }
425        let gain = self.envelope(plan) * event.amplitude * plan.volume;
426        let offset = plan.cents(0.0, self.elapsed);
427        let mut result = self.wide.sample(
428            wide,
429            plan.waveform(self.elapsed),
430            offset,
431            clock,
432            musical,
433            controls,
434        );
435        if let Some(expression) = controls.and_then(|c| c.get(ControlParameter::Expression)) {
436            for value in &mut result {
437                *value *= expression;
438            }
439        }
440        self.elapsed += 1;
441        for value in &mut result {
442            *value *= gain;
443        }
444        result
445    }
446}
447
448#[derive(Clone, Debug)]
449struct Track {
450    events: Vec<Event>,
451    next: usize,
452    /// First event at or after the loop start, and an event sounding across it.
453    loop_next: usize,
454    loop_carry: Option<Event>,
455    voice: Voice,
456    poly: Vec<Voice>,
457    poly_carry: Vec<Event>,
458}
459impl Track {
460    fn trigger(&mut self, event: Event, plans: &[Plan], clock: u64) {
461        let limit = plans[event.instrument].voices;
462        if limit > 0 {
463            if self.voice.held {
464                self.voice.release(plans);
465            }
466            for voice in self.poly.iter_mut().skip(limit) {
467                voice.stage = Stage::Idle;
468                voice.held = false;
469                voice.level = 0.0;
470            }
471            let voices = &mut self.poly[..limit];
472            let slot = voices.iter().position(|v| !v.active()).unwrap_or_else(|| {
473                voices
474                    .iter()
475                    .enumerate()
476                    .min_by_key(|(i, v)| (v.held, v.started, *i))
477                    .unwrap()
478                    .0
479            });
480            voices[slot].start(event, plans);
481            voices[slot].started = clock;
482        } else {
483            for voice in &mut self.poly {
484                if voice.held {
485                    voice.release(plans);
486                }
487            }
488            self.voice.start(event, plans);
489        }
490    }
491    fn release(&mut self, plans: &[Plan]) {
492        if self.voice.held {
493            self.voice.release(plans);
494        }
495        for voice in &mut self.poly {
496            if voice.held {
497                voice.release(plans);
498            }
499        }
500    }
501    fn active(&self) -> bool {
502        self.voice.active() || self.poly.iter().any(Voice::active)
503    }
504}
505
506/// Incremental, allocation-free rendering of a [`Score`] or [`FrameScore`] into
507/// mono or stereo frames.
508///
509/// All memory is allocated by the constructors, including [`Synth::from_frames`].
510/// [`Synth::fill`] allocates nothing,
511/// takes no lock and does not block, so it can run on an audio thread. The samples
512/// do not depend on how the output is split into chunks, and are identical on every
513/// platform: pitch and vibrato use the crate's own polynomials, not `sin` or `exp`.
514///
515/// At the song's end it continues from the loop point while loops remain. A note
516/// that sounds across the loop point restarts there on every repeat. After the last
517/// pass it renders the release of the sounding notes, then finishes.
518#[derive(Clone, Debug)]
519pub struct Synth {
520    rate: u32,
521    channels: usize,
522    aliasing: Aliasing,
523    gain: f64,
524    plans: Vec<Plan>,
525    tracks: Vec<Track>,
526    song_end: u64,
527    loop_start: u64,
528    loops: Option<u32>,
529    loops_left: Option<u32>,
530    position: u64,
531    ending: bool,
532    finished: bool,
533    produced: u64,
534    max_release: u64,
535    tempo_map: Option<TempoMap>,
536    controls: Option<ControlState>,
537}
538impl Synth {
539    pub fn new(score: &Score, sample_rate: u32, options: &RenderOptions) -> Result<Self> {
540        Self::with_channels(score, sample_rate, options, 1)
541    }
542    /// Builds a stereo renderer, with interleaved output through `fill` and the
543    /// mixer's `Stream` adapter. Preparation may allocate; fill/reset do not.
544    pub fn new_stereo(score: &Score, sample_rate: u32, options: &RenderOptions) -> Result<Self> {
545        Self::with_channels(score, sample_rate, options, 2)
546    }
547    fn with_channels(
548        score: &Score,
549        sample_rate: u32,
550        options: &RenderOptions,
551        channels: usize,
552    ) -> Result<Self> {
553        Limits::default().check(0, 1, sample_rate)?;
554        if !options.gain.is_finite() {
555            return Err(Error::invalid("Gain must be finite"));
556        }
557        let to_samples = |tick: u64| {
558            (tick as u128 * sample_rate as u128 * 60
559                / (score.tempo() as u128 * score.ticks_per_quarter() as u128)) as u64
560        };
561        let plans: Vec<Plan> = score
562            .instruments()
563            .iter()
564            .map(|(_, i)| Plan::new(i, sample_rate, score.tempo(), score.ticks_per_quarter()))
565            .collect();
566        let song_end = to_samples(score.length_ticks());
567        let loop_start = to_samples(score.loop_tick().unwrap_or(0));
568        if options.loops.is_none() && loop_start >= song_end {
569            return Err(Error::invalid(
570                "Cannot loop forever over an empty loop section",
571            ));
572        }
573        let tracks = score
574            .channels()
575            .iter()
576            .map(|channel| {
577                let events: Vec<Event> = channel
578                    .notes()
579                    .iter()
580                    .enumerate()
581                    .map(|(note, n)| {
582                        let on = to_samples(n.start);
583                        let end = to_samples(n.start + n.length);
584                        Event {
585                            note,
586                            on,
587                            off: on + (end - on) * n.gate as u64 / 8,
588                            cents: (n.pitch.midi() as f64 - 69.0) * 100.0,
589                            instrument: n.instrument,
590                            amplitude: if n.velocity == 127 {
591                                n.volume as f64 / 15.0
592                            } else {
593                                (n.volume as f64 / 15.0) * (n.velocity as f64 / 127.0)
594                            },
595                        }
596                    })
597                    .collect();
598                let loop_next = events.partition_point(|e| e.on < loop_start);
599                let loop_carry = loop_next
600                    .checked_sub(1)
601                    .map(|i| events[i])
602                    .filter(|e| {
603                        e.off > loop_start
604                            && plans[e.instrument].drum.is_none()
605                            && plans[e.instrument].voices == 0
606                    })
607                    .map(|e| Event {
608                        on: loop_start,
609                        ..e
610                    });
611                let poly_count = events
612                    .iter()
613                    .map(|e| plans[e.instrument].voices)
614                    .max()
615                    .unwrap_or(0);
616                let poly_carry = events
617                    .iter()
618                    .filter(|e| {
619                        e.on < loop_start && e.off > loop_start && plans[e.instrument].voices > 0
620                    })
621                    .map(|e| Event {
622                        on: loop_start,
623                        ..*e
624                    })
625                    .collect();
626                Track {
627                    events,
628                    next: 0,
629                    loop_next,
630                    loop_carry,
631                    voice: Voice::new(options.aliasing),
632                    poly: (0..poly_count)
633                        .map(|_| Voice::new(options.aliasing))
634                        .collect(),
635                    poly_carry,
636                }
637            })
638            .collect();
639        Ok(Self {
640            rate: sample_rate,
641            channels,
642            aliasing: options.aliasing,
643            gain: options.gain as f64,
644            max_release: plans
645                .iter()
646                .map(|p| p.release.max(p.drum.as_ref().map_or(0, |d| d.frames)))
647                .max()
648                .unwrap_or(0),
649            plans,
650            tracks,
651            song_end,
652            loop_start,
653            loops: options.loops,
654            loops_left: options.loops,
655            position: 0,
656            ending: false,
657            finished: false,
658            produced: 0,
659            tempo_map: None,
660            controls: None,
661        })
662    }
663    /// Prepare absolute note frames for mono output, using existing instruments
664    /// and allocation-free `fill`. Unlike Score, the map supports fractional and
665    /// changing tempo. Preparation is bounded to 8 tracks, 256 instruments,
666    /// 200000 notes and a 1200-second body. Map changes must be within that body.
667    ///
668    /// Notes require `on < off <= end_frame`, ordered onsets, a valid instrument,
669    /// volume 0..=15 and velocity 0..=127. Invalid arguments return
670    /// `InvalidArgument`, resource limits `LimitExceeded`. Expand repetitions
671    /// before preparation; `options.loops` must be `Some(0)` on this path.
672    ///
673    /// At each frame, onsets run first in track/input order, followed by gate-offs
674    /// on the remaining held voices, as on Score. Single-voice replacement and
675    /// pooled stealing retain their semantics. A tie is one extended FrameNote;
676    /// it preserves oscillator/envelope/filter state across tempo boundaries.
677    ///
678    /// Beat LFO phase integrates scheduled tempos continuously from frame zero;
679    /// tick macros integrate it relative to note start. Other clocks retain their
680    /// original units. Release tails drain after the exclusive body end; no
681    /// leading latency, post-render retiming or automatic limiting is added.
682    pub fn from_frames(
683        score: &FrameScore,
684        map: &TempoMap,
685        options: &RenderOptions,
686    ) -> Result<Self> {
687        Self::from_frame_channels(score, map, options, 1)
688    }
689    /// Stereo counterpart of [`Self::from_frames`], including existing synthwave
690    /// spread/pan. Multiple prepared sources can use the existing mixer Stream
691    /// adapter, with a common map and a shared scheduled start.
692    pub fn from_frames_stereo(
693        score: &FrameScore,
694        map: &TempoMap,
695        options: &RenderOptions,
696    ) -> Result<Self> {
697        Self::from_frame_channels(score, map, options, 2)
698    }
699    fn from_frame_channels(
700        score: &FrameScore,
701        map: &TempoMap,
702        options: &RenderOptions,
703        channels: usize,
704    ) -> Result<Self> {
705        score.validate(map)?;
706        if options.loops != Some(0) || !options.gain.is_finite() {
707            return Err(Error::invalid(
708                "Prepared synth requires finite gain and no implicit loops",
709            ));
710        }
711        let rate = map.sample_rate();
712        // The prepared musical clock replaces every tempo-dependent calculation;
713        // these legacy plan fields are used only by the unchanged Score path.
714        let plans: Vec<_> = score
715            .instruments
716            .iter()
717            .map(|i| Plan::new(i, rate, 120, map.ticks_per_quarter()))
718            .collect();
719        let tracks = score
720            .tracks
721            .iter()
722            .map(|notes| {
723                let events: Vec<_> = notes
724                    .iter()
725                    .enumerate()
726                    .map(|(note, n)| Event {
727                        note,
728                        on: n.on,
729                        off: n.off,
730                        cents: (n.pitch.midi() as f64 - 69.0) * 100.0,
731                        instrument: n.instrument,
732                        amplitude: if n.velocity == 127 {
733                            n.volume as f64 / 15.0
734                        } else {
735                            (n.volume as f64 / 15.0) * (n.velocity as f64 / 127.0)
736                        },
737                    })
738                    .collect();
739                let count = events
740                    .iter()
741                    .map(|e| plans[e.instrument].voices)
742                    .max()
743                    .unwrap_or(0);
744                Track {
745                    events,
746                    next: 0,
747                    loop_next: 0,
748                    loop_carry: None,
749                    poly_carry: Vec::new(),
750                    voice: Voice::new(options.aliasing),
751                    poly: (0..count).map(|_| Voice::new(options.aliasing)).collect(),
752                }
753            })
754            .collect();
755        Ok(Self {
756            rate,
757            channels,
758            aliasing: options.aliasing,
759            gain: options.gain as f64,
760            max_release: plans
761                .iter()
762                .map(|p| p.release.max(p.drum.as_ref().map_or(0, |d| d.frames)))
763                .max()
764                .unwrap_or(0),
765            plans,
766            tracks,
767            song_end: score.end_frame,
768            loop_start: 0,
769            loops: Some(0),
770            loops_left: Some(0),
771            position: 0,
772            ending: false,
773            finished: false,
774            produced: 0,
775            tempo_map: Some(map.clone()),
776            controls: None,
777        })
778    }
779    /// Prepare a replayable control plan before the first fill (or after reset).
780    /// Events must be frame-ordered; equal frames follow input order after note
781    /// onsets/gate-offs. Capacity includes retained history for reset, up to
782    /// 65536 events/results. Zero capacity with no events disables controls.
783    /// Preparation allocates; rejection preserves the current plan and renderer.
784    /// Implicit loops must be disabled. Controls start before the body end and
785    /// ramps end by it; held/releasing voices retain final values afterward.
786    pub fn prepare_controls(&mut self, events: &[ControlEvent], capacity: usize) -> Result<()> {
787        if self.produced != 0 || self.finished {
788            return Err(Error::invalid(
789                "Prepare synth controls before playback or after reset",
790            ));
791        }
792        if capacity > MAX_SYNTH_CONTROLS || events.len() > capacity {
793            return Err(Error::limit("Synth control capacity exceeded"));
794        }
795        if capacity == 0 {
796            self.controls = None;
797            return Ok(());
798        }
799        if self.loops != Some(0) {
800            return Err(Error::invalid("Synth controls require no implicit loops"));
801        }
802        let mut used = vec![false; self.tracks.len() * self.plans.len()];
803        for (t, track) in self.tracks.iter().enumerate() {
804            for event in &track.events {
805                used[t * self.plans.len() + event.instrument] = true;
806            }
807        }
808        let mut state =
809            ControlState::new(used, self.tracks.iter().map(|t| t.poly.len() + 1), capacity)?;
810        let entries: Vec<_> = events
811            .iter()
812            .copied()
813            .enumerate()
814            .map(|(id, event)| (ControlId(id as u32), event))
815            .collect();
816        state.queue = controls::compile(&entries, capacity, |event| {
817            self.resolve_control(&state, event)
818        })?;
819        self.controls = Some(state);
820        Ok(())
821    }
822    /// Add/retarget future control under exclusive source ownership, between
823    /// fills. This is a control-thread operation: it may allocate and recompile
824    /// queued ramps. Past frames, unsupported/missing targets, invalid ranges or
825    /// exponential endpoints fail atomically. A full retained plan is
826    /// `LimitExceeded`. Same-frame ordering is submission order; IDs stay stable.
827    pub fn schedule_control(&mut self, event: ControlEvent) -> Result<ControlId> {
828        let state = self
829            .controls
830            .as_ref()
831            .ok_or_else(|| Error::limit("No synth control capacity prepared"))?;
832        if state.queue.len() >= state.capacity {
833            return Err(Error::limit("Synth control plan is full"));
834        }
835        if event.frame < self.produced || self.finished {
836            return Err(Error::invalid(
837                "Cannot schedule a past/finished synth control",
838            ));
839        }
840        let id = ControlId(state.queue.len() as u32);
841        let mut entries: Vec<_> = state.queue.iter().map(|c| (c.id, c.event)).collect();
842        let index = entries.partition_point(|(_, e)| e.frame <= event.frame);
843        entries.insert(index, (id, event));
844        let queue = controls::compile(&entries, state.capacity, |event| {
845            self.resolve_control(state, event)
846        })?;
847        self.controls.as_mut().unwrap().queue = queue;
848        Ok(id)
849    }
850    pub fn controls_len(&self) -> usize {
851        self.controls.as_ref().map_or(0, |s| s.queue.len())
852    }
853    pub fn controls_capacity(&self) -> usize {
854        self.controls.as_ref().map_or(0, |s| s.capacity)
855    }
856    /// One result for every executed event. An authored note stolen/ended before
857    /// its event reports TargetInactive without changing any other voice/lane.
858    pub fn poll_control_event(&mut self) -> Option<ControlReport> {
859        self.controls.as_mut()?.poll()
860    }
861    /// Applied controller value at the next source frame (`frames_rendered`),
862    /// before queued events at that frame execute. Note targets must be active.
863    /// Returns base values until their first event. Requires a prepared plan.
864    pub fn control_value(&self, target: ControlTarget, parameter: ControlParameter) -> Result<f64> {
865        let state = self
866            .controls
867            .as_ref()
868            .ok_or_else(|| Error::invalid("No synth controls prepared"))?;
869        let (resolved, base) = self.resolve_control_target(state, target, parameter)?;
870        if let Some((track, note)) = resolved.note {
871            let slot = std::iter::once(&self.tracks[track].voice)
872                .chain(&self.tracks[track].poly)
873                .position(|v| v.active() && v.event.is_some_and(|e| e.note == note))
874                .ok_or_else(|| Error::invalid("Synth control note is inactive"))?;
875            Ok(state
876                .values(resolved.group, track, slot, self.produced)
877                .get(parameter)
878                .unwrap_or(base))
879        } else {
880            Ok(state
881                .group_value(resolved.group, parameter, self.produced)
882                .unwrap_or(base))
883        }
884    }
885    fn resolve_control_target(
886        &self,
887        state: &ControlState,
888        target: ControlTarget,
889        parameter: ControlParameter,
890    ) -> Result<(Resolved, f64)> {
891        let (track, instrument, note) = match target {
892            ControlTarget::Instrument { track, instrument } => (track, instrument, None),
893            ControlTarget::Note { track, note } => {
894                let event = self
895                    .tracks
896                    .get(track)
897                    .and_then(|t| t.events.get(note))
898                    .ok_or_else(|| Error::invalid("Missing synth control note"))?;
899                (track, event.instrument, Some((track, note)))
900            }
901        };
902        if track >= self.tracks.len()
903            || instrument >= self.plans.len()
904            || !state.used[track * self.plans.len() + instrument]
905        {
906            return Err(Error::invalid("Missing synth control track/instrument"));
907        }
908        let base = self.plans[instrument]
909            .control_base(parameter)
910            .ok_or_else(|| Error::invalid("Control is unsupported by this instrument"))?;
911        Ok((
912            Resolved {
913                group: track * self.plans.len() + instrument,
914                note,
915            },
916            base,
917        ))
918    }
919    fn resolve_control(
920        &self,
921        state: &ControlState,
922        event: ControlEvent,
923    ) -> Result<(Resolved, f64)> {
924        let end = event
925            .frame
926            .checked_add(event.ramp_frames)
927            .ok_or_else(|| Error::limit("Synth ramp frame overflow"))?;
928        if event.frame >= self.song_end || end > self.song_end {
929            return Err(Error::invalid("Synth control exceeds the body"));
930        }
931        let resolved = self.resolve_control_target(state, event.target, event.parameter)?;
932        if let Some((track, note)) = resolved.0.note {
933            let n = self.tracks[track].events[note];
934            if event.frame < n.on || event.frame >= n.off {
935                return Err(Error::invalid(
936                    "Note control must start inside its authored gate",
937                ));
938            }
939        }
940        Ok(resolved)
941    }
942    pub fn sample_rate(&self) -> u32 {
943        self.rate
944    }
945    /// 1 for mono constructors, 2 for stereo constructors.
946    pub fn channels(&self) -> usize {
947        self.channels
948    }
949    pub fn is_finished(&self) -> bool {
950        self.finished
951    }
952    /// Frames rendered since the start or the last [`Synth::reset`].
953    pub fn frames_rendered(&self) -> u64 {
954        self.produced
955    }
956    /// An upper bound on the frames this synthesizer renders, or `None` if it loops forever.
957    pub fn max_frames(&self) -> Option<u64> {
958        let body = self.song_end.saturating_sub(self.loop_start);
959        self.loops.map(|n| {
960            (n as u64)
961                .saturating_mul(body)
962                .saturating_add(self.song_end + self.max_release + 1)
963        })
964    }
965    /// Starts again from the beginning, as if newly created.
966    pub fn reset(&mut self) {
967        if let Some(controls) = &mut self.controls {
968            controls.reset();
969        }
970        for track in &mut self.tracks {
971            track.next = 0;
972            track.voice = Voice::new(self.aliasing);
973            for voice in &mut track.poly {
974                *voice = Voice::new(self.aliasing);
975            }
976        }
977        self.loops_left = self.loops;
978        self.position = 0;
979        self.ending = false;
980        self.finished = false;
981        self.produced = 0;
982    }
983    /// Fills whole interleaved frames and returns their count. Mono uses one sample
984    /// per frame, stereo two. Unused samples (including an incomplete frame) are untouched.
985    pub fn fill(&mut self, out: &mut [f32]) -> usize {
986        let frames = out.len() / self.channels;
987        for (written, slot) in out.chunks_exact_mut(self.channels).enumerate() {
988            if self.finished {
989                return written;
990            }
991            if !self.ending {
992                self.schedule();
993            }
994            if self.ending && self.tracks.iter().all(|t| !t.active()) {
995                self.finished = true;
996                return written;
997            }
998            let mut sum = 0.0;
999            let mut right = 0.0;
1000            let musical = self.tempo_map.as_ref().map(|map| map.clock(self.produced));
1001            if let Some(state) = &mut self.controls {
1002                for (t, track) in self.tracks.iter().enumerate() {
1003                    for (slot, voice) in
1004                        std::iter::once(&track.voice).chain(&track.poly).enumerate()
1005                    {
1006                        state.sync(
1007                            t,
1008                            slot,
1009                            voice.event.filter(|_| voice.active()).map(|e| e.note),
1010                        );
1011                    }
1012                }
1013                state.advance(self.produced);
1014            }
1015            for (t, track) in self.tracks.iter_mut().enumerate() {
1016                let values = |slot: usize, instrument: usize| {
1017                    self.controls.as_ref().and_then(|s| {
1018                        s.values(t * self.plans.len() + instrument, t, slot, self.produced)
1019                            .some()
1020                    })
1021                };
1022                let control = track.voice.event.and_then(|e| values(0, e.instrument));
1023                if control.is_some() {
1024                    let [l, r] = track.voice.sample_stereo(
1025                        &self.plans,
1026                        self.rate,
1027                        self.produced,
1028                        musical,
1029                        control,
1030                    );
1031                    if self.channels == 1 {
1032                        sum += (l + r) * 0.5;
1033                    } else {
1034                        sum += l;
1035                        right += r;
1036                    }
1037                } else {
1038                    let legacy = track.voice.sample(&self.plans, self.rate, musical, None);
1039                    sum += legacy;
1040                    right += legacy;
1041                }
1042                for (slot, voice) in track.poly.iter_mut().enumerate() {
1043                    let control = voice.event.and_then(|e| values(slot + 1, e.instrument));
1044                    let [l, r] = voice.sample_stereo(
1045                        &self.plans,
1046                        self.rate,
1047                        self.produced,
1048                        musical,
1049                        control,
1050                    );
1051                    if self.channels == 1 {
1052                        sum += (l + r) * 0.5;
1053                    } else {
1054                        sum += l;
1055                        right += r;
1056                    }
1057                }
1058            }
1059            slot[0] = (sum * self.gain) as f32;
1060            if self.channels == 2 {
1061                slot[1] = (right * self.gain) as f32;
1062            }
1063            self.position += 1;
1064            self.produced += 1;
1065        }
1066        frames
1067    }
1068    fn schedule(&mut self) {
1069        if self.position >= self.song_end {
1070            for track in &mut self.tracks {
1071                track.release(&self.plans);
1072            }
1073            let can_loop = self.loop_start < self.song_end && self.loops_left != Some(0);
1074            if !can_loop {
1075                self.ending = true;
1076                return;
1077            }
1078            if let Some(n) = &mut self.loops_left {
1079                *n -= 1;
1080            }
1081            self.position = self.loop_start;
1082            for track in &mut self.tracks {
1083                track.next = track.loop_next;
1084                if let Some(event) = track.loop_carry {
1085                    track.voice.start(event, &self.plans);
1086                }
1087                // Carries are precompiled; no clone/temporary allocation on audio.
1088                for i in 0..track.poly_carry.len() {
1089                    let event = track.poly_carry[i];
1090                    track.trigger(event, &self.plans, self.produced);
1091                }
1092            }
1093        }
1094        let position = self.position;
1095        for track in &mut self.tracks {
1096            while let Some(&event) = track.events.get(track.next) {
1097                if event.on > position {
1098                    break;
1099                }
1100                track.trigger(event, &self.plans, self.produced);
1101                track.next += 1;
1102            }
1103            let voice = &mut track.voice;
1104            if voice.held && voice.event.is_some_and(|e| e.off <= position) {
1105                voice.release(&self.plans);
1106            }
1107            for voice in &mut track.poly {
1108                if voice.held && voice.event.is_some_and(|e| e.off <= position) {
1109                    voice.release(&self.plans);
1110                }
1111            }
1112        }
1113    }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119    #[test]
1120    fn scheduled_sweeps_preserve_oscillator_filter_and_envelope_state() {
1121        use super::super::ControlCurve;
1122        let score=Score::parse("tempo 120\ninstrument pad voices=1 wave=saw adsr=1000,0,100,40 filter=lowpass cutoff=1000 filter-adsr=1000,0,100,40 filter-env=1200\nchannel a=@pad c1").unwrap();
1123        let mut source = Synth::new_stereo(&score, 1000, &Default::default()).unwrap();
1124        let mut reference = source.clone();
1125        source.prepare_controls(&[], 4).unwrap();
1126        let mut a = [0.0; 800];
1127        let mut b = [0.0; 800];
1128        source.fill(&mut a);
1129        reference.fill(&mut b);
1130        assert_eq!(a, b);
1131        let before = format!("{:?}", source.tracks[0].poly[0]);
1132        for (parameter, value) in [
1133            (ControlParameter::CutoffHz, 6000.0),
1134            (ControlParameter::Expression, 0.2),
1135        ] {
1136            source
1137                .schedule_control(ControlEvent {
1138                    frame: 400,
1139                    target: ControlTarget::Instrument {
1140                        track: 0,
1141                        instrument: 0,
1142                    },
1143                    parameter,
1144                    value,
1145                    ramp_frames: 400,
1146                    curve: ControlCurve::Linear,
1147                })
1148                .unwrap();
1149        }
1150        assert_eq!(
1151            format!("{:?}", source.tracks[0].poly[0]),
1152            before,
1153            "submission changes no DSP state"
1154        );
1155        let mut a = [0.0; 2];
1156        let mut b = [0.0; 2];
1157        source.fill(&mut a);
1158        reference.fill(&mut b);
1159        assert_eq!(a, b, "ramp start retains the current parameter values");
1160        assert_eq!(
1161            format!("{:?}", source.tracks[0].poly[0].wide),
1162            format!("{:?}", reference.tracks[0].poly[0].wide),
1163            "filter histories and oscillator phases retain their exact state"
1164        );
1165        source.fill(&mut [0.0; 798]);
1166        assert_eq!(source.tracks[0].poly[0].elapsed, 800);
1167        assert_eq!(source.tracks[0].poly[0].position, 800);
1168        assert_eq!(source.tracks[0].poly[0].stage, Stage::Attack);
1169        assert_eq!(
1170            source
1171                .control_value(
1172                    ControlTarget::Instrument {
1173                        track: 0,
1174                        instrument: 0
1175                    },
1176                    ControlParameter::CutoffHz
1177                )
1178                .unwrap(),
1179            6000.0
1180        );
1181    }
1182    #[test]
1183    fn legato_keeps_envelopes_but_short_gates_and_gaps_retrigger() {
1184        for gate in [4, 8] {
1185            let score=Score::parse(&format!("tempo 600\ninstrument p voices=1 legato=on glide=50 adsr=200,0,100,20 filter=lowpass cutoff=1000 filter-adsr=200,0,100,20\nchannel a=@p q{gate} a4 b4 r4 c4")).unwrap();
1186            let mut synth = Synth::new(&score, 1000, &Default::default()).unwrap();
1187            synth.fill(&mut [0.0; 126]);
1188            let voice = &synth.tracks[0].poly[0];
1189            assert_eq!(voice.position, if gate == 8 { 126 } else { 26 });
1190            synth.fill(&mut [0.0; 175]);
1191            let voice = &synth.tracks[0].poly[0];
1192            assert_eq!(voice.position, 1);
1193            assert!(voice.held);
1194        }
1195    }
1196    #[test]
1197    fn stealing_reuses_oldest_release_and_its_new_note_off() {
1198        let score = Score::parse(
1199            "tempo 600\ninstrument p voices=2 adsr=0,0,100,500\nchannel a=@p a4 b4 c4 d4",
1200        )
1201        .unwrap();
1202        let mut synth = Synth::new(&score, 1000, &Default::default()).unwrap();
1203        synth.fill(&mut [0.0; 201]);
1204        let voices = &synth.tracks[0].poly;
1205        assert_eq!(voices.len(), 2);
1206        assert_eq!(voices[0].event.unwrap().cents, -900.0); // C4 replaced A4's release.
1207        assert!(voices[0].held);
1208        assert!(!voices[1].held);
1209        synth.fill(&mut [0.0; 50]);
1210        assert!(synth.tracks[0].poly[0].held);
1211    }
1212    #[test]
1213    fn reducing_and_restoring_a_voice_limit_restarts_retired_slots_from_silence() {
1214        let score=Score::parse("tempo 600\ninstrument wide voices=3 adsr=100,0,100,0\ninstrument narrow voices=1 adsr=100,0,100,0\nchannel a=@wide {c e g}4 @narrow c4 @wide {c e g}4").unwrap();
1215        let mut synth = Synth::new(&score, 1000, &Default::default()).unwrap();
1216        synth.fill(&mut [0.0; 201]);
1217        for v in &synth.tracks[0].poly[1..] {
1218            assert_eq!(v.from, 0.0);
1219            assert_eq!(v.level, 0.0);
1220            assert_eq!(v.position, 1);
1221        }
1222    }
1223}