Skip to main content

audiobox/synth/
controls.rs

1//! Bounded source-frame control plans. No shared live handle or callback allocation.
2use crate::{Error, Result};
3use std::collections::BTreeMap;
4use std::str::FromStr;
5
6/// Maximum retained controls/results in one source's replayable plan.
7pub const MAX_SYNTH_CONTROLS: usize = 65536;
8pub(super) const PARAMETERS: usize = 9;
9
10/// Supported continuous quantities. Structural instrument edits are not controls.
11/// `FromStr` accepts the documented kebab-case names; unsupported names return
12/// `InvalidArgument`, including `voices`, `unison`, `waveform` and envelope edits.
13#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
14#[repr(usize)]
15pub enum ControlParameter {
16    /// Linear multiplier after the voice envelope, 0..=1. Default one.
17    Expression,
18    /// Whole-voice linear pan, -1..=1; overrides the base pan, before its LFO.
19    Pan,
20    /// Synthwave base cutoff in Hz, 1..=96000, before existing modulation/clamp.
21    CutoffHz,
22    /// Synthwave filter Q, 0.5..=10.
23    ResonanceQ,
24    /// Synthwave additive oscillator pitch offset, -2400..=2400 cents.
25    PitchCents,
26    /// Existing pitch LFO depth, 0..=2400 cents; a configured LFO is required.
27    PitchLfoDepth,
28    /// Existing cutoff LFO depth, 0..=9600 cents; a configured LFO is required.
29    CutoffLfoDepth,
30    /// Existing amplitude LFO depth, 0..=100 percent.
31    AmplitudeLfoDepth,
32    /// Existing pan LFO depth, 0..=100 percent.
33    PanLfoDepth,
34}
35impl ControlParameter {
36    pub(super) fn validate(self, value: f64) -> Result<()> {
37        let (min, max) = match self {
38            Self::Expression => (0.0, 1.0),
39            Self::Pan => (-1.0, 1.0),
40            Self::CutoffHz => (1.0, 96000.0),
41            Self::ResonanceQ => (0.5, 10.0),
42            Self::PitchCents => (-2400.0, 2400.0),
43            Self::PitchLfoDepth => (0.0, 2400.0),
44            Self::CutoffLfoDepth => (0.0, 9600.0),
45            Self::AmplitudeLfoDepth | Self::PanLfoDepth => (0.0, 100.0),
46        };
47        if !value.is_finite() || !(min..=max).contains(&value) {
48            return Err(Error::invalid(
49                "Synth control value exceeds its parameter range",
50            ));
51        }
52        Ok(())
53    }
54}
55impl FromStr for ControlParameter {
56    type Err = Error;
57    fn from_str(name: &str) -> Result<Self> {
58        Ok(match name {
59            "expression" => Self::Expression,
60            "pan" => Self::Pan,
61            "cutoff-hz" => Self::CutoffHz,
62            "resonance-q" => Self::ResonanceQ,
63            "pitch-cents" => Self::PitchCents,
64            "pitch-lfo-depth" => Self::PitchLfoDepth,
65            "cutoff-lfo-depth" => Self::CutoffLfoDepth,
66            "amplitude-lfo-depth" => Self::AmplitudeLfoDepth,
67            "pan-lfo-depth" => Self::PanLfoDepth,
68            _ => return Err(Error::invalid("Unsupported synth control parameter")),
69        })
70    }
71}
72
73/// Persistent track/instrument control, or an individual authored note index.
74/// Note indices are positions in the track's Score notes / FrameScore notes.
75#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
76pub enum ControlTarget {
77    /// Affects existing/releasing and future matching voices, independently per track.
78    Instrument { track: usize, instrument: usize },
79    /// Overrides that note's instrument lane. It must start within the authored
80    /// gate. A note already stolen/ended at execution reports `TargetInactive`.
81    Note { track: usize, note: usize },
82}
83
84/// Ramp interpolation, separate from the mixer's fade-easing curves.
85#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
86pub enum ControlCurve {
87    #[default]
88    Linear,
89    /// Geometric interpolation in log2 space, requiring strictly positive
90    /// source/target endpoints. Uses deterministic engine log2/exp2.
91    Exponential,
92}
93
94/// One source-frame event. Zero ramp frames steps immediately and then holds.
95/// A positive ramp starts at the current value on `frame` and reaches `value`
96/// exactly on `frame + ramp_frames`; an overlapping event retargets from there.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct ControlEvent {
99    pub frame: u64,
100    pub target: ControlTarget,
101    pub parameter: ControlParameter,
102    pub value: f64,
103    pub ramp_frames: u64,
104    pub curve: ControlCurve,
105}
106impl ControlEvent {
107    pub fn new(frame: u64, target: ControlTarget, parameter: ControlParameter, value: f64) -> Self {
108        Self {
109            frame,
110            target,
111            parameter,
112            value,
113            ramp_frames: 0,
114            curve: ControlCurve::Linear,
115        }
116    }
117}
118
119/// Submission index in the current plan. Reset replays the same IDs; replacing
120/// a plan starts a new ID space. Later scheduling does not renumber earlier IDs.
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub struct ControlId(pub u32);
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum ControlStatus {
125    Applied,
126    TargetInactive,
127}
128/// One bounded result per executed event. Polling does not allocate or free.
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub struct ControlReport {
131    pub id: ControlId,
132    pub frame: u64,
133    pub status: ControlStatus,
134}
135
136#[derive(Clone, Copy, Debug)]
137pub(super) struct Ramp {
138    start: u64,
139    end: u64,
140    from: f64,
141    to: f64,
142    curve: ControlCurve,
143    log_from: f64,
144    log_to: f64,
145}
146impl Ramp {
147    fn new(from: f64, event: ControlEvent) -> Result<Self> {
148        let (log_from, log_to) = if event.curve == ControlCurve::Exponential {
149            if from <= 0.0 || event.value <= 0.0 {
150                return Err(Error::invalid(
151                    "Exponential synth controls require positive endpoints",
152                ));
153            }
154            (crate::math::log2(from), crate::math::log2(event.value))
155        } else {
156            (0.0, 0.0)
157        };
158        Ok(Self {
159            start: event.frame,
160            end: event
161                .frame
162                .checked_add(event.ramp_frames)
163                .ok_or_else(|| Error::limit("Synth ramp frame overflow"))?,
164            from,
165            to: event.value,
166            curve: event.curve,
167            log_from,
168            log_to,
169        })
170    }
171    pub(super) fn value(self, frame: u64) -> f64 {
172        if frame >= self.end {
173            return self.to;
174        }
175        if frame <= self.start {
176            return self.from;
177        }
178        let t = (frame - self.start) as f64 / (self.end - self.start) as f64;
179        let value = match self.curve {
180            ControlCurve::Linear => self.from + (self.to - self.from) * t,
181            ControlCurve::Exponential => {
182                crate::math::exp2(self.log_from + (self.log_to - self.log_from) * t)
183            }
184        };
185        value.clamp(self.from.min(self.to), self.from.max(self.to))
186    }
187}
188pub(super) type Lanes = [Option<Ramp>; PARAMETERS];
189#[derive(Clone, Copy, Debug, Default)]
190pub(super) struct Values {
191    mask: u16,
192    values: [f64; PARAMETERS],
193}
194impl Values {
195    pub(super) fn get(self, p: ControlParameter) -> Option<f64> {
196        (self.mask & (1 << p as usize) != 0).then_some(self.values[p as usize])
197    }
198    fn set(&mut self, p: usize, value: f64) {
199        self.mask |= 1 << p;
200        self.values[p] = value;
201    }
202    pub(super) fn some(self) -> Option<Self> {
203        (self.mask != 0).then_some(self)
204    }
205}
206#[derive(Clone, Copy, Debug)]
207pub(super) struct Resolved {
208    pub group: usize,
209    pub note: Option<(usize, usize)>,
210}
211#[derive(Clone, Copy, Debug)]
212pub(super) struct Scheduled {
213    pub id: ControlId,
214    pub event: ControlEvent,
215    target: Resolved,
216    ramp: Ramp,
217}
218#[derive(Clone, Debug)]
219pub(super) struct VoiceLanes {
220    note: Option<usize>,
221    lanes: Lanes,
222}
223#[derive(Debug)]
224pub(super) struct ControlState {
225    pub queue: Vec<Scheduled>,
226    pub next: usize,
227    pub capacity: usize,
228    pub used: Vec<bool>,
229    groups: Vec<Lanes>,
230    voices: Vec<Vec<VoiceLanes>>,
231    reports: Vec<ControlReport>,
232    poll: usize,
233}
234impl Clone for ControlState {
235    fn clone(&self) -> Self {
236        // Vec::clone retains only length, losing reserved callback capacity.
237        let mut queue = Vec::with_capacity(self.capacity);
238        queue.extend_from_slice(&self.queue);
239        let mut reports = Vec::with_capacity(self.capacity);
240        reports.extend_from_slice(&self.reports);
241        Self {
242            queue,
243            reports,
244            next: self.next,
245            capacity: self.capacity,
246            used: self.used.clone(),
247            groups: self.groups.clone(),
248            voices: self.voices.clone(),
249            poll: self.poll,
250        }
251    }
252}
253pub(super) fn compile(
254    entries: &[(ControlId, ControlEvent)],
255    capacity: usize,
256    resolve: impl Fn(ControlEvent) -> Result<(Resolved, f64)>,
257) -> Result<Vec<Scheduled>> {
258    if entries.windows(2).any(|p| p[0].1.frame > p[1].1.frame) {
259        return Err(Error::invalid("Synth controls must be frame-ordered"));
260    }
261    let mut queue = Vec::new();
262    queue
263        .try_reserve_exact(capacity)
264        .map_err(|_| Error::limit("Cannot reserve synth controls"))?;
265    let mut history: BTreeMap<(usize, Option<usize>, ControlParameter), Ramp> = BTreeMap::new();
266    for &(id, event) in entries {
267        event.parameter.validate(event.value)?;
268        let (target, base) = resolve(event)?;
269        let key = (
270            target.group,
271            target.note.map(|(_, note)| note),
272            event.parameter,
273        );
274        let from = history
275            .get(&key)
276            .or_else(|| history.get(&(target.group, None, event.parameter)))
277            .map_or(base, |r| r.value(event.frame));
278        let ramp = Ramp::new(from, event)?;
279        history.insert(key, ramp);
280        queue.push(Scheduled {
281            id,
282            event,
283            target,
284            ramp,
285        });
286    }
287    Ok(queue)
288}
289impl ControlState {
290    pub fn new(
291        used: Vec<bool>,
292        voice_counts: impl Iterator<Item = usize>,
293        capacity: usize,
294    ) -> Result<Self> {
295        let groups = vec![[None; PARAMETERS]; used.len()];
296        let voices = voice_counts
297            .map(|n| {
298                vec![
299                    VoiceLanes {
300                        note: None,
301                        lanes: [None; PARAMETERS]
302                    };
303                    n
304                ]
305            })
306            .collect();
307        let mut reports = Vec::new();
308        reports
309            .try_reserve_exact(capacity)
310            .map_err(|_| Error::limit("Cannot reserve synth control reports"))?;
311        Ok(Self {
312            queue: Vec::new(),
313            next: 0,
314            capacity,
315            used,
316            groups,
317            voices,
318            reports,
319            poll: 0,
320        })
321    }
322    pub fn sync(&mut self, track: usize, slot: usize, note: Option<usize>) {
323        let state = &mut self.voices[track][slot];
324        if state.note != note {
325            state.note = note;
326            state.lanes.fill(None);
327        }
328    }
329    pub fn advance(&mut self, frame: u64) {
330        while let Some(c) = self.queue.get(self.next).copied() {
331            if c.event.frame > frame {
332                break;
333            }
334            let lanes = match c.target.note {
335                None => Some(&mut self.groups[c.target.group]),
336                Some((track, note)) => self.voices[track]
337                    .iter_mut()
338                    .find(|v| v.note == Some(note))
339                    .map(|v| &mut v.lanes),
340            };
341            let status = if let Some(lanes) = lanes {
342                lanes[c.event.parameter as usize] = Some(c.ramp);
343                ControlStatus::Applied
344            } else {
345                ControlStatus::TargetInactive
346            };
347            self.reports.push(ControlReport {
348                id: c.id,
349                frame,
350                status,
351            });
352            self.next += 1;
353        }
354    }
355    pub fn values(&self, group: usize, track: usize, slot: usize, frame: u64) -> Values {
356        let mut out = Values::default();
357        for (p, (note, instrument)) in self.voices[track][slot]
358            .lanes
359            .iter()
360            .zip(self.groups[group].iter())
361            .enumerate()
362        {
363            if let Some(ramp) = note.or(*instrument) {
364                out.set(p, ramp.value(frame));
365            }
366        }
367        out
368    }
369    pub fn group_value(&self, group: usize, p: ControlParameter, frame: u64) -> Option<f64> {
370        self.groups[group][p as usize].map(|r| r.value(frame))
371    }
372    pub fn poll(&mut self) -> Option<ControlReport> {
373        let report = self.reports.get(self.poll).copied()?;
374        self.poll += 1;
375        Some(report)
376    }
377    pub fn reset(&mut self) {
378        self.next = 0;
379        self.poll = 0;
380        self.reports.clear();
381        self.groups.fill([None; PARAMETERS]);
382        for track in &mut self.voices {
383            for voice in track {
384                voice.note = None;
385                voice.lanes.fill(None);
386            }
387        }
388    }
389}