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}