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}