conformal_component/
parameters.rs

1//! Code related to the _parameters_ of a processor.
2//!
3//! A processor has a number of _parameters_ that can be changed over time.
4//!
5//! The parameters state is managed by Conformal, with changes ultimately coming
6//! from either the UI or the hosting application.
7//! The parameters form the "logical interface" of the processor.
8//!
9//! Each parameter is one of the following types:
10//!
11//! - Numeric: A numeric value that can vary within a range of possible values.
12//! - Enum: An value that can take one of a discrete set of named values.
13//! - Switch: A value that can be either on or off.
14//!
15//! Note that future versions may add more types of parameters!
16//!
17//! Components tell Conformal about which parameters exist in their [`crate::Component::parameter_infos`] method.
18//!
19//! Conformal will then provide the current state to the processor during processing,
20//! either [`crate::synth::Synth::process`] or [`crate::effect::Effect::process`].
21//!
22//! Note that conformal may also change parameters outside of processing and call
23//! the [`crate::synth::Synth::handle_events`] or
24//! [`crate::effect::Effect::handle_parameters`] methods, Components can update any
25//! internal state in these methods.
26use std::{
27    ops::{Range, RangeBounds, RangeInclusive},
28    string::ToString,
29};
30
31mod utils;
32pub use utils::*;
33
34macro_rules! info_enum_doc {
35    () => {
36        "Information specific to an enum parameter."
37    };
38}
39
40macro_rules! info_enum_default_doc {
41    () => {
42        "Index of the default value.
43
44Note that this _must_ be less than the length of `values`."
45    };
46}
47
48macro_rules! info_enum_values_doc {
49    () => {
50        "A list of possible values for the parameter.
51
52Note that values _must_ contain at least 2 elements."
53    };
54}
55
56macro_rules! info_numeric_doc {
57    () => {
58        "Information specific to a numeric parameter."
59    };
60}
61
62macro_rules! info_numeric_default_doc {
63    () => {
64        "The default value of the parameter.
65
66This value _must_ be within the `valid_range`."
67    };
68}
69
70macro_rules! info_numeric_valid_range_doc {
71    () => {
72        "The valid range of the parameter."
73    };
74}
75
76macro_rules! info_numeric_units_doc {
77    () => {
78        "The units of the parameter.
79
80Here an empty string indicates unitless values, while a non-empty string
81indicates the logical units of a parmater, e.g., \"hz\""
82    };
83}
84
85macro_rules! info_switch_doc {
86    () => {
87        "Information specific to a switch parameter."
88    };
89}
90
91macro_rules! info_switch_default_doc {
92    () => {
93        "The default value of the parameter."
94    };
95}
96
97/// Contains information specific to a certain type of parameter.
98///
99/// This is a non-owning reference type, pointing to data with lifetime `'a`.
100///
101/// Here the `S` represents the type of strings, this generally will be
102/// either `&'a str` or `String`.
103///
104/// # Examples
105///
106/// ```
107/// # use conformal_component::parameters::{TypeSpecificInfoRef};
108/// let enum_info = TypeSpecificInfoRef::Enum {
109///    default: 0,
110///    values: &["A", "B", "C"],
111/// };
112///
113/// let numeric_info: TypeSpecificInfoRef<'static, &'static str> = TypeSpecificInfoRef::Numeric {
114///   default: 0.0,
115///   valid_range: 0.0..=1.0,
116///   units: None,
117/// };
118///
119/// let switch_info: TypeSpecificInfoRef<'static, &'static str> = TypeSpecificInfoRef::Switch {
120///  default: false,
121/// };
122/// ```
123#[derive(Debug, Clone, PartialEq)]
124pub enum TypeSpecificInfoRef<'a, S> {
125    #[doc = info_enum_doc!()]
126    Enum {
127        #[doc = info_enum_default_doc!()]
128        default: u32,
129
130        #[doc = info_enum_values_doc!()]
131        values: &'a [S],
132    },
133
134    #[doc = info_numeric_doc!()]
135    Numeric {
136        #[doc = info_numeric_default_doc!()]
137        default: f32,
138
139        #[doc = info_numeric_valid_range_doc!()]
140        valid_range: RangeInclusive<f32>,
141
142        #[doc = info_numeric_units_doc!()]
143        units: Option<&'a str>,
144    },
145
146    #[doc = info_switch_doc!()]
147    Switch {
148        #[doc = info_switch_default_doc!()]
149        default: bool,
150    },
151}
152
153/// Contains information specific to a certain type of parameter.
154///
155/// This is an owning version of [`TypeSpecificInfoRef`].
156///
157/// # Examples
158///
159/// ```
160/// # use conformal_component::parameters::{TypeSpecificInfo};
161/// let enum_info = TypeSpecificInfo::Enum {
162///   default: 0,
163///   values: vec!["A".to_string(), "B".to_string(), "C".to_string()],
164/// };
165/// let numeric_info = TypeSpecificInfo::Numeric {
166///   default: 0.0,
167///   valid_range: 0.0..=1.0,
168///   units: None,
169/// };
170/// let switch_info = TypeSpecificInfo::Switch {
171///   default: false,
172/// };
173/// ```
174#[derive(Debug, Clone, PartialEq)]
175pub enum TypeSpecificInfo {
176    #[doc = info_enum_doc!()]
177    Enum {
178        #[doc = info_enum_default_doc!()]
179        default: u32,
180
181        #[doc = info_enum_values_doc!()]
182        values: Vec<String>,
183    },
184
185    #[doc = info_numeric_doc!()]
186    Numeric {
187        #[doc = info_numeric_default_doc!()]
188        default: f32,
189
190        #[doc = info_numeric_valid_range_doc!()]
191        valid_range: std::ops::RangeInclusive<f32>,
192
193        #[doc = info_numeric_units_doc!()]
194        units: Option<String>,
195    },
196
197    #[doc = info_switch_doc!()]
198    Switch {
199        #[doc = info_switch_default_doc!()]
200        default: bool,
201    },
202}
203
204impl<'a, S: AsRef<str>> From<&'a TypeSpecificInfoRef<'a, S>> for TypeSpecificInfo {
205    fn from(v: &'a TypeSpecificInfoRef<'a, S>) -> Self {
206        match v {
207            TypeSpecificInfoRef::Enum { default, values } => {
208                let values: Vec<String> = values.iter().map(|s| s.as_ref().to_string()).collect();
209                assert!(values.len() < i32::MAX as usize);
210                TypeSpecificInfo::Enum {
211                    default: *default,
212                    values,
213                }
214            }
215            TypeSpecificInfoRef::Numeric {
216                default,
217                valid_range,
218                units,
219            } => TypeSpecificInfo::Numeric {
220                default: *default,
221                valid_range: valid_range.clone(),
222                units: (*units).map(ToString::to_string),
223            },
224            TypeSpecificInfoRef::Switch { default } => {
225                TypeSpecificInfo::Switch { default: *default }
226            }
227        }
228    }
229}
230
231impl<'a> From<&'a TypeSpecificInfo> for TypeSpecificInfoRef<'a, String> {
232    fn from(v: &'a TypeSpecificInfo) -> Self {
233        match v {
234            TypeSpecificInfo::Enum { default, values } => TypeSpecificInfoRef::Enum {
235                default: *default,
236                values: values.as_slice(),
237            },
238            TypeSpecificInfo::Numeric {
239                default,
240                valid_range,
241                units,
242            } => TypeSpecificInfoRef::Numeric {
243                default: *default,
244                valid_range: valid_range.clone(),
245                units: units.as_ref().map(String::as_str),
246            },
247            TypeSpecificInfo::Switch { default } => {
248                TypeSpecificInfoRef::Switch { default: *default }
249            }
250        }
251    }
252}
253
254/// Metadata about a parameter.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct Flags {
257    /// Whether the parameter can be automated.
258    ///
259    /// In some hosting applications, parameters can be _automated_,
260    /// that is, users are provided with a UI to program the parameter
261    /// to change over time. If this is `true` (the default), then
262    /// this parameter will appear in the automation UI. Otherwise,
263    /// it will not.
264    ///
265    /// You may want to set a parameter to `false` here if it does not
266    /// sound good when it is change frequently, or if it is a parameter
267    /// that may be confusing to users if it appeared in an automation UI.
268    pub automatable: bool,
269}
270
271impl Default for Flags {
272    fn default() -> Self {
273        Flags { automatable: true }
274    }
275}
276
277/// Reserved unique id prefix for internal parameters. No component
278/// should have any parameters with unique ids that start with this prefix.
279pub const UNIQUE_ID_INTERNAL_PREFIX: &str = "_conformal_internal_";
280
281macro_rules! unique_id_doc {
282    () => {
283        "The unique ID of the parameter.
284
285As the name implies, each parameter's id must be unique within
286the comonent's parameters.
287
288Note that this ID will not be presented to the user, it is only
289used to refer to the parameter in code.
290
291The ID must not begin with the prefix `_conformal_internal`, as
292this is reserved for use by the Conformal library itself."
293    };
294}
295
296macro_rules! title_doc {
297    () => {
298        "Human-readable title of the parameter."
299    };
300}
301
302macro_rules! short_title_doc {
303    () => {
304        "A short title of the parameter.
305
306In some hosting applications, this may appear as an
307abbreviated version of the title. If the title is already
308short, it's okay to use the same value for `title` and `short_title`."
309    };
310}
311
312macro_rules! flags_doc {
313    () => {
314        "Metadata about the parameter"
315    };
316}
317
318macro_rules! type_specific_doc {
319    () => {
320        "Information specific to the type of parameter."
321    };
322}
323
324/// Information about a parameter.
325///
326/// This is a non-owning reference type.
327///
328/// If you are referencing static data, use [`StaticInfoRef`] below for simplicity.
329///
330/// This references data with lifetime `'a`.
331/// Here the `S` represents the type of strings, this generally will be
332/// either `&'a str` or `String`.
333#[derive(Debug, Clone, PartialEq)]
334pub struct InfoRef<'a, S> {
335    #[doc = unique_id_doc!()]
336    pub unique_id: &'a str,
337
338    #[doc = title_doc!()]
339    pub title: &'a str,
340
341    #[doc = short_title_doc!()]
342    pub short_title: &'a str,
343
344    #[doc = flags_doc!()]
345    pub flags: Flags,
346
347    #[doc = type_specific_doc!()]
348    pub type_specific: TypeSpecificInfoRef<'a, S>,
349}
350
351/// Owning version of [`InfoRef`].
352#[derive(Debug, Clone, PartialEq)]
353pub struct Info {
354    #[doc = unique_id_doc!()]
355    pub unique_id: String,
356
357    #[doc = title_doc!()]
358    pub title: String,
359
360    #[doc = short_title_doc!()]
361    pub short_title: String,
362
363    #[doc = flags_doc!()]
364    pub flags: Flags,
365
366    #[doc = type_specific_doc!()]
367    pub type_specific: TypeSpecificInfo,
368}
369
370impl<'a, S: AsRef<str>> From<&'a InfoRef<'a, S>> for Info {
371    fn from(v: &'a InfoRef<'a, S>) -> Self {
372        Info {
373            title: v.title.to_string(),
374            short_title: v.short_title.to_string(),
375            unique_id: v.unique_id.to_string(),
376            flags: v.flags.clone(),
377            type_specific: (&v.type_specific).into(),
378        }
379    }
380}
381
382impl<'a> From<&'a Info> for InfoRef<'a, String> {
383    fn from(v: &'a Info) -> Self {
384        InfoRef {
385            title: &v.title,
386            short_title: &v.short_title,
387            unique_id: &v.unique_id,
388            flags: v.flags.clone(),
389            type_specific: (&v.type_specific).into(),
390        }
391    }
392}
393
394/// [`InfoRef`] of static data
395///
396/// In many cases, the `InfoRef` will be a reference to static data,
397/// in which case the type parameters can seem noisy. This type
398/// alias is here for convenience!
399///
400/// # Examples
401///
402/// ```
403/// # use conformal_component::parameters::{TypeSpecificInfoRef, StaticInfoRef};
404/// let enum_info = StaticInfoRef {
405///   title: "Enum",
406///   short_title: "Enum",
407///   unique_id: "enum",
408///   flags: Default::default(),
409///   type_specific: TypeSpecificInfoRef::Enum {
410///     default: 0,
411///     values: &["A", "B", "C"],
412///   },
413/// };
414/// let numeric_info = StaticInfoRef {
415///   title: "Numeric",
416///   short_title: "Num",
417///   unique_id: "numeric",
418///   flags: Default::default(),
419///   type_specific: TypeSpecificInfoRef::Numeric {
420///     default: 0.0,
421///     valid_range: 0.0..=1.0,
422///     units: None,
423///   },
424/// };
425/// let switch_info = StaticInfoRef {
426///   title: "Switch",
427///   short_title: "Switch",
428///   unique_id: "switch",
429///   flags: Default::default(),
430///   type_specific: TypeSpecificInfoRef::Switch {
431///     default: false,
432///   },
433/// };
434/// ```
435pub type StaticInfoRef = InfoRef<'static, &'static str>;
436
437/// Converts a slice of [`InfoRef`]s to a vector of [`Info`]s.
438///
439/// # Examples
440///
441/// ```
442/// # use conformal_component::parameters::{StaticInfoRef, TypeSpecificInfoRef, Info, to_infos};
443/// let infos: Vec<Info> = to_infos(&[
444///   StaticInfoRef {
445///     title: "Switch",
446///     short_title: "Switch",
447///     unique_id: "switch",
448///     flags: Default::default(),
449///     type_specific: TypeSpecificInfoRef::Switch {
450///       default: false,
451///     },
452///   },
453/// ]);
454/// ```
455pub fn to_infos(v: &[InfoRef<'_, &'_ str>]) -> Vec<Info> {
456    v.iter().map(Into::into).collect()
457}
458
459/// A numeric hash of a parameter's ID.
460///
461/// In contexts where performance is critical, we refer to parameters
462/// by a numeric hash of their `unique_id`.
463#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
464pub struct IdHash {
465    internal_hash: u32,
466}
467
468#[doc(hidden)]
469#[must_use]
470pub fn id_hash_from_internal_hash(internal_hash: u32) -> IdHash {
471    IdHash {
472        internal_hash: internal_hash & 0x7fff_ffff,
473    }
474}
475
476impl IdHash {
477    #[doc(hidden)]
478    #[must_use]
479    pub fn internal_hash(&self) -> u32 {
480        self.internal_hash
481    }
482}
483
484/// Creates a hash from a unique ID.
485///
486/// This converts a parameter's `unique_id` into an [`IdHash`].
487///
488/// # Examples
489///
490/// ```
491/// use conformal_component::parameters::hash_id;
492/// let hash = hash_id("my_parameter");
493/// ```
494#[must_use]
495pub fn hash_id(unique_id: &str) -> IdHash {
496    id_hash_from_internal_hash(fxhash::hash32(unique_id) & 0x7fff_ffff)
497}
498
499/// A value of a parameter used in performance-critical ocntexts.
500///
501/// This is used when performance is critical and we don't want to
502/// refer to enums by their string values.
503#[derive(Debug, Clone, PartialEq, Copy)]
504pub enum InternalValue {
505    /// A numeric value.
506    Numeric(f32),
507
508    /// The _index_ of an enum value.
509    ///
510    /// This refers to the index of the current value in the `values`
511    /// array of the parameter.
512    Enum(u32),
513
514    /// A switch value.
515    Switch(bool),
516}
517
518/// A value of a parameter
519///
520/// Outside of performance-critical contexts, we use this to refer
521/// to parameter values.
522#[derive(Debug, Clone, PartialEq)]
523pub enum Value {
524    /// A numeric value.
525    Numeric(f32),
526
527    /// An enum value.
528    Enum(String),
529
530    /// A switch value.
531    Switch(bool),
532}
533
534impl From<f32> for Value {
535    fn from(v: f32) -> Self {
536        Value::Numeric(v)
537    }
538}
539
540impl From<String> for Value {
541    fn from(v: String) -> Self {
542        Value::Enum(v)
543    }
544}
545
546impl From<bool> for Value {
547    fn from(v: bool) -> Self {
548        Value::Switch(v)
549    }
550}
551
552/// Represents a snapshot of all valid parameters at a given point in time.
553///
554/// We use this trait to provide information about parameters when we are
555/// _not_ processing a buffer (for that, we use [`BufferStates`]).
556///
557/// This is passed into [`crate::synth::Synth::handle_events`] and
558/// [`crate::effect::Effect::handle_parameters`].
559///
560/// For convenience, we provide [`States::get_numeric`], [`States::get_enum`],
561/// and [`States::get_switch`] functions, which return the value of the parameter
562/// if it is of the correct type, or `None` otherwise.
563/// Note that all parmeter types re-use the same `ID` space, so only one of the
564/// specialized `get` methods will return a value for a given `ParameterID`.
565///
566/// Note that in general, the Conformal wrapper will implement this trait
567/// for you, but we provide a simple implementation called [`StatesMap`]
568/// that's appropriate to use in tests or other cases where you need to
569/// create this trait outside of a Conformal wrapper.
570pub trait States {
571    /// Get the current value of a parameter by it's hashed unique ID.
572    ///
573    /// You can get the hash of a unique ID using [`hash_id`].
574    ///
575    /// If there is no parameter with the given ID, this will return `None`.
576    fn get_by_hash(&self, id_hash: IdHash) -> Option<InternalValue>;
577
578    /// Get the current value of a parameter by it's unique ID.
579    ///
580    /// If there is no parameter with the given ID, this will return `None`.
581    fn get(&self, unique_id: &str) -> Option<InternalValue> {
582        self.get_by_hash(hash_id(unique_id))
583    }
584
585    /// Get the current numeric value of a parameter by it's hashed unique ID.
586    ///
587    /// You can get the hash of a unique ID using [`hash_id`].
588    ///
589    /// If the parameter is not present or is not numeric, this will return `None`.
590    fn numeric_by_hash(&self, id_hash: IdHash) -> Option<f32> {
591        match self.get_by_hash(id_hash) {
592            Some(InternalValue::Numeric(v)) => Some(v),
593            _ => None,
594        }
595    }
596
597    /// Get the current numeric value of a parameter by it's unique ID.
598    ///
599    /// If the parameter is not present or is not numeric, this will return `None`.
600    fn get_numeric(&self, unique_id: &str) -> Option<f32> {
601        self.numeric_by_hash(hash_id(unique_id))
602    }
603
604    /// Get the current enum value of a parameter by it's hashed unique ID.
605    ///
606    /// You can get the hash of a unique ID using [`hash_id`].
607    ///
608    /// If the parameter is not present or is not an enum, this will return `None`.
609    fn enum_by_hash(&self, id_hash: IdHash) -> Option<u32> {
610        match self.get_by_hash(id_hash) {
611            Some(InternalValue::Enum(v)) => Some(v),
612            _ => None,
613        }
614    }
615
616    /// Get the current enum value of a parameter by it's unique ID.
617    ///
618    /// If the parameter is not present or is not an enum, this will return `None`.
619    fn get_enum(&self, unique_id: &str) -> Option<u32> {
620        self.enum_by_hash(hash_id(unique_id))
621    }
622
623    /// Get the current switch value of a parameter by it's hashed unique ID.
624    ///
625    /// You can get the hash of a unique ID using [`hash_id`].
626    ///
627    /// If the parameter is not present or is not a switch, this will return `None`.
628    fn switch_by_hash(&self, id_hash: IdHash) -> Option<bool> {
629        match self.get_by_hash(id_hash) {
630            Some(InternalValue::Switch(v)) => Some(v),
631            _ => None,
632        }
633    }
634
635    /// Get the current switch value of a parameter by it's unique ID.
636    ///
637    /// If the parameter is not present or is not a switch, this will return `None`.
638    fn get_switch(&self, unique_id: &str) -> Option<bool> {
639        self.switch_by_hash(hash_id(unique_id))
640    }
641}
642
643/// Represents a single point of a piecewise linear curve.
644#[derive(Debug, Clone, PartialEq)]
645pub struct PiecewiseLinearCurvePoint {
646    /// The number of samples from the start of the buffer this point occurs at.
647    pub sample_offset: usize,
648
649    /// The value of the curve at this point.
650    pub value: f32,
651}
652
653/// Represents a numeric value that changes over the course of the buffer.
654///
655/// We represent values changing over the course of the buffer as a piecewise
656/// linear curve, where the curve moving linearly from point to point.
657///
658/// Note that the curve is _guaranteed_ to begin at 0, however it
659/// may end before the end of the buffer - in this case, the value
660/// remains constant until the end of the buffer.
661///
662/// Some invariants:
663///  - There will always be at least one point
664///  - The first point's `sample_offset` will be 0
665///  - `sample_offset`s will be monotonically increasing and only one
666///    point will appear for each `sample_offset`
667///  - All point's `value` will be between the parameter's `min` and `max`
668#[derive(Clone)]
669pub struct PiecewiseLinearCurve<I> {
670    points: I,
671
672    buffer_size: usize,
673}
674
675trait ValueAndSampleOffset<V> {
676    fn value(&self) -> &V;
677    fn sample_offset(&self) -> usize;
678}
679
680impl ValueAndSampleOffset<f32> for PiecewiseLinearCurvePoint {
681    fn value(&self) -> &f32 {
682        &self.value
683    }
684
685    fn sample_offset(&self) -> usize {
686        self.sample_offset
687    }
688}
689
690fn check_curve_invariants<
691    V: PartialOrd + PartialEq + core::fmt::Debug,
692    P: ValueAndSampleOffset<V>,
693    I: Iterator<Item = P>,
694>(
695    iter: I,
696    buffer_size: usize,
697    valid_range: impl RangeBounds<V>,
698) -> bool {
699    let mut last_sample_offset = None;
700    for point in iter {
701        if point.sample_offset() >= buffer_size {
702            return false;
703        }
704        if let Some(last) = last_sample_offset {
705            if point.sample_offset() <= last {
706                return false;
707            }
708        } else if point.sample_offset() != 0 {
709            return false;
710        }
711        if !valid_range.contains(point.value()) {
712            return false;
713        }
714        last_sample_offset = Some(point.sample_offset());
715    }
716    last_sample_offset.is_some()
717}
718
719impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint> + Clone> PiecewiseLinearCurve<I> {
720    /// Construct a new [`PiecewiseLinearCurve`] from an iterator of points.
721    ///
722    /// This will check the invariants for the curve, and if any are invalid, this will
723    /// return `None`.
724    ///
725    /// # Examples
726    ///
727    /// ```
728    /// # use conformal_component::parameters::{PiecewiseLinearCurve, PiecewiseLinearCurvePoint};
729    /// assert!(PiecewiseLinearCurve::new(
730    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
731    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 }],
732    ///   128,
733    ///   0.0..=1.0,
734    /// ).is_some());
735    ///
736    /// // Curves must include at least one point
737    /// assert!(PiecewiseLinearCurve::new(vec![], 128, 0.0..=1.0).is_none());
738    ///
739    /// // Curves can't go outside the valid range.
740    /// assert!(PiecewiseLinearCurve::new(
741    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
742    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 2.0 }],
743    ///   128,
744    ///   0.0..=1.0,
745    /// ).is_none());
746    ///
747    /// // The curve must not go past the end of the buffer
748    /// assert!(PiecewiseLinearCurve::new(
749    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
750    ///        PiecewiseLinearCurvePoint { sample_offset: 128, value: 1.0 }],
751    ///   128,
752    ///   0.0..=1.0,
753    /// ).is_none());
754    ///
755    /// // The first point must be at 0
756    /// assert!(PiecewiseLinearCurve::new(
757    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 50, value: 0.0 },
758    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 }],
759    ///   128,
760    ///   0.0..=1.0,
761    /// ).is_none());
762    ///
763    /// // Sample offsets must monotonically increase
764    /// assert!(PiecewiseLinearCurve::new(
765    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
766    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 },
767    ///        PiecewiseLinearCurvePoint { sample_offset: 50, value: 0.5 }],
768    ///   128,
769    ///   0.0..=1.0,
770    /// ).is_none());
771    /// ```
772    pub fn new(points: I, buffer_size: usize, valid_range: RangeInclusive<f32>) -> Option<Self> {
773        if buffer_size == 0 {
774            return None;
775        }
776        if check_curve_invariants(points.clone().into_iter(), buffer_size, valid_range) {
777            Some(Self {
778                points,
779                buffer_size,
780            })
781        } else {
782            None
783        }
784    }
785}
786
787impl<I> PiecewiseLinearCurve<I> {
788    /// Get the size of the buffer this curve is defined over.
789    ///
790    /// Note that the last point may occur _before_ the end of the buffer,
791    /// in which case the value remains constant from that point until the
792    /// end of the buffer.
793    pub fn buffer_size(&self) -> usize {
794        self.buffer_size
795    }
796}
797
798impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> IntoIterator for PiecewiseLinearCurve<I> {
799    type Item = PiecewiseLinearCurvePoint;
800    type IntoIter = I::IntoIter;
801
802    fn into_iter(self) -> Self::IntoIter {
803        self.points.into_iter()
804    }
805}
806
807/// Represents a value at a specific point in time in a buffer.
808#[derive(Debug, Clone, PartialEq)]
809pub struct TimedValue<V> {
810    /// The number of samples from the start of the buffer.
811    pub sample_offset: usize,
812
813    /// The value at this point in time.
814    pub value: V,
815}
816
817impl<V> ValueAndSampleOffset<V> for TimedValue<V> {
818    fn value(&self) -> &V {
819        &self.value
820    }
821
822    fn sample_offset(&self) -> usize {
823        self.sample_offset
824    }
825}
826
827/// Represents an enum value that changes over the course of a buffer.
828///
829/// Each point represents a change in value at a given sample offset -
830/// the value remains constant until the next point (or the end of the buffer)
831///
832/// Some invariants:
833///  - There will always be at least one point
834///  - The first point's `sample_offset` will be 0
835///  - `sample_offset`s will be monotonically increasing and only one
836///    point will appear for each `sample_offset`
837///  - All point's `value` will be valid
838#[derive(Clone)]
839pub struct TimedEnumValues<I> {
840    points: I,
841    buffer_size: usize,
842}
843
844impl<I: IntoIterator<Item = TimedValue<u32>> + Clone> TimedEnumValues<I> {
845    /// Construct a new [`TimedEnumValues`] from an iterator of points.
846    ///
847    /// This will check the invariants for the curve, and if any are invalid, this will
848    /// return `None`.
849    ///
850    /// Note that here we refer to the enum by the _index_ of the value,
851    /// that is, the index of the value in the `values` array of the parameter.
852    ///
853    /// # Examples
854    ///
855    /// ```
856    /// # use conformal_component::parameters::{TimedEnumValues, TimedValue};
857    /// assert!(TimedEnumValues::new(
858    ///   vec![TimedValue { sample_offset: 0, value: 0 },
859    ///        TimedValue { sample_offset: 100, value: 1 }],
860    ///   128,
861    ///   0..2,
862    /// ).is_some());
863    /// ```
864    pub fn new(points: I, buffer_size: usize, valid_range: Range<u32>) -> Option<Self> {
865        if buffer_size == 0 {
866            return None;
867        }
868        if check_curve_invariants(points.clone().into_iter(), buffer_size, valid_range) {
869            Some(Self {
870                points,
871                buffer_size,
872            })
873        } else {
874            None
875        }
876    }
877}
878
879impl<I> TimedEnumValues<I> {
880    /// Get the size of the buffer this curve is defined over.
881    pub fn buffer_size(&self) -> usize {
882        self.buffer_size
883    }
884}
885
886impl<I: IntoIterator<Item = TimedValue<u32>>> IntoIterator for TimedEnumValues<I> {
887    type Item = TimedValue<u32>;
888    type IntoIter = I::IntoIter;
889
890    fn into_iter(self) -> Self::IntoIter {
891        self.points.into_iter()
892    }
893}
894
895/// Represents a switched value that changes over the course of a buffer.
896///
897/// Each point represents a change in value at a given sample offset -
898/// the value remains constant until the next point (or the end of the buffer)
899///
900/// Some invariants:
901///  - There will always be at least one point
902///  - The first point's `sample_offset` will be 0
903///  - `sample_offset`s will be monotonically increasing and only one
904///    point will appear for each `sample_offset`
905#[derive(Clone)]
906pub struct TimedSwitchValues<I> {
907    points: I,
908    buffer_size: usize,
909}
910
911impl<I: IntoIterator<Item = TimedValue<bool>> + Clone> TimedSwitchValues<I> {
912    /// Construct a new [`TimedSwitchValues`] from an iterator of points.
913    ///
914    /// This will check the invariants for the curve, and if any are invalid, this will
915    /// return `None`.
916    ///
917    /// # Examples
918    ///
919    /// ```
920    /// # use conformal_component::parameters::{TimedSwitchValues, TimedValue};
921    /// assert!(TimedSwitchValues::new(
922    ///   vec![TimedValue { sample_offset: 0, value: false },
923    ///        TimedValue { sample_offset: 100, value: true }],
924    ///   128,
925    /// ).is_some());
926    /// ```
927    pub fn new(points: I, buffer_size: usize) -> Option<Self> {
928        if buffer_size == 0 {
929            return None;
930        }
931        if check_curve_invariants(points.clone().into_iter(), buffer_size, false..=true) {
932            Some(Self {
933                points,
934                buffer_size,
935            })
936        } else {
937            None
938        }
939    }
940}
941
942impl<I> TimedSwitchValues<I> {
943    /// Get the size of the buffer this curve is defined over.
944    pub fn buffer_size(&self) -> usize {
945        self.buffer_size
946    }
947}
948
949impl<I: IntoIterator<Item = TimedValue<bool>>> IntoIterator for TimedSwitchValues<I> {
950    type Item = TimedValue<bool>;
951    type IntoIter = I::IntoIter;
952
953    fn into_iter(self) -> Self::IntoIter {
954        self.points.into_iter()
955    }
956}
957
958/// Represents the state of a numeric value across a buffer
959#[derive(Clone)]
960pub enum NumericBufferState<I> {
961    /// The value is constant across the buffer.
962    Constant(f32),
963
964    /// The value changes over the course of the buffer, represented by a
965    /// [`PiecewiseLinearCurve`].
966    PiecewiseLinear(PiecewiseLinearCurve<I>),
967}
968
969impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> NumericBufferState<I> {
970    /// Get the value of the parameter at the start of the buffer.
971    ///
972    /// # Examples
973    ///
974    /// ```
975    /// # use conformal_component::parameters::{NumericBufferState, PiecewiseLinearCurve, PiecewiseLinearCurvePoint};
976    /// assert_eq!(NumericBufferState::PiecewiseLinear(PiecewiseLinearCurve::new(
977    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.5 },
978    ///       PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 }],
979    ///   128,
980    ///   0.0..=1.0,
981    /// ).unwrap()).value_at_start_of_buffer(), 0.5);
982    /// ```
983    #[allow(clippy::missing_panics_doc)] // Only panics when invariants are broken.
984    pub fn value_at_start_of_buffer(self) -> f32 {
985        match self {
986            NumericBufferState::Constant(v) => v,
987            NumericBufferState::PiecewiseLinear(v) => v.points.into_iter().next().unwrap().value,
988        }
989    }
990}
991
992/// Represents the state of an enum value across a buffer
993///
994/// Here we refer to the enum by the _index_ of the value,
995/// that is, the index of the value in the `values` array of the parameter.
996#[derive(Clone)]
997pub enum EnumBufferState<I> {
998    /// The value is constant across the buffer.
999    Constant(u32),
1000
1001    /// The value changes over the course of the buffer, represented by a
1002    /// [`TimedEnumValues`].
1003    Varying(TimedEnumValues<I>),
1004}
1005
1006impl<I: IntoIterator<Item = TimedValue<u32>>> EnumBufferState<I> {
1007    /// Get the value of the parameter at the start of the buffer,
1008    /// represented by the index of the value in the `values` array of the parameter.
1009    ///
1010    /// # Examples
1011    ///
1012    /// ```
1013    /// # use conformal_component::parameters::{EnumBufferState, TimedEnumValues, TimedValue};
1014    /// assert_eq!(EnumBufferState::Varying(TimedEnumValues::new(
1015    ///   vec![TimedValue { sample_offset: 0, value: 1 },
1016    ///        TimedValue { sample_offset: 100, value: 2 }],
1017    ///   128,
1018    ///   0..3
1019    /// ).unwrap()).value_at_start_of_buffer(), 1);
1020    /// ```
1021    #[allow(clippy::missing_panics_doc)] // Only panics when invariants are broken.
1022    pub fn value_at_start_of_buffer(self) -> u32 {
1023        match self {
1024            EnumBufferState::Constant(v) => v,
1025            EnumBufferState::Varying(v) => v.points.into_iter().next().unwrap().value,
1026        }
1027    }
1028}
1029
1030/// Represents the state of an switched value across a buffer
1031#[derive(Clone)]
1032pub enum SwitchBufferState<I> {
1033    /// The value is constant across the buffer.
1034    Constant(bool),
1035
1036    /// The value changes over the course of the buffer, represented by a
1037    /// [`TimedSwitchValues`].
1038    Varying(TimedSwitchValues<I>),
1039}
1040
1041impl<I: IntoIterator<Item = TimedValue<bool>>> SwitchBufferState<I> {
1042    /// Get the value of the parameter at the start of the buffer.
1043    ///
1044    /// # Examples
1045    ///
1046    /// ```
1047    /// # use conformal_component::parameters::{SwitchBufferState, TimedSwitchValues, TimedValue};
1048    /// assert_eq!(SwitchBufferState::Varying(TimedSwitchValues::new(
1049    ///   vec![TimedValue { sample_offset: 0, value: true },
1050    ///        TimedValue { sample_offset: 100, value: false }],
1051    ///   128,
1052    /// ).unwrap()).value_at_start_of_buffer(), true);
1053    /// ```
1054    #[allow(clippy::missing_panics_doc)] // Only panics when invariants are broken.
1055    pub fn value_at_start_of_buffer(self) -> bool {
1056        match self {
1057            SwitchBufferState::Constant(v) => v,
1058            SwitchBufferState::Varying(v) => v.points.into_iter().next().unwrap().value,
1059        }
1060    }
1061}
1062
1063/// Represents the value of a parameter as it varies across a buffer.
1064pub enum BufferState<N, E, S> {
1065    /// The value of a numeric parameter represented by a [`NumericBufferState`].
1066    Numeric(NumericBufferState<N>),
1067
1068    /// The value of an enum parameter represented by a [`EnumBufferState`].
1069    Enum(EnumBufferState<E>),
1070
1071    /// The value of a switch parameter represented by a [`SwitchBufferState`].
1072    Switch(SwitchBufferState<S>),
1073}
1074
1075/// Represents the state of several parameters across a buffer.
1076///
1077/// Each parameter is represented by a [`BufferState`], which represents
1078/// a value for that parameter at each sample of the buffer.
1079///
1080/// To easily process parameters from this struct, you can use the
1081/// [`crate::pzip`] macro, which converts a [`BufferStates`] into a per-sample
1082/// iterator containing the values of each parameter you want to look at.
1083///
1084/// For more low-level usages, you can deal directly with the underlying [`BufferState`]
1085/// objects, which might yield higher performance in some cases than the [`crate::pzip`] macro.
1086///
1087/// Most of the time, this trait will be provided by the Conformal framework.
1088/// However, we provide simple implementations for this trait for testing or
1089/// in other scenarios where you need to call process functions outside of
1090/// Conformal.
1091///
1092///  - [`ConstantBufferStates`] - A simple implementation where all parameters are constant.
1093///  - [`RampedStatesMap`] - A simple implementation where the parameter can be different at
1094///    the start and end of the buffer.
1095pub trait BufferStates {
1096    /// Get the state of a parameter by it's hashed unique ID.
1097    ///
1098    /// You can get the hash of a unique ID using [`hash_id`].
1099    ///
1100    /// If there is no parameter with the given ID, this will return `None`.
1101    fn get_by_hash(
1102        &self,
1103        id_hash: IdHash,
1104    ) -> Option<
1105        BufferState<
1106            impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
1107            impl Iterator<Item = TimedValue<u32>> + Clone,
1108            impl Iterator<Item = TimedValue<bool>> + Clone,
1109        >,
1110    >;
1111
1112    /// Get the state of a parameter by it's unique ID.
1113    ///
1114    /// If there is no parameter with the given ID, this will return `None`.
1115    fn get(
1116        &self,
1117        unique_id: &str,
1118    ) -> Option<
1119        BufferState<
1120            impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
1121            impl Iterator<Item = TimedValue<u32>> + Clone,
1122            impl Iterator<Item = TimedValue<bool>> + Clone,
1123        >,
1124    > {
1125        self.get_by_hash(hash_id(unique_id))
1126    }
1127
1128    /// Get the state of a numeric parameter by it's hashed unique ID.
1129    ///
1130    /// You can get the hash of a unique ID using [`hash_id`].
1131    ///
1132    /// If there is no parameter with the given ID, or the parameter is not numeric,
1133    /// this will return `None`.
1134    fn numeric_by_hash(
1135        &self,
1136        param_id: IdHash,
1137    ) -> Option<NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>> {
1138        match self.get_by_hash(param_id) {
1139            Some(BufferState::Numeric(v)) => Some(v),
1140            _ => None,
1141        }
1142    }
1143
1144    /// Get the state of a numeric parameter by it's unique ID.
1145    ///
1146    /// If there is no parameter with the given ID, or the parameter is not numeric,
1147    /// this will return `None`.
1148    fn get_numeric(
1149        &self,
1150        unique_id: &str,
1151    ) -> Option<NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>> {
1152        self.numeric_by_hash(hash_id(unique_id))
1153    }
1154
1155    /// Get the state of an enum parameter by it's hashed unique ID.
1156    ///
1157    /// You can get the hash of a unique ID using [`hash_id`].
1158    ///
1159    /// If there is no parameter with the given ID, or the parameter is not an enum,
1160    /// this will return `None`.
1161    fn enum_by_hash(
1162        &self,
1163        param_id: IdHash,
1164    ) -> Option<EnumBufferState<impl Iterator<Item = TimedValue<u32>> + Clone>> {
1165        match self.get_by_hash(param_id) {
1166            Some(BufferState::Enum(v)) => Some(v),
1167            _ => None,
1168        }
1169    }
1170
1171    /// Get the state of an enum parameter by it's unique ID.
1172    ///
1173    /// If there is no parameter with the given ID, or the parameter is not an enum,
1174    /// this will return `None`.
1175    fn get_enum(
1176        &self,
1177        unique_id: &str,
1178    ) -> Option<EnumBufferState<impl Iterator<Item = TimedValue<u32>> + Clone>> {
1179        self.enum_by_hash(hash_id(unique_id))
1180    }
1181
1182    /// Get the state of a switch parameter by it's hashed unique ID.
1183    ///
1184    /// You can get the hash of a unique ID using [`hash_id`].
1185    ///
1186    /// If there is no parameter with the given ID, or the parameter is not a switch,
1187    /// this will return `None`.
1188    fn switch_by_hash(
1189        &self,
1190        param_id: IdHash,
1191    ) -> Option<SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone>> {
1192        match self.get_by_hash(param_id) {
1193            Some(BufferState::Switch(v)) => Some(v),
1194            _ => None,
1195        }
1196    }
1197
1198    /// Get the state of a switch parameter by it's unique ID.
1199    ///
1200    /// If there is no parameter with the given ID, or the parameter is not a switch,
1201    /// this will return `None`.
1202    fn get_switch(
1203        &self,
1204        unique_id: &str,
1205    ) -> Option<SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone>> {
1206        self.switch_by_hash(hash_id(unique_id))
1207    }
1208}
1209
1210#[cfg(test)]
1211mod tests {
1212    use super::{
1213        IdHash, InternalValue, PiecewiseLinearCurve, PiecewiseLinearCurvePoint, States, hash_id,
1214    };
1215
1216    struct MyState {}
1217    impl States for MyState {
1218        fn get_by_hash(&self, param_hash: IdHash) -> Option<InternalValue> {
1219            if param_hash == hash_id("numeric") {
1220                return Some(InternalValue::Numeric(0.5));
1221            } else if param_hash == hash_id("enum") {
1222                return Some(InternalValue::Enum(2));
1223            } else if param_hash == hash_id("switch") {
1224                return Some(InternalValue::Switch(true));
1225            } else {
1226                return None;
1227            }
1228        }
1229    }
1230
1231    #[test]
1232    fn parameter_states_default_functions() {
1233        let state = MyState {};
1234        assert_eq!(state.get_numeric("numeric"), Some(0.5));
1235        assert_eq!(state.get_numeric("enum"), None);
1236        assert_eq!(state.get_enum("numeric"), None);
1237        assert_eq!(state.get_enum("enum"), Some(2));
1238        assert_eq!(state.get_switch("switch"), Some(true));
1239        assert_eq!(state.get_switch("numeric"), None);
1240    }
1241
1242    #[test]
1243    fn valid_curve() {
1244        assert!(
1245            PiecewiseLinearCurve::new(
1246                (&[
1247                    PiecewiseLinearCurvePoint {
1248                        sample_offset: 0,
1249                        value: 0.5
1250                    },
1251                    PiecewiseLinearCurvePoint {
1252                        sample_offset: 3,
1253                        value: 0.4
1254                    },
1255                    PiecewiseLinearCurvePoint {
1256                        sample_offset: 4,
1257                        value: 0.3
1258                    }
1259                ])
1260                    .iter()
1261                    .cloned(),
1262                10,
1263                0.0..=1.0
1264            )
1265            .is_some()
1266        )
1267    }
1268
1269    #[test]
1270    fn out_of_order_curve_points_rejected() {
1271        assert!(
1272            PiecewiseLinearCurve::new(
1273                (&[
1274                    PiecewiseLinearCurvePoint {
1275                        sample_offset: 0,
1276                        value: 0.5
1277                    },
1278                    PiecewiseLinearCurvePoint {
1279                        sample_offset: 4,
1280                        value: 0.4
1281                    },
1282                    PiecewiseLinearCurvePoint {
1283                        sample_offset: 3,
1284                        value: 0.3
1285                    }
1286                ])
1287                    .iter()
1288                    .cloned(),
1289                10,
1290                0.0..=1.0
1291            )
1292            .is_none()
1293        )
1294    }
1295
1296    #[test]
1297    fn empty_curves_rejected() {
1298        assert!(PiecewiseLinearCurve::new((&[]).iter().cloned(), 10, 0.0..=1.0).is_none())
1299    }
1300
1301    #[test]
1302    fn zero_length_buffers_rejected() {
1303        assert!(
1304            PiecewiseLinearCurve::new(
1305                (&[PiecewiseLinearCurvePoint {
1306                    sample_offset: 0,
1307                    value: 0.2
1308                }])
1309                    .iter()
1310                    .cloned(),
1311                0,
1312                0.0..=1.0
1313            )
1314            .is_none()
1315        )
1316    }
1317
1318    #[test]
1319    fn out_of_bounds_sample_counts_rejected() {
1320        assert!(
1321            PiecewiseLinearCurve::new(
1322                (&[
1323                    PiecewiseLinearCurvePoint {
1324                        sample_offset: 0,
1325                        value: 0.2
1326                    },
1327                    PiecewiseLinearCurvePoint {
1328                        sample_offset: 12,
1329                        value: 0.3
1330                    }
1331                ])
1332                    .iter()
1333                    .cloned(),
1334                10,
1335                0.0..=1.0
1336            )
1337            .is_none()
1338        )
1339    }
1340
1341    #[test]
1342    fn out_of_bounds_curve_values_rejected() {
1343        assert!(
1344            PiecewiseLinearCurve::new(
1345                (&[
1346                    PiecewiseLinearCurvePoint {
1347                        sample_offset: 0,
1348                        value: 0.2
1349                    },
1350                    PiecewiseLinearCurvePoint {
1351                        sample_offset: 3,
1352                        value: 1.3
1353                    }
1354                ])
1355                    .iter()
1356                    .cloned(),
1357                10,
1358                0.0..=1.0
1359            )
1360            .is_none()
1361        )
1362    }
1363
1364    #[test]
1365    fn curve_does_not_start_at_zero_rejected() {
1366        assert!(
1367            PiecewiseLinearCurve::new(
1368                (&[
1369                    PiecewiseLinearCurvePoint {
1370                        sample_offset: 3,
1371                        value: 0.5
1372                    },
1373                    PiecewiseLinearCurvePoint {
1374                        sample_offset: 6,
1375                        value: 0.4
1376                    },
1377                    PiecewiseLinearCurvePoint {
1378                        sample_offset: 7,
1379                        value: 0.3
1380                    }
1381                ])
1382                    .iter()
1383                    .cloned(),
1384                10,
1385                0.0..=1.0
1386            )
1387            .is_none()
1388        )
1389    }
1390
1391    #[test]
1392    fn curve_multiple_points_same_sample_rejected() {
1393        assert!(
1394            PiecewiseLinearCurve::new(
1395                (&[
1396                    PiecewiseLinearCurvePoint {
1397                        sample_offset: 0,
1398                        value: 0.5
1399                    },
1400                    PiecewiseLinearCurvePoint {
1401                        sample_offset: 6,
1402                        value: 0.4
1403                    },
1404                    PiecewiseLinearCurvePoint {
1405                        sample_offset: 6,
1406                        value: 0.3
1407                    }
1408                ])
1409                    .iter()
1410                    .cloned(),
1411                10,
1412                0.0..=1.0
1413            )
1414            .is_none()
1415        )
1416    }
1417}