conformal_component/parameters/utils/
ramped.rs

1use std::{
2    collections::{HashMap, HashSet},
3    hash::BuildHasher,
4    ops::{Range, RangeInclusive},
5};
6
7use crate::{
8    audio::approx_eq,
9    events::NoteID,
10    synth::{
11        NumericGlobalExpression, NumericPerNoteExpression, SwitchGlobalExpression,
12        SynthParamBufferStates, valid_range_for_per_note_expression,
13    },
14};
15
16use super::super::{
17    BufferState, BufferStates, EnumBufferState, IdHash, IdHashMap, InfoRef, InternalValue,
18    NumericBufferState, PiecewiseLinearCurve, PiecewiseLinearCurvePoint, SwitchBufferState,
19    TimedEnumValues, TimedSwitchValues, TimedValue, TypeSpecificInfoRef, hash_id,
20};
21
22#[derive(Clone, Debug)]
23struct RampedNumeric {
24    start: f32,
25    end: f32,
26    range: RangeInclusive<f32>,
27}
28
29#[derive(Clone, Debug)]
30struct RampedEnum {
31    start: u32,
32    end: u32,
33    range: Range<u32>,
34}
35
36#[derive(Clone, Debug)]
37struct RampedSwitch {
38    start: bool,
39    end: bool,
40}
41
42#[derive(Clone, Debug)]
43enum RampedState {
44    Constant(InternalValue),
45    Numeric(RampedNumeric),
46    Enum(RampedEnum),
47    Switch(RampedSwitch),
48}
49
50/// A simple implementation of a [`BufferStates`] that allows
51/// for parameters to change between the start and end of a buffer.
52///
53/// Each parameter can be either constant or ramped between two values.
54///
55/// For numeric parameters, the ramp is linear, for other parameter types
56/// the value changes half-way through the buffer.
57#[derive(Clone, Debug, Default)]
58pub struct RampedStatesMap {
59    buffer_size: usize,
60    map: IdHashMap<RampedState>,
61}
62
63fn ramped_numeric(start: f32, end: f32, range: RangeInclusive<f32>) -> RampedState {
64    if approx_eq(start, end, 1e-6) {
65        RampedState::Constant(InternalValue::Numeric(start))
66    } else {
67        RampedState::Numeric(RampedNumeric { start, end, range })
68    }
69}
70
71fn ramped_enum(start: u32, end: u32, num_values: usize) -> RampedState {
72    if start == end {
73        RampedState::Constant(InternalValue::Enum(start))
74    } else {
75        RampedState::Enum(RampedEnum {
76            start,
77            end,
78            range: 0..u32::try_from(num_values).unwrap(),
79        })
80    }
81}
82
83fn ramped_switch(start: bool, end: bool) -> RampedState {
84    if start == end {
85        RampedState::Constant(InternalValue::Switch(start))
86    } else {
87        RampedState::Switch(RampedSwitch { start, end })
88    }
89}
90
91fn ramp_for_numeric(
92    default: f32,
93    valid_range: RangeInclusive<f32>,
94    start_override: Option<InternalValue>,
95    end_override: Option<InternalValue>,
96) -> RampedState {
97    let start = match start_override {
98        Some(InternalValue::Numeric(v)) => v,
99        None => default,
100        _ => panic!(),
101    };
102    let end = match end_override {
103        Some(InternalValue::Numeric(v)) => v,
104        None => default,
105        _ => panic!(),
106    };
107    ramped_numeric(start, end, valid_range)
108}
109
110fn ramp_for_enum(
111    default: u32,
112    num_values: usize,
113    start_override: Option<InternalValue>,
114    end_override: Option<InternalValue>,
115) -> RampedState {
116    let start = match start_override {
117        Some(InternalValue::Enum(v)) => v,
118        None => default,
119        _ => panic!(),
120    };
121    let end = match end_override {
122        Some(InternalValue::Enum(v)) => v,
123        None => default,
124        _ => panic!(),
125    };
126    ramped_enum(start, end, num_values)
127}
128
129fn ramp_for_switch(
130    default: bool,
131    start_override: Option<InternalValue>,
132    end_override: Option<InternalValue>,
133) -> RampedState {
134    let start = match start_override {
135        Some(InternalValue::Switch(v)) => v,
136        None => default,
137        _ => panic!(),
138    };
139    let end = match end_override {
140        Some(InternalValue::Switch(v)) => v,
141        None => default,
142        _ => panic!(),
143    };
144    ramped_switch(start, end)
145}
146
147impl RampedStatesMap {
148    /// Constructor that creates a `RampedStatesMap`
149    /// from a list of `Info`s and `override`s.at the start and end of the buffer.
150    ///
151    /// These overrides work the same way as in [`override_defaults`].
152    ///
153    /// Note for a synth, you should use [`SynthRampedStatesMap::new`] instead.
154    ///
155    /// # Examples
156    /// ```
157    /// # use conformal_component::parameters::{StaticInfoRef, InternalValue, TypeSpecificInfoRef, RampedStatesMap, NumericBufferState, BufferStates};
158    /// # use std::collections::HashMap;
159    /// let infos = vec![
160    ///   StaticInfoRef {
161    ///     title: "Numeric",
162    ///     short_title: "Numeric",
163    ///     unique_id: "numeric",
164    ///     flags: Default::default(),
165    ///     type_specific: TypeSpecificInfoRef::Numeric {
166    ///       default: 0.0,
167    ///       valid_range: 0.0..=1.0,
168    ///       units: None,
169    ///     },
170    ///   },
171    /// ];
172    ///
173    /// let start_overrides: HashMap<_, _> = vec![].into_iter().collect();
174    /// let end_overrides: HashMap<_, _> = vec![("numeric", InternalValue::Numeric(0.5))].into_iter().collect();
175    /// let states = RampedStatesMap::new(infos.iter().cloned(), &start_overrides, &end_overrides, 10);
176    ///
177    /// match states.get_numeric("numeric") {
178    ///   Some(NumericBufferState::PiecewiseLinear(_)) => (),
179    ///   _ => panic!("Expected a ramped value"),
180    /// };
181    /// ```
182    ///
183    /// # Panics
184    ///
185    /// Panics if `start_overrides` or `end_overrides` do not match the type of the parameter
186    /// specified in `infos`.
187    ///
188    /// Also panics if any of the enum parameters in `infos` has a number of values
189    /// that will not fit into a `u32`.
190    pub fn new<'a, S: AsRef<str> + 'a, H: BuildHasher, H_: BuildHasher>(
191        infos: impl IntoIterator<Item = InfoRef<'a, S>> + 'a,
192        start_overrides: &HashMap<&'_ str, InternalValue, H>,
193        end_overrides: &HashMap<&'_ str, InternalValue, H_>,
194        buffer_size: usize,
195    ) -> Self {
196        let map = infos
197            .into_iter()
198            .map(|info| {
199                let id = hash_id(info.unique_id);
200                let start_override = start_overrides.get(info.unique_id);
201                let end_override = end_overrides.get(info.unique_id);
202                let value = match info.type_specific {
203                    TypeSpecificInfoRef::Numeric {
204                        default,
205                        valid_range,
206                        ..
207                    } => ramp_for_numeric(
208                        default,
209                        valid_range,
210                        start_override.copied(),
211                        end_override.copied(),
212                    ),
213                    TypeSpecificInfoRef::Enum {
214                        default, values, ..
215                    } => ramp_for_enum(
216                        default,
217                        values.len(),
218                        start_override.copied(),
219                        end_override.copied(),
220                    ),
221                    TypeSpecificInfoRef::Switch { default } => {
222                        ramp_for_switch(default, start_override.copied(), end_override.copied())
223                    }
224                };
225                (id, value)
226            })
227            .collect();
228        Self { buffer_size, map }
229    }
230
231    /// Helper to make a `RampedStatesMap` with all parameters constant.
232    ///
233    /// This is useful for _performance_ testing because while the parameters
234    /// are constant at run-time, the `RampedStatesMap` has the ability to
235    /// ramp between values, so consumers cannot be specialized to handle constant
236    /// values only
237    ///
238    /// Note that if you want to pass this into a synth, you should use [`Self::new_const_synth`]
239    /// instead.
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// # use conformal_component::parameters::{StaticInfoRef, InternalValue, TypeSpecificInfoRef, RampedStatesMap, NumericBufferState, BufferStates};
245    /// let infos = vec![
246    ///   StaticInfoRef {
247    ///     title: "Numeric",
248    ///     short_title: "Numeric",
249    ///     unique_id: "numeric",
250    ///     flags: Default::default(),
251    ///     type_specific: TypeSpecificInfoRef::Numeric {
252    ///       default: 0.0,
253    ///       valid_range: 0.0..=1.0,
254    ///       units: None,
255    ///     },
256    ///   },
257    /// ];
258    ///
259    /// let overrides = vec![("numeric", InternalValue::Numeric(0.5))].into_iter().collect();
260    /// let states = RampedStatesMap::new_const(infos.iter().cloned(), &overrides);
261    /// match states.get_numeric("numeric") {
262    ///   Some(NumericBufferState::Constant(0.5)) => (),
263    ///   _ => panic!("Expected constant value of 0.5"),
264    /// };
265    /// ```
266    pub fn new_const<'a, S: AsRef<str> + 'a>(
267        infos: impl IntoIterator<Item = InfoRef<'a, S>> + 'a,
268        overrides: &HashMap<&'_ str, InternalValue>,
269    ) -> Self {
270        Self::new(infos, overrides, overrides, 0)
271    }
272}
273
274fn ramp_numeric(
275    start: f32,
276    end: f32,
277    buffer_size: usize,
278) -> impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone {
279    [0, 1].iter().map(move |i| {
280        let value = if *i == 0 { start } else { end };
281        let sample_offset = if *i == 0 { 0 } else { buffer_size - 1 };
282        PiecewiseLinearCurvePoint {
283            sample_offset,
284            value,
285        }
286    })
287}
288
289fn ramp_enum(
290    start: u32,
291    end: u32,
292    buffer_size: usize,
293) -> impl Iterator<Item = TimedValue<u32>> + Clone {
294    [0, 1].iter().map(move |i| {
295        let value = if *i == 0 { start } else { end };
296        let sample_offset = if *i == 0 { 0 } else { buffer_size / 2 };
297        TimedValue {
298            sample_offset,
299            value,
300        }
301    })
302}
303
304fn ramp_switch(
305    start: bool,
306    end: bool,
307    buffer_size: usize,
308) -> impl Iterator<Item = TimedValue<bool>> + Clone {
309    [0, 1].iter().map(move |i| {
310        let value = if *i == 0 { start } else { end };
311        let sample_offset = if *i == 0 { 0 } else { buffer_size / 2 };
312        TimedValue {
313            sample_offset,
314            value,
315        }
316    })
317}
318
319impl BufferStates for RampedStatesMap {
320    fn get_by_hash(
321        &self,
322        id_hash: IdHash,
323    ) -> std::option::Option<
324        BufferState<
325            impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
326            impl Iterator<Item = TimedValue<u32>> + Clone,
327            impl Iterator<Item = TimedValue<bool>> + Clone,
328        >,
329    > {
330        let param = self.map.get(&id_hash)?;
331        match param {
332            RampedState::Constant(value) => match value {
333                InternalValue::Numeric(n) => {
334                    Some(BufferState::Numeric(NumericBufferState::Constant(*n)))
335                }
336                InternalValue::Enum(e) => Some(BufferState::Enum(EnumBufferState::Constant(*e))),
337                InternalValue::Switch(s) => {
338                    Some(BufferState::Switch(SwitchBufferState::Constant(*s)))
339                }
340            },
341            RampedState::Numeric(RampedNumeric { start, end, range }) => {
342                Some(BufferState::Numeric(NumericBufferState::PiecewiseLinear(
343                    PiecewiseLinearCurve::new(
344                        ramp_numeric(*start, *end, self.buffer_size),
345                        self.buffer_size,
346                        range.clone(),
347                    )?,
348                )))
349            }
350            RampedState::Enum(RampedEnum { start, end, range }) => Some(BufferState::Enum(
351                EnumBufferState::Varying(TimedEnumValues::new(
352                    ramp_enum(*start, *end, self.buffer_size),
353                    self.buffer_size,
354                    range.clone(),
355                )?),
356            )),
357            RampedState::Switch(RampedSwitch { start, end }) => Some(BufferState::Switch(
358                SwitchBufferState::Varying(TimedSwitchValues::new(
359                    ramp_switch(*start, *end, self.buffer_size),
360                    self.buffer_size,
361                )?),
362            )),
363        }
364    }
365}
366
367fn valid_range_for_numeric_global_expression(
368    expression: NumericGlobalExpression,
369) -> RangeInclusive<f32> {
370    match expression {
371        NumericGlobalExpression::PitchBend => -1.0..=1.0,
372        NumericGlobalExpression::Timbre
373        | NumericGlobalExpression::Aftertouch
374        | NumericGlobalExpression::ExpressionPedal
375        | NumericGlobalExpression::ModWheel => 0.0..=1.0,
376    }
377}
378
379fn ramp_numeric_expressions(
380    start_overrides: &HashMap<NumericGlobalExpression, f32>,
381    end_overrides: &HashMap<NumericGlobalExpression, f32>,
382) -> HashMap<NumericGlobalExpression, RampedState> {
383    let all_expressions = start_overrides
384        .keys()
385        .chain(end_overrides.keys())
386        .collect::<HashSet<_>>();
387    all_expressions
388        .into_iter()
389        .map(|expression| {
390            (
391                *expression,
392                ramp_for_numeric(
393                    Default::default(),
394                    valid_range_for_numeric_global_expression(*expression),
395                    start_overrides
396                        .get(expression)
397                        .copied()
398                        .map(InternalValue::Numeric),
399                    end_overrides
400                        .get(expression)
401                        .copied()
402                        .map(InternalValue::Numeric),
403                ),
404            )
405        })
406        .collect()
407}
408
409fn ramp_switch_expressions(
410    start_overrides: &HashMap<SwitchGlobalExpression, bool>,
411    end_overrides: &HashMap<SwitchGlobalExpression, bool>,
412) -> HashMap<SwitchGlobalExpression, RampedState> {
413    let all_expressions = start_overrides
414        .keys()
415        .chain(end_overrides.keys())
416        .collect::<HashSet<_>>();
417    all_expressions
418        .into_iter()
419        .map(|expression| {
420            (
421                *expression,
422                ramp_for_switch(
423                    Default::default(),
424                    start_overrides
425                        .get(expression)
426                        .copied()
427                        .map(InternalValue::Switch),
428                    end_overrides
429                        .get(expression)
430                        .copied()
431                        .map(InternalValue::Switch),
432                ),
433            )
434        })
435        .collect()
436}
437
438fn ramp_per_note_expressions(
439    start_overrides: &HashMap<(NumericPerNoteExpression, NoteID), f32>,
440    end_overrides: &HashMap<(NumericPerNoteExpression, NoteID), f32>,
441) -> HashMap<(NumericPerNoteExpression, NoteID), RampedState> {
442    let all_keys = start_overrides
443        .keys()
444        .chain(end_overrides.keys())
445        .collect::<HashSet<_>>();
446    all_keys
447        .into_iter()
448        .map(|key| {
449            let (expression, _) = *key;
450            (
451                *key,
452                ramp_for_numeric(
453                    Default::default(),
454                    valid_range_for_per_note_expression(expression),
455                    start_overrides
456                        .get(key)
457                        .copied()
458                        .map(InternalValue::Numeric),
459                    end_overrides.get(key).copied().map(InternalValue::Numeric),
460                ),
461            )
462        })
463        .collect()
464}
465
466/// A simple implementation of a [`SynthParamBufferStates`] that allows
467/// for parameters to change between the start and end of a buffer.
468///
469/// This is similar to [`RampedStatesMap`], but it also includes the expression controller parameters
470/// needed for synths.
471///
472/// Each parameter can be either constant or ramped between two values.
473///
474/// For numeric parameters, the ramp is linear, for other parameter types
475/// the value changes half-way through the buffer.
476pub struct SynthRampedStatesMap {
477    states: RampedStatesMap,
478    numeric_expressions: HashMap<NumericGlobalExpression, RampedState>,
479    switch_expressions: HashMap<SwitchGlobalExpression, RampedState>,
480    per_note_expressions: HashMap<(NumericPerNoteExpression, NoteID), RampedState>,
481}
482
483/// Params for [`SynthRampedStatesMap::new`]
484pub struct SynthRampedOverrides<'a, 'b> {
485    /// Overrides for parameters at the start of the buffer
486    pub start_params: &'a HashMap<&'b str, InternalValue>,
487    /// Overrides for parameters at the end of the buffer
488    pub end_params: &'a HashMap<&'b str, InternalValue>,
489    /// Overrides for numeric global expression controllerss at the start of the buffer
490    pub start_numeric_expressions: &'a HashMap<NumericGlobalExpression, f32>,
491    /// Overrides for numeric global expression controllers at the end of the buffer
492    pub end_numeric_expressions: &'a HashMap<NumericGlobalExpression, f32>,
493    /// Overrides for switch global expression controllers at the start of the buffer
494    pub start_switch_expressions: &'a HashMap<SwitchGlobalExpression, bool>,
495    /// Overrides for switch global expression controllers at the end of the buffer
496    pub end_switch_expressions: &'a HashMap<SwitchGlobalExpression, bool>,
497}
498
499impl SynthRampedStatesMap {
500    /// Create a new [`SynthRampedStatesMap`] for synths from a list of `Info`s and `override`s.
501    ///
502    /// This is similar to [`RampedStatesMap::new`], but it also includes the expression controller parameters.
503    ///
504    /// # Examples
505    ///
506    /// ```
507    /// # use conformal_component::parameters::{StaticInfoRef, InternalValue, TypeSpecificInfoRef, SynthRampedStatesMap, NumericBufferState, BufferStates, SynthRampedOverrides};
508    /// # use conformal_component::synth::{SynthParamBufferStates, NumericGlobalExpression};
509    /// let infos = vec![
510    ///   StaticInfoRef {
511    ///     title: "Numeric",
512    ///     short_title: "Numeric",
513    ///     unique_id: "numeric",
514    ///     flags: Default::default(),
515    ///     type_specific: TypeSpecificInfoRef::Numeric {
516    ///       default: 0.0,
517    ///       valid_range: 0.0..=1.0,
518    ///       units: None,
519    ///     },
520    ///   },
521    /// ];
522    ///
523    /// let start_expression_overrides = vec![(NumericGlobalExpression::ModWheel, 1.0)].into_iter().collect();
524    /// let end_param_overrides = vec![("numeric", InternalValue::Numeric(0.5))].into_iter().collect();
525    /// let states = SynthRampedStatesMap::new(
526    ///   infos.iter().cloned(),
527    ///   SynthRampedOverrides {
528    ///     start_params: &Default::default(),
529    ///     end_params: &end_param_overrides,
530    ///     start_numeric_expressions: &start_expression_overrides,
531    ///     end_numeric_expressions: &Default::default(),
532    ///     start_switch_expressions: &Default::default(),
533    ///     end_switch_expressions: &Default::default(),
534    ///   },
535    ///   10
536    /// );
537    ///
538    /// // If we only overrode a value at the beginning or end
539    /// // it should be ramped
540    /// match states.get_numeric("numeric") {
541    ///   Some(NumericBufferState::PiecewiseLinear(_)) => (),
542    ///   _ => panic!("Expected a ramped value"),
543    /// };
544    /// match states.get_numeric_global_expression(NumericGlobalExpression::ModWheel) {
545    ///   NumericBufferState::PiecewiseLinear(_) => (),
546    ///   _ => panic!("Expected a ramped value"),
547    /// };
548    ///
549    /// // Params left at default should be constants
550    /// match states.get_numeric_global_expression(NumericGlobalExpression::PitchBend) {
551    ///   NumericBufferState::Constant(0.0) => (),
552    ///   _ => panic!("Expected a constant value"),
553    /// };
554    /// ```
555    ///
556    /// # Panics
557    ///
558    /// Panics if `start_overrides` or `end_overrides` do not match the type of the parameter
559    /// specified in `infos`.
560    pub fn new<'a, S: AsRef<str> + 'a>(
561        infos: impl IntoIterator<Item = InfoRef<'a, S>> + 'a,
562        SynthRampedOverrides {
563            start_params,
564            end_params,
565            start_numeric_expressions,
566            end_numeric_expressions,
567            start_switch_expressions,
568            end_switch_expressions,
569        }: SynthRampedOverrides<'_, '_>,
570        buffer_size: usize,
571    ) -> Self {
572        Self {
573            states: RampedStatesMap::new(infos, start_params, end_params, buffer_size),
574            numeric_expressions: ramp_numeric_expressions(
575                start_numeric_expressions,
576                end_numeric_expressions,
577            ),
578            switch_expressions: ramp_switch_expressions(
579                start_switch_expressions,
580                end_switch_expressions,
581            ),
582            per_note_expressions: Default::default(),
583        }
584    }
585
586    /// Create a new [`SynthRampedStatesMap`] with per-note expression overrides.
587    ///
588    /// This is similar to [`Self::new`], but also allows specifying per-note expression
589    /// values for specific notes at the start and end of the buffer.
590    ///
591    /// # Examples
592    ///
593    /// ```
594    /// # use conformal_component::parameters::{StaticInfoRef, InternalValue, TypeSpecificInfoRef, SynthRampedStatesMap, NumericBufferState, BufferStates, SynthRampedOverrides};
595    /// # use conformal_component::synth::{SynthParamBufferStates, NumericGlobalExpression, NumericPerNoteExpression};
596    /// # use conformal_component::events::{NoteID};
597    /// let infos = vec![
598    ///   StaticInfoRef {
599    ///     title: "Numeric",
600    ///     short_title: "Numeric",
601    ///     unique_id: "numeric",
602    ///     flags: Default::default(),
603    ///     type_specific: TypeSpecificInfoRef::Numeric {
604    ///       default: 0.0,
605    ///       valid_range: 0.0..=1.0,
606    ///       units: None,
607    ///     },
608    ///   },
609    /// ];
610    ///
611    /// let note_id = NoteID::from_pitch(60);
612    /// let start_per_note = vec![
613    ///   ((NumericPerNoteExpression::PitchBend, note_id), 0.0),
614    /// ].into_iter().collect();
615    /// let end_per_note = vec![
616    ///   ((NumericPerNoteExpression::PitchBend, note_id), 2.0),
617    /// ].into_iter().collect();
618    ///
619    /// let states = SynthRampedStatesMap::new_with_per_note(
620    ///   infos.iter().cloned(),
621    ///   SynthRampedOverrides {
622    ///     start_params: &Default::default(),
623    ///     end_params: &Default::default(),
624    ///     start_numeric_expressions: &Default::default(),
625    ///     end_numeric_expressions: &Default::default(),
626    ///     start_switch_expressions: &Default::default(),
627    ///     end_switch_expressions: &Default::default(),
628    ///   },
629    ///   &start_per_note,
630    ///   &end_per_note,
631    ///   10,
632    /// );
633    ///
634    /// match states.get_numeric_expression_for_note(NumericPerNoteExpression::PitchBend, note_id) {
635    ///   NumericBufferState::PiecewiseLinear(_) => (),
636    ///   _ => panic!("Expected a ramped value"),
637    /// };
638    /// ```
639    pub fn new_with_per_note<'a, S: AsRef<str> + 'a>(
640        infos: impl IntoIterator<Item = InfoRef<'a, S>> + 'a,
641        SynthRampedOverrides {
642            start_params,
643            end_params,
644            start_numeric_expressions,
645            end_numeric_expressions,
646            start_switch_expressions,
647            end_switch_expressions,
648        }: SynthRampedOverrides<'_, '_>,
649        start_per_note_expressions: &HashMap<(NumericPerNoteExpression, NoteID), f32>,
650        end_per_note_expressions: &HashMap<(NumericPerNoteExpression, NoteID), f32>,
651        buffer_size: usize,
652    ) -> Self {
653        Self {
654            states: RampedStatesMap::new(infos, start_params, end_params, buffer_size),
655            numeric_expressions: ramp_numeric_expressions(
656                start_numeric_expressions,
657                end_numeric_expressions,
658            ),
659            switch_expressions: ramp_switch_expressions(
660                start_switch_expressions,
661                end_switch_expressions,
662            ),
663            per_note_expressions: ramp_per_note_expressions(
664                start_per_note_expressions,
665                end_per_note_expressions,
666            ),
667        }
668    }
669
670    /// Create a new [`SynthRampedStatesMap`] for synths with all parameters constant.
671    ///
672    /// This is useful for _performance_ testing because while the parameters
673    /// are constant at run-time, the `SynthRampedStatesMap` has the ability to
674    /// ramp between values, so consumers cannot be specialized to handle constant
675    /// values only
676    ///
677    /// This is similar to [`RampedStatesMap::new_const`], but it also includes the expression controller parameters.
678    ///
679    /// # Examples
680    ///
681    /// ```
682    /// # use conformal_component::parameters::{StaticInfoRef, InternalValue, TypeSpecificInfoRef, SynthRampedStatesMap, NumericBufferState, BufferStates};
683    /// # use conformal_component::synth::{SynthParamBufferStates, NumericGlobalExpression};
684    ///
685    /// let infos = vec![
686    ///   StaticInfoRef {
687    ///     title: "Numeric",
688    ///     short_title: "Numeric",
689    ///     unique_id: "numeric",
690    ///     flags: Default::default(),
691    ///     type_specific: TypeSpecificInfoRef::Numeric {
692    ///       default: 0.0,
693    ///       valid_range: 0.0..=1.0,
694    ///       units: None,
695    ///     },
696    ///   },
697    /// ];
698    /// let overrides = vec![("numeric", InternalValue::Numeric(0.5))].into_iter().collect();
699    /// let states = SynthRampedStatesMap::new_const(infos.iter().cloned(), &overrides, &Default::default(), &Default::default());
700    ///
701    /// // Overridden parameters get the values you passed in
702    /// match states.get_numeric("numeric") {
703    ///   Some(NumericBufferState::Constant(0.5)) => (),
704    ///   _ => panic!("Expected constant value of 0.5"),
705    /// };
706    ///
707    /// // Controller parameters will also be included
708    /// match states.get_numeric_global_expression(NumericGlobalExpression::ModWheel) {
709    ///   NumericBufferState::Constant(0.0) => (),
710    ///   _ => panic!("Expected constant value of 0.0"),
711    /// };
712    /// ```
713    pub fn new_const<'a, S: AsRef<str> + 'a>(
714        infos: impl IntoIterator<Item = InfoRef<'a, S>> + 'a,
715        overrides: &HashMap<&'_ str, InternalValue>,
716        numeric_expression_overrides: &HashMap<NumericGlobalExpression, f32>,
717        switch_expression_overrides: &HashMap<SwitchGlobalExpression, bool>,
718    ) -> Self {
719        Self::new_with_per_note(
720            infos,
721            SynthRampedOverrides {
722                start_params: overrides,
723                end_params: overrides,
724                start_numeric_expressions: numeric_expression_overrides,
725                end_numeric_expressions: numeric_expression_overrides,
726                start_switch_expressions: switch_expression_overrides,
727                end_switch_expressions: switch_expression_overrides,
728            },
729            &Default::default(),
730            &Default::default(),
731            0,
732        )
733    }
734
735    /// Create a new [`SynthRampedStatesMap`] for synths with all parameters constant,
736    /// including per-note expression overrides.
737    ///
738    /// This is similar to [`Self::new_const`], but also allows specifying per-note
739    /// expression values for specific notes.
740    ///
741    /// # Examples
742    ///
743    /// ```
744    /// # use conformal_component::parameters::{StaticInfoRef, InternalValue, TypeSpecificInfoRef, SynthRampedStatesMap, NumericBufferState, BufferStates};
745    /// # use conformal_component::synth::{SynthParamBufferStates, NumericGlobalExpression, NumericPerNoteExpression};
746    /// # use conformal_component::events::{NoteID};
747    ///
748    /// let infos = vec![
749    ///   StaticInfoRef {
750    ///     title: "Numeric",
751    ///     short_title: "Numeric",
752    ///     unique_id: "numeric",
753    ///     flags: Default::default(),
754    ///     type_specific: TypeSpecificInfoRef::Numeric {
755    ///       default: 0.0,
756    ///       valid_range: 0.0..=1.0,
757    ///       units: None,
758    ///     },
759    ///   },
760    /// ];
761    ///
762    /// let note_id = NoteID::from_pitch(60);
763    /// let per_note_overrides = vec![
764    ///   ((NumericPerNoteExpression::PitchBend, note_id), 1.5),
765    /// ].into_iter().collect();
766    ///
767    /// let states = SynthRampedStatesMap::new_const_with_per_note(
768    ///   infos.iter().cloned(),
769    ///   &Default::default(),
770    ///   &Default::default(),
771    ///   &Default::default(),
772    ///   &per_note_overrides,
773    /// );
774    ///
775    /// match states.get_numeric_expression_for_note(NumericPerNoteExpression::PitchBend, note_id) {
776    ///   NumericBufferState::Constant(v) if (v - 1.5).abs() < 1e-6 => (),
777    ///   _ => panic!("Expected constant value of 1.5"),
778    /// };
779    /// ```
780    pub fn new_const_with_per_note<'a, S: AsRef<str> + 'a>(
781        infos: impl IntoIterator<Item = InfoRef<'a, S>> + 'a,
782        overrides: &HashMap<&'_ str, InternalValue>,
783        numeric_expression_overrides: &HashMap<NumericGlobalExpression, f32>,
784        switch_expression_overrides: &HashMap<SwitchGlobalExpression, bool>,
785        per_note_expression_overrides: &HashMap<(NumericPerNoteExpression, NoteID), f32>,
786    ) -> Self {
787        Self::new_with_per_note(
788            infos,
789            SynthRampedOverrides {
790                start_params: overrides,
791                end_params: overrides,
792                start_numeric_expressions: numeric_expression_overrides,
793                end_numeric_expressions: numeric_expression_overrides,
794                start_switch_expressions: switch_expression_overrides,
795                end_switch_expressions: switch_expression_overrides,
796            },
797            per_note_expression_overrides,
798            per_note_expression_overrides,
799            0,
800        )
801    }
802}
803
804impl BufferStates for SynthRampedStatesMap {
805    fn get_by_hash(
806        &self,
807        id_hash: IdHash,
808    ) -> std::option::Option<
809        BufferState<
810            impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
811            impl Iterator<Item = TimedValue<u32>> + Clone,
812            impl Iterator<Item = TimedValue<bool>> + Clone,
813        >,
814    > {
815        self.states.get_by_hash(id_hash)
816    }
817}
818
819impl SynthParamBufferStates for SynthRampedStatesMap {
820    fn get_numeric_global_expression(
821        &self,
822        expression: NumericGlobalExpression,
823    ) -> NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone> {
824        match self.numeric_expressions.get(&expression) {
825            Some(RampedState::Constant(InternalValue::Numeric(v))) => {
826                NumericBufferState::Constant(*v)
827            }
828            Some(RampedState::Numeric(RampedNumeric { start, end, range })) => {
829                let curve = PiecewiseLinearCurve::new(
830                    ramp_numeric(*start, *end, self.states.buffer_size),
831                    self.states.buffer_size,
832                    range.clone(),
833                );
834                if let Some(curve) = curve {
835                    NumericBufferState::PiecewiseLinear(curve)
836                } else {
837                    panic!(
838                        "{start} -> {end} is not a valid ramp for {expression:?} (range: {range:?})"
839                    );
840                }
841            }
842
843            None => NumericBufferState::Constant(Default::default()),
844            _ => unreachable!(
845                "internal invariant violation: expected a numeric global expression to be either constant or ramped numeric"
846            ),
847        }
848    }
849
850    fn get_switch_global_expression(
851        &self,
852        expression: SwitchGlobalExpression,
853    ) -> SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone> {
854        match self.switch_expressions.get(&expression) {
855            Some(RampedState::Constant(InternalValue::Switch(v))) => {
856                SwitchBufferState::Constant(*v)
857            }
858            Some(RampedState::Switch(RampedSwitch { start, end })) => {
859                let values = TimedSwitchValues::new(
860                    ramp_switch(*start, *end, self.states.buffer_size),
861                    self.states.buffer_size,
862                );
863                if let Some(values) = values {
864                    SwitchBufferState::Varying(values)
865                } else {
866                    unreachable!(
867                        "TimedSwitchValues invariant violated when ramping {expression:?} from {start} to {end}"
868                    )
869                }
870            }
871            None => SwitchBufferState::Constant(Default::default()),
872            _ => unreachable!(
873                "internal invariant violation: expected a switch global expression to be either constant or ramped switch"
874            ),
875        }
876    }
877
878    fn get_numeric_expression_for_note(
879        &self,
880        expression: NumericPerNoteExpression,
881        note_id: NoteID,
882    ) -> NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone> {
883        match self.per_note_expressions.get(&(expression, note_id)) {
884            Some(RampedState::Constant(InternalValue::Numeric(v))) => {
885                NumericBufferState::Constant(*v)
886            }
887            Some(RampedState::Numeric(RampedNumeric { start, end, range })) => {
888                let curve = PiecewiseLinearCurve::new(
889                    ramp_numeric(*start, *end, self.states.buffer_size),
890                    self.states.buffer_size,
891                    range.clone(),
892                );
893                if let Some(curve) = curve {
894                    NumericBufferState::PiecewiseLinear(curve)
895                } else {
896                    panic!(
897                        "{start} -> {end} is not a valid ramp for {expression:?} (range: {range:?})"
898                    );
899                }
900            }
901            None => NumericBufferState::Constant(Default::default()),
902            _ => unreachable!(
903                "internal invariant violation: expected a per-note expression to be either constant or ramped numeric"
904            ),
905        }
906    }
907}