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
468const FXHASH_SEED32: u32 = 0x2722_0a95;
469
470const fn fxhash_word(hash: u32, word: u32) -> u32 {
471    (hash.rotate_left(5) ^ word).wrapping_mul(FXHASH_SEED32)
472}
473
474/// Const reimplementation of `fxhash::hash32::<str>` for little-endian targets.
475///
476/// Replicates the exact sequence of operations that `fxhash::hash32` performs
477/// when hashing a `&str` via the `Hash` trait: `write(bytes)` then `write_u8(0xff)`,
478/// where `write` processes 4-byte little-endian chunks via `hash_word`, then
479/// remaining bytes individually.
480#[cfg(target_endian = "little")]
481const fn fxhash32_str(s: &str) -> u32 {
482    let bytes = s.as_bytes();
483    let len = bytes.len();
484    let mut hash: u32 = 0;
485    let mut i = 0;
486    while i + 4 <= len {
487        let n = (bytes[i] as u32)
488            | ((bytes[i + 1] as u32) << 8)
489            | ((bytes[i + 2] as u32) << 16)
490            | ((bytes[i + 3] as u32) << 24);
491        hash = fxhash_word(hash, n);
492        i += 4;
493    }
494    while i < len {
495        hash = fxhash_word(hash, bytes[i] as u32);
496        i += 1;
497    }
498    fxhash_word(hash, 0xff)
499}
500
501#[doc(hidden)]
502#[must_use]
503pub const fn id_hash_from_internal_hash(internal_hash: u32) -> IdHash {
504    IdHash {
505        internal_hash: internal_hash & 0x7fff_ffff,
506    }
507}
508
509impl IdHash {
510    #[doc(hidden)]
511    #[must_use]
512    pub fn internal_hash(&self) -> u32 {
513        self.internal_hash
514    }
515}
516
517/// Creates a hash from a unique ID.
518///
519/// This converts a parameter's `unique_id` into an [`IdHash`].
520///
521/// # Examples
522///
523/// ```
524/// use conformal_component::parameters::hash_id;
525/// let hash = hash_id("my_parameter");
526/// ```
527#[must_use]
528pub const fn hash_id(unique_id: &str) -> IdHash {
529    id_hash_from_internal_hash(fxhash32_str(unique_id) & 0x7fff_ffff)
530}
531
532/// Identity hasher for [`IdHash`] keys.
533///
534/// Since [`IdHash`] already contains a well-distributed hash value
535/// (from `FxHash`), re-hashing it through the default `SipHash` is
536/// redundant. This hasher passes the value through directly.
537#[derive(Default)]
538pub struct IdHashHasher(u64);
539
540impl core::hash::Hasher for IdHashHasher {
541    fn write(&mut self, _bytes: &[u8]) {
542        unreachable!()
543    }
544    fn write_u32(&mut self, i: u32) {
545        self.0 = u64::from(i);
546    }
547    fn finish(&self) -> u64 {
548        self.0
549    }
550}
551
552/// A [`HashMap`](std::collections::HashMap) using [`IdHash`] keys with an identity hasher.
553///
554/// This avoids redundant re-hashing since `IdHash` already contains a
555/// well-distributed hash value.
556pub type IdHashMap<V> =
557    std::collections::HashMap<IdHash, V, core::hash::BuildHasherDefault<IdHashHasher>>;
558
559/// A value of a parameter used in performance-critical ocntexts.
560///
561/// This is used when performance is critical and we don't want to
562/// refer to enums by their string values.
563#[derive(Debug, Clone, PartialEq, Copy)]
564pub enum InternalValue {
565    /// A numeric value.
566    Numeric(f32),
567
568    /// The _index_ of an enum value.
569    ///
570    /// This refers to the index of the current value in the `values`
571    /// array of the parameter.
572    Enum(u32),
573
574    /// A switch value.
575    Switch(bool),
576}
577
578/// A value of a parameter
579///
580/// Outside of performance-critical contexts, we use this to refer
581/// to parameter values.
582#[derive(Debug, Clone, PartialEq)]
583pub enum Value {
584    /// A numeric value.
585    Numeric(f32),
586
587    /// An enum value.
588    Enum(String),
589
590    /// A switch value.
591    Switch(bool),
592}
593
594impl From<f32> for Value {
595    fn from(v: f32) -> Self {
596        Value::Numeric(v)
597    }
598}
599
600impl From<String> for Value {
601    fn from(v: String) -> Self {
602        Value::Enum(v)
603    }
604}
605
606impl From<bool> for Value {
607    fn from(v: bool) -> Self {
608        Value::Switch(v)
609    }
610}
611
612/// Represents a snapshot of all valid parameters at a given point in time.
613///
614/// We use this trait to provide information about parameters when we are
615/// _not_ processing a buffer (for that, we use [`BufferStates`]).
616///
617/// This is passed into [`crate::synth::Synth::handle_events`] and
618/// [`crate::effect::Effect::handle_parameters`].
619///
620/// For convenience, we provide [`States::get_numeric`], [`States::get_enum`],
621/// and [`States::get_switch`] functions, which return the value of the parameter
622/// if it is of the correct type, or `None` otherwise.
623/// Note that all parmeter types re-use the same `ID` space, so only one of the
624/// specialized `get` methods will return a value for a given `ParameterID`.
625///
626/// Note that in general, the Conformal wrapper will implement this trait
627/// for you, but we provide a simple implementation called [`StatesMap`]
628/// that's appropriate to use in tests or other cases where you need to
629/// create this trait outside of a Conformal wrapper.
630pub trait States {
631    /// Get the current value of a parameter by it's hashed unique ID.
632    ///
633    /// You can get the hash of a unique ID using [`hash_id`].
634    ///
635    /// If there is no parameter with the given ID, this will return `None`.
636    fn get_by_hash(&self, id_hash: IdHash) -> Option<InternalValue>;
637
638    /// Get the current value of a parameter by it's unique ID.
639    ///
640    /// If there is no parameter with the given ID, this will return `None`.
641    fn get(&self, unique_id: &str) -> Option<InternalValue> {
642        self.get_by_hash(hash_id(unique_id))
643    }
644
645    /// Get the current numeric value of a parameter by it's hashed unique ID.
646    ///
647    /// You can get the hash of a unique ID using [`hash_id`].
648    ///
649    /// If the parameter is not present or is not numeric, this will return `None`.
650    fn numeric_by_hash(&self, id_hash: IdHash) -> Option<f32> {
651        match self.get_by_hash(id_hash) {
652            Some(InternalValue::Numeric(v)) => Some(v),
653            _ => None,
654        }
655    }
656
657    /// Get the current numeric value of a parameter by it's unique ID.
658    ///
659    /// If the parameter is not present or is not numeric, this will return `None`.
660    fn get_numeric(&self, unique_id: &str) -> Option<f32> {
661        self.numeric_by_hash(hash_id(unique_id))
662    }
663
664    /// Get the current enum value of a parameter by it's hashed unique ID.
665    ///
666    /// You can get the hash of a unique ID using [`hash_id`].
667    ///
668    /// If the parameter is not present or is not an enum, this will return `None`.
669    fn enum_by_hash(&self, id_hash: IdHash) -> Option<u32> {
670        match self.get_by_hash(id_hash) {
671            Some(InternalValue::Enum(v)) => Some(v),
672            _ => None,
673        }
674    }
675
676    /// Get the current enum value of a parameter by it's unique ID.
677    ///
678    /// If the parameter is not present or is not an enum, this will return `None`.
679    fn get_enum(&self, unique_id: &str) -> Option<u32> {
680        self.enum_by_hash(hash_id(unique_id))
681    }
682
683    /// Get the current switch value of a parameter by it's hashed unique ID.
684    ///
685    /// You can get the hash of a unique ID using [`hash_id`].
686    ///
687    /// If the parameter is not present or is not a switch, this will return `None`.
688    fn switch_by_hash(&self, id_hash: IdHash) -> Option<bool> {
689        match self.get_by_hash(id_hash) {
690            Some(InternalValue::Switch(v)) => Some(v),
691            _ => None,
692        }
693    }
694
695    /// Get the current switch value of a parameter by it's unique ID.
696    ///
697    /// If the parameter is not present or is not a switch, this will return `None`.
698    fn get_switch(&self, unique_id: &str) -> Option<bool> {
699        self.switch_by_hash(hash_id(unique_id))
700    }
701}
702
703/// Represents a single point of a piecewise linear curve.
704#[derive(Debug, Clone, Copy, PartialEq)]
705pub struct PiecewiseLinearCurvePoint {
706    /// The number of samples from the start of the buffer this point occurs at.
707    pub sample_offset: usize,
708
709    /// The value of the curve at this point.
710    pub value: f32,
711}
712
713/// Represents a numeric value that changes over the course of the buffer.
714///
715/// We represent values changing over the course of the buffer as a piecewise
716/// linear curve, where the curve moving linearly from point to point.
717///
718/// Note that the curve is _guaranteed_ to begin at 0, however it
719/// may end before the end of the buffer - in this case, the value
720/// remains constant until the end of the buffer.
721///
722/// Some invariants:
723///  - There will always be at least one point
724///  - The first point's `sample_offset` will be 0
725///  - `sample_offset`s will be monotonically increasing and only one
726///    point will appear for each `sample_offset`
727///  - All point's `value` will be between the parameter's `min` and `max`
728#[derive(Clone)]
729pub struct PiecewiseLinearCurve<I> {
730    points: I,
731
732    buffer_size: usize,
733}
734
735trait ValueAndSampleOffset<V> {
736    fn value(&self) -> &V;
737    fn sample_offset(&self) -> usize;
738}
739
740impl ValueAndSampleOffset<f32> for PiecewiseLinearCurvePoint {
741    fn value(&self) -> &f32 {
742        &self.value
743    }
744
745    fn sample_offset(&self) -> usize {
746        self.sample_offset
747    }
748}
749
750fn check_curve_invariants<
751    V: PartialOrd + PartialEq + core::fmt::Debug,
752    P: ValueAndSampleOffset<V>,
753    I: Iterator<Item = P>,
754>(
755    iter: I,
756    buffer_size: usize,
757    valid_range: impl RangeBounds<V>,
758) -> bool {
759    let mut last_sample_offset = None;
760    for point in iter {
761        if point.sample_offset() >= buffer_size {
762            return false;
763        }
764        if let Some(last) = last_sample_offset {
765            if point.sample_offset() <= last {
766                return false;
767            }
768        } else if point.sample_offset() != 0 {
769            return false;
770        }
771        if !valid_range.contains(point.value()) {
772            return false;
773        }
774        last_sample_offset = Some(point.sample_offset());
775    }
776    last_sample_offset.is_some()
777}
778
779impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint> + Clone> PiecewiseLinearCurve<I> {
780    /// Construct a new [`PiecewiseLinearCurve`] from an iterator of points.
781    ///
782    /// This will check the invariants for the curve, and if any are invalid, this will
783    /// return `None`.
784    ///
785    /// # Examples
786    ///
787    /// ```
788    /// # use conformal_component::parameters::{PiecewiseLinearCurve, PiecewiseLinearCurvePoint};
789    /// assert!(PiecewiseLinearCurve::new(
790    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
791    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 }],
792    ///   128,
793    ///   0.0..=1.0,
794    /// ).is_some());
795    ///
796    /// // Curves must include at least one point
797    /// assert!(PiecewiseLinearCurve::new(vec![], 128, 0.0..=1.0).is_none());
798    ///
799    /// // Curves can't go outside the valid range.
800    /// assert!(PiecewiseLinearCurve::new(
801    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
802    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 2.0 }],
803    ///   128,
804    ///   0.0..=1.0,
805    /// ).is_none());
806    ///
807    /// // The curve must not go past the end of the buffer
808    /// assert!(PiecewiseLinearCurve::new(
809    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
810    ///        PiecewiseLinearCurvePoint { sample_offset: 128, value: 1.0 }],
811    ///   128,
812    ///   0.0..=1.0,
813    /// ).is_none());
814    ///
815    /// // The first point must be at 0
816    /// assert!(PiecewiseLinearCurve::new(
817    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 50, value: 0.0 },
818    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 }],
819    ///   128,
820    ///   0.0..=1.0,
821    /// ).is_none());
822    ///
823    /// // Sample offsets must monotonically increase
824    /// assert!(PiecewiseLinearCurve::new(
825    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.0 },
826    ///        PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 },
827    ///        PiecewiseLinearCurvePoint { sample_offset: 50, value: 0.5 }],
828    ///   128,
829    ///   0.0..=1.0,
830    /// ).is_none());
831    /// ```
832    pub fn new(points: I, buffer_size: usize, valid_range: RangeInclusive<f32>) -> Option<Self> {
833        if buffer_size == 0 {
834            return None;
835        }
836        if check_curve_invariants(points.clone().into_iter(), buffer_size, valid_range) {
837            Some(Self {
838                points,
839                buffer_size,
840            })
841        } else {
842            None
843        }
844    }
845
846    #[doc(hidden)]
847    pub unsafe fn from_parts_unchecked(points: I, buffer_size: usize) -> Self {
848        Self {
849            points,
850            buffer_size,
851        }
852    }
853}
854
855impl<I> PiecewiseLinearCurve<I> {
856    /// Get the size of the buffer this curve is defined over.
857    ///
858    /// Note that the last point may occur _before_ the end of the buffer,
859    /// in which case the value remains constant from that point until the
860    /// end of the buffer.
861    pub fn buffer_size(&self) -> usize {
862        self.buffer_size
863    }
864}
865
866impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> IntoIterator for PiecewiseLinearCurve<I> {
867    type Item = PiecewiseLinearCurvePoint;
868    type IntoIter = I::IntoIter;
869
870    fn into_iter(self) -> Self::IntoIter {
871        self.points.into_iter()
872    }
873}
874
875/// Represents a value at a specific point in time in a buffer.
876#[derive(Debug, Clone, PartialEq)]
877pub struct TimedValue<V> {
878    /// The number of samples from the start of the buffer.
879    pub sample_offset: usize,
880
881    /// The value at this point in time.
882    pub value: V,
883}
884
885impl<V> ValueAndSampleOffset<V> for TimedValue<V> {
886    fn value(&self) -> &V {
887        &self.value
888    }
889
890    fn sample_offset(&self) -> usize {
891        self.sample_offset
892    }
893}
894
895/// Represents an enum 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///  - All point's `value` will be valid
906#[derive(Clone)]
907pub struct TimedEnumValues<I> {
908    points: I,
909    buffer_size: usize,
910}
911
912impl<I: IntoIterator<Item = TimedValue<u32>> + Clone> TimedEnumValues<I> {
913    /// Construct a new [`TimedEnumValues`] from an iterator of points.
914    ///
915    /// This will check the invariants for the curve, and if any are invalid, this will
916    /// return `None`.
917    ///
918    /// Note that here we refer to the enum by the _index_ of the value,
919    /// that is, the index of the value in the `values` array of the parameter.
920    ///
921    /// # Examples
922    ///
923    /// ```
924    /// # use conformal_component::parameters::{TimedEnumValues, TimedValue};
925    /// assert!(TimedEnumValues::new(
926    ///   vec![TimedValue { sample_offset: 0, value: 0 },
927    ///        TimedValue { sample_offset: 100, value: 1 }],
928    ///   128,
929    ///   0..2,
930    /// ).is_some());
931    /// ```
932    pub fn new(points: I, buffer_size: usize, valid_range: Range<u32>) -> Option<Self> {
933        if buffer_size == 0 {
934            return None;
935        }
936        if check_curve_invariants(points.clone().into_iter(), buffer_size, valid_range) {
937            Some(Self {
938                points,
939                buffer_size,
940            })
941        } else {
942            None
943        }
944    }
945}
946
947impl<I> TimedEnumValues<I> {
948    /// Get the size of the buffer this curve is defined over.
949    pub fn buffer_size(&self) -> usize {
950        self.buffer_size
951    }
952}
953
954impl<I: IntoIterator<Item = TimedValue<u32>>> IntoIterator for TimedEnumValues<I> {
955    type Item = TimedValue<u32>;
956    type IntoIter = I::IntoIter;
957
958    fn into_iter(self) -> Self::IntoIter {
959        self.points.into_iter()
960    }
961}
962
963/// Represents a switched value that changes over the course of a buffer.
964///
965/// Each point represents a change in value at a given sample offset -
966/// the value remains constant until the next point (or the end of the buffer)
967///
968/// Some invariants:
969///  - There will always be at least one point
970///  - The first point's `sample_offset` will be 0
971///  - `sample_offset`s will be monotonically increasing and only one
972///    point will appear for each `sample_offset`
973#[derive(Clone)]
974pub struct TimedSwitchValues<I> {
975    points: I,
976    buffer_size: usize,
977}
978
979impl<I: IntoIterator<Item = TimedValue<bool>> + Clone> TimedSwitchValues<I> {
980    /// Construct a new [`TimedSwitchValues`] from an iterator of points.
981    ///
982    /// This will check the invariants for the curve, and if any are invalid, this will
983    /// return `None`.
984    ///
985    /// # Examples
986    ///
987    /// ```
988    /// # use conformal_component::parameters::{TimedSwitchValues, TimedValue};
989    /// assert!(TimedSwitchValues::new(
990    ///   vec![TimedValue { sample_offset: 0, value: false },
991    ///        TimedValue { sample_offset: 100, value: true }],
992    ///   128,
993    /// ).is_some());
994    /// ```
995    pub fn new(points: I, buffer_size: usize) -> Option<Self> {
996        if buffer_size == 0 {
997            return None;
998        }
999        if check_curve_invariants(points.clone().into_iter(), buffer_size, false..=true) {
1000            Some(Self {
1001                points,
1002                buffer_size,
1003            })
1004        } else {
1005            None
1006        }
1007    }
1008}
1009
1010impl<I> TimedSwitchValues<I> {
1011    /// Get the size of the buffer this curve is defined over.
1012    pub fn buffer_size(&self) -> usize {
1013        self.buffer_size
1014    }
1015}
1016
1017impl<I: IntoIterator<Item = TimedValue<bool>>> IntoIterator for TimedSwitchValues<I> {
1018    type Item = TimedValue<bool>;
1019    type IntoIter = I::IntoIter;
1020
1021    fn into_iter(self) -> Self::IntoIter {
1022        self.points.into_iter()
1023    }
1024}
1025
1026/// Represents the state of a numeric value across a buffer
1027#[derive(Clone)]
1028pub enum NumericBufferState<I> {
1029    /// The value is constant across the buffer.
1030    Constant(f32),
1031
1032    /// The value changes over the course of the buffer, represented by a
1033    /// [`PiecewiseLinearCurve`].
1034    PiecewiseLinear(PiecewiseLinearCurve<I>),
1035}
1036
1037impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> NumericBufferState<I> {
1038    /// Get the value of the parameter at the start of the buffer.
1039    ///
1040    /// # Examples
1041    ///
1042    /// ```
1043    /// # use conformal_component::parameters::{NumericBufferState, PiecewiseLinearCurve, PiecewiseLinearCurvePoint};
1044    /// assert_eq!(NumericBufferState::PiecewiseLinear(PiecewiseLinearCurve::new(
1045    ///   vec![PiecewiseLinearCurvePoint { sample_offset: 0, value: 0.5 },
1046    ///       PiecewiseLinearCurvePoint { sample_offset: 100, value: 1.0 }],
1047    ///   128,
1048    ///   0.0..=1.0,
1049    /// ).unwrap()).value_at_start_of_buffer(), 0.5);
1050    /// ```
1051    #[allow(clippy::missing_panics_doc)] // Only panics when invariants are broken.
1052    pub fn value_at_start_of_buffer(self) -> f32 {
1053        match self {
1054            NumericBufferState::Constant(v) => v,
1055            NumericBufferState::PiecewiseLinear(v) => v.points.into_iter().next().unwrap().value,
1056        }
1057    }
1058}
1059
1060/// Represents the state of an enum value across a buffer
1061///
1062/// Here we refer to the enum by the _index_ of the value,
1063/// that is, the index of the value in the `values` array of the parameter.
1064#[derive(Clone)]
1065pub enum EnumBufferState<I> {
1066    /// The value is constant across the buffer.
1067    Constant(u32),
1068
1069    /// The value changes over the course of the buffer, represented by a
1070    /// [`TimedEnumValues`].
1071    Varying(TimedEnumValues<I>),
1072}
1073
1074impl<I: IntoIterator<Item = TimedValue<u32>>> EnumBufferState<I> {
1075    /// Get the value of the parameter at the start of the buffer,
1076    /// represented by the index of the value in the `values` array of the parameter.
1077    ///
1078    /// # Examples
1079    ///
1080    /// ```
1081    /// # use conformal_component::parameters::{EnumBufferState, TimedEnumValues, TimedValue};
1082    /// assert_eq!(EnumBufferState::Varying(TimedEnumValues::new(
1083    ///   vec![TimedValue { sample_offset: 0, value: 1 },
1084    ///        TimedValue { sample_offset: 100, value: 2 }],
1085    ///   128,
1086    ///   0..3
1087    /// ).unwrap()).value_at_start_of_buffer(), 1);
1088    /// ```
1089    #[allow(clippy::missing_panics_doc)] // Only panics when invariants are broken.
1090    pub fn value_at_start_of_buffer(self) -> u32 {
1091        match self {
1092            EnumBufferState::Constant(v) => v,
1093            EnumBufferState::Varying(v) => v.points.into_iter().next().unwrap().value,
1094        }
1095    }
1096}
1097
1098/// Represents the state of an switched value across a buffer
1099#[derive(Clone)]
1100pub enum SwitchBufferState<I> {
1101    /// The value is constant across the buffer.
1102    Constant(bool),
1103
1104    /// The value changes over the course of the buffer, represented by a
1105    /// [`TimedSwitchValues`].
1106    Varying(TimedSwitchValues<I>),
1107}
1108
1109impl<I: IntoIterator<Item = TimedValue<bool>>> SwitchBufferState<I> {
1110    /// Get the value of the parameter at the start of the buffer.
1111    ///
1112    /// # Examples
1113    ///
1114    /// ```
1115    /// # use conformal_component::parameters::{SwitchBufferState, TimedSwitchValues, TimedValue};
1116    /// assert_eq!(SwitchBufferState::Varying(TimedSwitchValues::new(
1117    ///   vec![TimedValue { sample_offset: 0, value: true },
1118    ///        TimedValue { sample_offset: 100, value: false }],
1119    ///   128,
1120    /// ).unwrap()).value_at_start_of_buffer(), true);
1121    /// ```
1122    #[allow(clippy::missing_panics_doc)] // Only panics when invariants are broken.
1123    pub fn value_at_start_of_buffer(self) -> bool {
1124        match self {
1125            SwitchBufferState::Constant(v) => v,
1126            SwitchBufferState::Varying(v) => v.points.into_iter().next().unwrap().value,
1127        }
1128    }
1129}
1130
1131/// Represents the value of a parameter as it varies across a buffer.
1132pub enum BufferState<N, E, S> {
1133    /// The value of a numeric parameter represented by a [`NumericBufferState`].
1134    Numeric(NumericBufferState<N>),
1135
1136    /// The value of an enum parameter represented by a [`EnumBufferState`].
1137    Enum(EnumBufferState<E>),
1138
1139    /// The value of a switch parameter represented by a [`SwitchBufferState`].
1140    Switch(SwitchBufferState<S>),
1141}
1142
1143/// Represents the state of several parameters across a buffer.
1144///
1145/// Each parameter is represented by a [`BufferState`], which represents
1146/// a value for that parameter at each sample of the buffer.
1147///
1148/// To easily process parameters from this struct, you can use the
1149/// [`crate::pzip`] macro, which converts a [`BufferStates`] into a per-sample
1150/// iterator containing the values of each parameter you want to look at.
1151///
1152/// For more low-level usages, you can deal directly with the underlying [`BufferState`]
1153/// objects, which might yield higher performance in some cases than the [`crate::pzip`] macro.
1154///
1155/// Most of the time, this trait will be provided by the Conformal framework.
1156/// However, we provide simple implementations for this trait for testing or
1157/// in other scenarios where you need to call process functions outside of
1158/// Conformal.
1159///
1160///  - [`ConstantBufferStates`] - A simple implementation where all parameters are constant.
1161///  - [`RampedStatesMap`] - A simple implementation where the parameter can be different at
1162///    the start and end of the buffer.
1163pub trait BufferStates {
1164    /// Get the state of a parameter by it's hashed unique ID.
1165    ///
1166    /// You can get the hash of a unique ID using [`hash_id`].
1167    ///
1168    /// If there is no parameter with the given ID, this will return `None`.
1169    fn get_by_hash(
1170        &self,
1171        id_hash: IdHash,
1172    ) -> Option<
1173        BufferState<
1174            impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
1175            impl Iterator<Item = TimedValue<u32>> + Clone,
1176            impl Iterator<Item = TimedValue<bool>> + Clone,
1177        >,
1178    >;
1179
1180    /// Get the state of a parameter by it's unique ID.
1181    ///
1182    /// If there is no parameter with the given ID, this will return `None`.
1183    fn get(
1184        &self,
1185        unique_id: &str,
1186    ) -> Option<
1187        BufferState<
1188            impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
1189            impl Iterator<Item = TimedValue<u32>> + Clone,
1190            impl Iterator<Item = TimedValue<bool>> + Clone,
1191        >,
1192    > {
1193        self.get_by_hash(hash_id(unique_id))
1194    }
1195
1196    /// Get the state of a numeric parameter by it's hashed unique ID.
1197    ///
1198    /// You can get the hash of a unique ID using [`hash_id`].
1199    ///
1200    /// If there is no parameter with the given ID, or the parameter is not numeric,
1201    /// this will return `None`.
1202    fn numeric_by_hash(
1203        &self,
1204        param_id: IdHash,
1205    ) -> Option<NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>> {
1206        match self.get_by_hash(param_id) {
1207            Some(BufferState::Numeric(v)) => Some(v),
1208            _ => None,
1209        }
1210    }
1211
1212    /// Get the state of a numeric parameter by it's unique ID.
1213    ///
1214    /// If there is no parameter with the given ID, or the parameter is not numeric,
1215    /// this will return `None`.
1216    fn get_numeric(
1217        &self,
1218        unique_id: &str,
1219    ) -> Option<NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>> {
1220        self.numeric_by_hash(hash_id(unique_id))
1221    }
1222
1223    /// Get the state of an enum parameter by it's hashed unique ID.
1224    ///
1225    /// You can get the hash of a unique ID using [`hash_id`].
1226    ///
1227    /// If there is no parameter with the given ID, or the parameter is not an enum,
1228    /// this will return `None`.
1229    fn enum_by_hash(
1230        &self,
1231        param_id: IdHash,
1232    ) -> Option<EnumBufferState<impl Iterator<Item = TimedValue<u32>> + Clone>> {
1233        match self.get_by_hash(param_id) {
1234            Some(BufferState::Enum(v)) => Some(v),
1235            _ => None,
1236        }
1237    }
1238
1239    /// Get the state of an enum parameter by it's unique ID.
1240    ///
1241    /// If there is no parameter with the given ID, or the parameter is not an enum,
1242    /// this will return `None`.
1243    fn get_enum(
1244        &self,
1245        unique_id: &str,
1246    ) -> Option<EnumBufferState<impl Iterator<Item = TimedValue<u32>> + Clone>> {
1247        self.enum_by_hash(hash_id(unique_id))
1248    }
1249
1250    /// Get the state of a switch parameter by it's hashed unique ID.
1251    ///
1252    /// You can get the hash of a unique ID using [`hash_id`].
1253    ///
1254    /// If there is no parameter with the given ID, or the parameter is not a switch,
1255    /// this will return `None`.
1256    fn switch_by_hash(
1257        &self,
1258        param_id: IdHash,
1259    ) -> Option<SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone>> {
1260        match self.get_by_hash(param_id) {
1261            Some(BufferState::Switch(v)) => Some(v),
1262            _ => None,
1263        }
1264    }
1265
1266    /// Get the state of a switch parameter by it's unique ID.
1267    ///
1268    /// If there is no parameter with the given ID, or the parameter is not a switch,
1269    /// this will return `None`.
1270    fn get_switch(
1271        &self,
1272        unique_id: &str,
1273    ) -> Option<SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone>> {
1274        self.switch_by_hash(hash_id(unique_id))
1275    }
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280    use super::{
1281        IdHash, InternalValue, PiecewiseLinearCurve, PiecewiseLinearCurvePoint, States, hash_id,
1282    };
1283
1284    struct MyState {}
1285    impl States for MyState {
1286        fn get_by_hash(&self, param_hash: IdHash) -> Option<InternalValue> {
1287            if param_hash == hash_id("numeric") {
1288                return Some(InternalValue::Numeric(0.5));
1289            } else if param_hash == hash_id("enum") {
1290                return Some(InternalValue::Enum(2));
1291            } else if param_hash == hash_id("switch") {
1292                return Some(InternalValue::Switch(true));
1293            } else {
1294                return None;
1295            }
1296        }
1297    }
1298
1299    #[test]
1300    fn parameter_states_default_functions() {
1301        let state = MyState {};
1302        assert_eq!(state.get_numeric("numeric"), Some(0.5));
1303        assert_eq!(state.get_numeric("enum"), None);
1304        assert_eq!(state.get_enum("numeric"), None);
1305        assert_eq!(state.get_enum("enum"), Some(2));
1306        assert_eq!(state.get_switch("switch"), Some(true));
1307        assert_eq!(state.get_switch("numeric"), None);
1308    }
1309
1310    #[test]
1311    fn valid_curve() {
1312        assert!(
1313            PiecewiseLinearCurve::new(
1314                (&[
1315                    PiecewiseLinearCurvePoint {
1316                        sample_offset: 0,
1317                        value: 0.5
1318                    },
1319                    PiecewiseLinearCurvePoint {
1320                        sample_offset: 3,
1321                        value: 0.4
1322                    },
1323                    PiecewiseLinearCurvePoint {
1324                        sample_offset: 4,
1325                        value: 0.3
1326                    }
1327                ])
1328                    .iter()
1329                    .cloned(),
1330                10,
1331                0.0..=1.0
1332            )
1333            .is_some()
1334        )
1335    }
1336
1337    #[test]
1338    fn out_of_order_curve_points_rejected() {
1339        assert!(
1340            PiecewiseLinearCurve::new(
1341                (&[
1342                    PiecewiseLinearCurvePoint {
1343                        sample_offset: 0,
1344                        value: 0.5
1345                    },
1346                    PiecewiseLinearCurvePoint {
1347                        sample_offset: 4,
1348                        value: 0.4
1349                    },
1350                    PiecewiseLinearCurvePoint {
1351                        sample_offset: 3,
1352                        value: 0.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 empty_curves_rejected() {
1366        assert!(PiecewiseLinearCurve::new((&[]).iter().cloned(), 10, 0.0..=1.0).is_none())
1367    }
1368
1369    #[test]
1370    fn zero_length_buffers_rejected() {
1371        assert!(
1372            PiecewiseLinearCurve::new(
1373                (&[PiecewiseLinearCurvePoint {
1374                    sample_offset: 0,
1375                    value: 0.2
1376                }])
1377                    .iter()
1378                    .cloned(),
1379                0,
1380                0.0..=1.0
1381            )
1382            .is_none()
1383        )
1384    }
1385
1386    #[test]
1387    fn out_of_bounds_sample_counts_rejected() {
1388        assert!(
1389            PiecewiseLinearCurve::new(
1390                (&[
1391                    PiecewiseLinearCurvePoint {
1392                        sample_offset: 0,
1393                        value: 0.2
1394                    },
1395                    PiecewiseLinearCurvePoint {
1396                        sample_offset: 12,
1397                        value: 0.3
1398                    }
1399                ])
1400                    .iter()
1401                    .cloned(),
1402                10,
1403                0.0..=1.0
1404            )
1405            .is_none()
1406        )
1407    }
1408
1409    #[test]
1410    fn out_of_bounds_curve_values_rejected() {
1411        assert!(
1412            PiecewiseLinearCurve::new(
1413                (&[
1414                    PiecewiseLinearCurvePoint {
1415                        sample_offset: 0,
1416                        value: 0.2
1417                    },
1418                    PiecewiseLinearCurvePoint {
1419                        sample_offset: 3,
1420                        value: 1.3
1421                    }
1422                ])
1423                    .iter()
1424                    .cloned(),
1425                10,
1426                0.0..=1.0
1427            )
1428            .is_none()
1429        )
1430    }
1431
1432    #[test]
1433    fn curve_does_not_start_at_zero_rejected() {
1434        assert!(
1435            PiecewiseLinearCurve::new(
1436                (&[
1437                    PiecewiseLinearCurvePoint {
1438                        sample_offset: 3,
1439                        value: 0.5
1440                    },
1441                    PiecewiseLinearCurvePoint {
1442                        sample_offset: 6,
1443                        value: 0.4
1444                    },
1445                    PiecewiseLinearCurvePoint {
1446                        sample_offset: 7,
1447                        value: 0.3
1448                    }
1449                ])
1450                    .iter()
1451                    .cloned(),
1452                10,
1453                0.0..=1.0
1454            )
1455            .is_none()
1456        )
1457    }
1458
1459    #[test]
1460    fn curve_multiple_points_same_sample_rejected() {
1461        assert!(
1462            PiecewiseLinearCurve::new(
1463                (&[
1464                    PiecewiseLinearCurvePoint {
1465                        sample_offset: 0,
1466                        value: 0.5
1467                    },
1468                    PiecewiseLinearCurvePoint {
1469                        sample_offset: 6,
1470                        value: 0.4
1471                    },
1472                    PiecewiseLinearCurvePoint {
1473                        sample_offset: 6,
1474                        value: 0.3
1475                    }
1476                ])
1477                    .iter()
1478                    .cloned(),
1479                10,
1480                0.0..=1.0
1481            )
1482            .is_none()
1483        )
1484    }
1485
1486    #[test]
1487    fn const_hash_matches_fxhash() {
1488        use super::fxhash32_str;
1489        for s in [
1490            "",
1491            "a",
1492            "ab",
1493            "abc",
1494            "gain",
1495            "abcde",
1496            "bypass",
1497            "abcdefgh",
1498            "my special switch",
1499        ] {
1500            assert_eq!(
1501                fxhash32_str(s),
1502                fxhash::hash32(s),
1503                "const hash mismatch for '{s}'"
1504            );
1505        }
1506    }
1507
1508    const _: () = assert!(hash_id("gain").internal_hash != 0);
1509}