use std::{
ops::{Range, RangeBounds, RangeInclusive},
string::ToString,
};
mod utils;
pub use utils::*;
macro_rules! info_enum_doc {
() => {
"Information specific to an enum parameter."
};
}
macro_rules! info_enum_default_doc {
() => {
"Index of the default value.
Note that this _must_ be less than the length of `values`."
};
}
macro_rules! info_enum_values_doc {
() => {
"A list of possible values for the parameter.
Note that values _must_ contain at least 2 elements."
};
}
macro_rules! info_numeric_doc {
() => {
"Information specific to a numeric parameter."
};
}
macro_rules! info_numeric_default_doc {
() => {
"The default value of the parameter.
This value _must_ be within the `valid_range`."
};
}
macro_rules! info_numeric_valid_range_doc {
() => {
"The valid range of the parameter."
};
}
macro_rules! info_numeric_units_doc {
() => {
"The units of the parameter.
Here an empty string indicates unitless values, while a non-empty string
indicates the logical units of a parmater, e.g., \"hz\""
};
}
macro_rules! info_switch_doc {
() => {
"Information specific to a switch parameter."
};
}
macro_rules! info_switch_default_doc {
() => {
"The default value of the parameter."
};
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSpecificInfoRef<'a, S> {
#[doc = info_enum_doc!()]
Enum {
#[doc = info_enum_default_doc!()]
default: u32,
#[doc = info_enum_values_doc!()]
values: &'a [S],
},
#[doc = info_numeric_doc!()]
Numeric {
#[doc = info_numeric_default_doc!()]
default: f32,
#[doc = info_numeric_valid_range_doc!()]
valid_range: RangeInclusive<f32>,
#[doc = info_numeric_units_doc!()]
units: Option<&'a str>,
},
#[doc = info_switch_doc!()]
Switch {
#[doc = info_switch_default_doc!()]
default: bool,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSpecificInfo {
#[doc = info_enum_doc!()]
Enum {
#[doc = info_enum_default_doc!()]
default: u32,
#[doc = info_enum_values_doc!()]
values: Vec<String>,
},
#[doc = info_numeric_doc!()]
Numeric {
#[doc = info_numeric_default_doc!()]
default: f32,
#[doc = info_numeric_valid_range_doc!()]
valid_range: std::ops::RangeInclusive<f32>,
#[doc = info_numeric_units_doc!()]
units: Option<String>,
},
#[doc = info_switch_doc!()]
Switch {
#[doc = info_switch_default_doc!()]
default: bool,
},
}
impl<'a, S: AsRef<str>> From<&'a TypeSpecificInfoRef<'a, S>> for TypeSpecificInfo {
fn from(v: &'a TypeSpecificInfoRef<'a, S>) -> Self {
match v {
TypeSpecificInfoRef::Enum { default, values } => {
let values: Vec<String> = values.iter().map(|s| s.as_ref().to_string()).collect();
assert!(values.len() < i32::MAX as usize);
TypeSpecificInfo::Enum {
default: *default,
values,
}
}
TypeSpecificInfoRef::Numeric {
default,
valid_range,
units,
} => TypeSpecificInfo::Numeric {
default: *default,
valid_range: valid_range.clone(),
units: (*units).map(ToString::to_string),
},
TypeSpecificInfoRef::Switch { default } => {
TypeSpecificInfo::Switch { default: *default }
}
}
}
}
impl<'a> From<&'a TypeSpecificInfo> for TypeSpecificInfoRef<'a, String> {
fn from(v: &'a TypeSpecificInfo) -> Self {
match v {
TypeSpecificInfo::Enum { default, values } => TypeSpecificInfoRef::Enum {
default: *default,
values: values.as_slice(),
},
TypeSpecificInfo::Numeric {
default,
valid_range,
units,
} => TypeSpecificInfoRef::Numeric {
default: *default,
valid_range: valid_range.clone(),
units: units.as_ref().map(String::as_str),
},
TypeSpecificInfo::Switch { default } => {
TypeSpecificInfoRef::Switch { default: *default }
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Flags {
pub automatable: bool,
}
impl Default for Flags {
fn default() -> Self {
Flags { automatable: true }
}
}
pub const UNIQUE_ID_INTERNAL_PREFIX: &str = "_conformal_internal_";
macro_rules! unique_id_doc {
() => {
"The unique ID of the parameter.
As the name implies, each parameter's id must be unique within
the comonent's parameters.
Note that this ID will not be presented to the user, it is only
used to refer to the parameter in code.
The ID must not begin with the prefix `_conformal_internal`, as
this is reserved for use by the Conformal library itself."
};
}
macro_rules! title_doc {
() => {
"Human-readable title of the parameter."
};
}
macro_rules! short_title_doc {
() => {
"A short title of the parameter.
In some hosting applications, this may appear as an
abbreviated version of the title. If the title is already
short, it's okay to use the same value for `title` and `short_title`."
};
}
macro_rules! flags_doc {
() => {
"Metadata about the parameter"
};
}
macro_rules! type_specific_doc {
() => {
"Information specific to the type of parameter."
};
}
#[derive(Debug, Clone, PartialEq)]
pub struct InfoRef<'a, S> {
#[doc = unique_id_doc!()]
pub unique_id: &'a str,
#[doc = title_doc!()]
pub title: &'a str,
#[doc = short_title_doc!()]
pub short_title: &'a str,
#[doc = flags_doc!()]
pub flags: Flags,
#[doc = type_specific_doc!()]
pub type_specific: TypeSpecificInfoRef<'a, S>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Info {
#[doc = unique_id_doc!()]
pub unique_id: String,
#[doc = title_doc!()]
pub title: String,
#[doc = short_title_doc!()]
pub short_title: String,
#[doc = flags_doc!()]
pub flags: Flags,
#[doc = type_specific_doc!()]
pub type_specific: TypeSpecificInfo,
}
impl<'a, S: AsRef<str>> From<&'a InfoRef<'a, S>> for Info {
fn from(v: &'a InfoRef<'a, S>) -> Self {
Info {
title: v.title.to_string(),
short_title: v.short_title.to_string(),
unique_id: v.unique_id.to_string(),
flags: v.flags.clone(),
type_specific: (&v.type_specific).into(),
}
}
}
impl<'a> From<&'a Info> for InfoRef<'a, String> {
fn from(v: &'a Info) -> Self {
InfoRef {
title: &v.title,
short_title: &v.short_title,
unique_id: &v.unique_id,
flags: v.flags.clone(),
type_specific: (&v.type_specific).into(),
}
}
}
pub type StaticInfoRef = InfoRef<'static, &'static str>;
pub fn to_infos(v: &[InfoRef<'_, &'_ str>]) -> Vec<Info> {
v.iter().map(Into::into).collect()
}
#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
pub struct IdHash {
internal_hash: u32,
}
const FXHASH_SEED32: u32 = 0x2722_0a95;
const fn fxhash_word(hash: u32, word: u32) -> u32 {
(hash.rotate_left(5) ^ word).wrapping_mul(FXHASH_SEED32)
}
#[cfg(target_endian = "little")]
const fn fxhash32_str(s: &str) -> u32 {
let bytes = s.as_bytes();
let len = bytes.len();
let mut hash: u32 = 0;
let mut i = 0;
while i + 4 <= len {
let n = (bytes[i] as u32)
| ((bytes[i + 1] as u32) << 8)
| ((bytes[i + 2] as u32) << 16)
| ((bytes[i + 3] as u32) << 24);
hash = fxhash_word(hash, n);
i += 4;
}
while i < len {
hash = fxhash_word(hash, bytes[i] as u32);
i += 1;
}
fxhash_word(hash, 0xff)
}
#[doc(hidden)]
#[must_use]
pub const fn id_hash_from_internal_hash(internal_hash: u32) -> IdHash {
IdHash {
internal_hash: internal_hash & 0x7fff_ffff,
}
}
impl IdHash {
#[doc(hidden)]
#[must_use]
pub fn internal_hash(&self) -> u32 {
self.internal_hash
}
}
#[must_use]
pub const fn hash_id(unique_id: &str) -> IdHash {
id_hash_from_internal_hash(fxhash32_str(unique_id) & 0x7fff_ffff)
}
#[derive(Default)]
pub struct IdHashHasher(u64);
impl core::hash::Hasher for IdHashHasher {
fn write(&mut self, _bytes: &[u8]) {
unreachable!()
}
fn write_u32(&mut self, i: u32) {
self.0 = u64::from(i);
}
fn finish(&self) -> u64 {
self.0
}
}
pub type IdHashMap<V> =
std::collections::HashMap<IdHash, V, core::hash::BuildHasherDefault<IdHashHasher>>;
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum InternalValue {
Numeric(f32),
Enum(u32),
Switch(bool),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Numeric(f32),
Enum(String),
Switch(bool),
}
impl From<f32> for Value {
fn from(v: f32) -> Self {
Value::Numeric(v)
}
}
impl From<String> for Value {
fn from(v: String) -> Self {
Value::Enum(v)
}
}
impl From<bool> for Value {
fn from(v: bool) -> Self {
Value::Switch(v)
}
}
pub trait States {
fn get_by_hash(&self, id_hash: IdHash) -> Option<InternalValue>;
fn get(&self, unique_id: &str) -> Option<InternalValue> {
self.get_by_hash(hash_id(unique_id))
}
fn numeric_by_hash(&self, id_hash: IdHash) -> Option<f32> {
match self.get_by_hash(id_hash) {
Some(InternalValue::Numeric(v)) => Some(v),
_ => None,
}
}
fn get_numeric(&self, unique_id: &str) -> Option<f32> {
self.numeric_by_hash(hash_id(unique_id))
}
fn enum_by_hash(&self, id_hash: IdHash) -> Option<u32> {
match self.get_by_hash(id_hash) {
Some(InternalValue::Enum(v)) => Some(v),
_ => None,
}
}
fn get_enum(&self, unique_id: &str) -> Option<u32> {
self.enum_by_hash(hash_id(unique_id))
}
fn switch_by_hash(&self, id_hash: IdHash) -> Option<bool> {
match self.get_by_hash(id_hash) {
Some(InternalValue::Switch(v)) => Some(v),
_ => None,
}
}
fn get_switch(&self, unique_id: &str) -> Option<bool> {
self.switch_by_hash(hash_id(unique_id))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PiecewiseLinearCurvePoint {
pub sample_offset: usize,
pub value: f32,
}
#[derive(Clone)]
pub struct PiecewiseLinearCurve<I> {
points: I,
buffer_size: usize,
}
trait ValueAndSampleOffset<V> {
fn value(&self) -> &V;
fn sample_offset(&self) -> usize;
}
impl ValueAndSampleOffset<f32> for PiecewiseLinearCurvePoint {
fn value(&self) -> &f32 {
&self.value
}
fn sample_offset(&self) -> usize {
self.sample_offset
}
}
fn check_curve_invariants<
V: PartialOrd + PartialEq + core::fmt::Debug,
P: ValueAndSampleOffset<V>,
I: Iterator<Item = P>,
>(
iter: I,
buffer_size: usize,
valid_range: impl RangeBounds<V>,
) -> bool {
let mut last_sample_offset = None;
for point in iter {
if point.sample_offset() >= buffer_size {
return false;
}
if let Some(last) = last_sample_offset {
if point.sample_offset() <= last {
return false;
}
} else if point.sample_offset() != 0 {
return false;
}
if !valid_range.contains(point.value()) {
return false;
}
last_sample_offset = Some(point.sample_offset());
}
last_sample_offset.is_some()
}
impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint> + Clone> PiecewiseLinearCurve<I> {
pub fn new(points: I, buffer_size: usize, valid_range: RangeInclusive<f32>) -> Option<Self> {
if buffer_size == 0 {
return None;
}
if check_curve_invariants(points.clone().into_iter(), buffer_size, valid_range) {
Some(Self {
points,
buffer_size,
})
} else {
None
}
}
#[doc(hidden)]
pub unsafe fn from_parts_unchecked(points: I, buffer_size: usize) -> Self {
Self {
points,
buffer_size,
}
}
}
impl<I> PiecewiseLinearCurve<I> {
pub fn buffer_size(&self) -> usize {
self.buffer_size
}
}
impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> IntoIterator for PiecewiseLinearCurve<I> {
type Item = PiecewiseLinearCurvePoint;
type IntoIter = I::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.points.into_iter()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TimedValue<V> {
pub sample_offset: usize,
pub value: V,
}
impl<V> ValueAndSampleOffset<V> for TimedValue<V> {
fn value(&self) -> &V {
&self.value
}
fn sample_offset(&self) -> usize {
self.sample_offset
}
}
#[derive(Clone)]
pub struct TimedEnumValues<I> {
points: I,
buffer_size: usize,
}
impl<I: IntoIterator<Item = TimedValue<u32>> + Clone> TimedEnumValues<I> {
pub fn new(points: I, buffer_size: usize, valid_range: Range<u32>) -> Option<Self> {
if buffer_size == 0 {
return None;
}
if check_curve_invariants(points.clone().into_iter(), buffer_size, valid_range) {
Some(Self {
points,
buffer_size,
})
} else {
None
}
}
}
impl<I> TimedEnumValues<I> {
pub fn buffer_size(&self) -> usize {
self.buffer_size
}
}
impl<I: IntoIterator<Item = TimedValue<u32>>> IntoIterator for TimedEnumValues<I> {
type Item = TimedValue<u32>;
type IntoIter = I::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.points.into_iter()
}
}
#[derive(Clone)]
pub struct TimedSwitchValues<I> {
points: I,
buffer_size: usize,
}
impl<I: IntoIterator<Item = TimedValue<bool>> + Clone> TimedSwitchValues<I> {
pub fn new(points: I, buffer_size: usize) -> Option<Self> {
if buffer_size == 0 {
return None;
}
if check_curve_invariants(points.clone().into_iter(), buffer_size, false..=true) {
Some(Self {
points,
buffer_size,
})
} else {
None
}
}
}
impl<I> TimedSwitchValues<I> {
pub fn buffer_size(&self) -> usize {
self.buffer_size
}
}
impl<I: IntoIterator<Item = TimedValue<bool>>> IntoIterator for TimedSwitchValues<I> {
type Item = TimedValue<bool>;
type IntoIter = I::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.points.into_iter()
}
}
#[derive(Clone)]
pub enum NumericBufferState<I> {
Constant(f32),
PiecewiseLinear(PiecewiseLinearCurve<I>),
}
impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> NumericBufferState<I> {
#[allow(clippy::missing_panics_doc)] pub fn value_at_start_of_buffer(self) -> f32 {
match self {
NumericBufferState::Constant(v) => v,
NumericBufferState::PiecewiseLinear(v) => v.points.into_iter().next().unwrap().value,
}
}
}
#[derive(Clone)]
pub enum EnumBufferState<I> {
Constant(u32),
Varying(TimedEnumValues<I>),
}
impl<I: IntoIterator<Item = TimedValue<u32>>> EnumBufferState<I> {
#[allow(clippy::missing_panics_doc)] pub fn value_at_start_of_buffer(self) -> u32 {
match self {
EnumBufferState::Constant(v) => v,
EnumBufferState::Varying(v) => v.points.into_iter().next().unwrap().value,
}
}
}
#[derive(Clone)]
pub enum SwitchBufferState<I> {
Constant(bool),
Varying(TimedSwitchValues<I>),
}
impl<I: IntoIterator<Item = TimedValue<bool>>> SwitchBufferState<I> {
#[allow(clippy::missing_panics_doc)] pub fn value_at_start_of_buffer(self) -> bool {
match self {
SwitchBufferState::Constant(v) => v,
SwitchBufferState::Varying(v) => v.points.into_iter().next().unwrap().value,
}
}
}
pub enum BufferState<N, E, S> {
Numeric(NumericBufferState<N>),
Enum(EnumBufferState<E>),
Switch(SwitchBufferState<S>),
}
pub trait BufferStates {
fn get_by_hash(
&self,
id_hash: IdHash,
) -> Option<
BufferState<
impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
impl Iterator<Item = TimedValue<u32>> + Clone,
impl Iterator<Item = TimedValue<bool>> + Clone,
>,
>;
fn get(
&self,
unique_id: &str,
) -> Option<
BufferState<
impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone,
impl Iterator<Item = TimedValue<u32>> + Clone,
impl Iterator<Item = TimedValue<bool>> + Clone,
>,
> {
self.get_by_hash(hash_id(unique_id))
}
fn numeric_by_hash(
&self,
param_id: IdHash,
) -> Option<NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>> {
match self.get_by_hash(param_id) {
Some(BufferState::Numeric(v)) => Some(v),
_ => None,
}
}
fn get_numeric(
&self,
unique_id: &str,
) -> Option<NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>> {
self.numeric_by_hash(hash_id(unique_id))
}
fn enum_by_hash(
&self,
param_id: IdHash,
) -> Option<EnumBufferState<impl Iterator<Item = TimedValue<u32>> + Clone>> {
match self.get_by_hash(param_id) {
Some(BufferState::Enum(v)) => Some(v),
_ => None,
}
}
fn get_enum(
&self,
unique_id: &str,
) -> Option<EnumBufferState<impl Iterator<Item = TimedValue<u32>> + Clone>> {
self.enum_by_hash(hash_id(unique_id))
}
fn switch_by_hash(
&self,
param_id: IdHash,
) -> Option<SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone>> {
match self.get_by_hash(param_id) {
Some(BufferState::Switch(v)) => Some(v),
_ => None,
}
}
fn get_switch(
&self,
unique_id: &str,
) -> Option<SwitchBufferState<impl Iterator<Item = TimedValue<bool>> + Clone>> {
self.switch_by_hash(hash_id(unique_id))
}
}
#[cfg(test)]
mod tests {
use super::{
IdHash, InternalValue, PiecewiseLinearCurve, PiecewiseLinearCurvePoint, States, hash_id,
};
struct MyState {}
impl States for MyState {
fn get_by_hash(&self, param_hash: IdHash) -> Option<InternalValue> {
if param_hash == hash_id("numeric") {
return Some(InternalValue::Numeric(0.5));
} else if param_hash == hash_id("enum") {
return Some(InternalValue::Enum(2));
} else if param_hash == hash_id("switch") {
return Some(InternalValue::Switch(true));
} else {
return None;
}
}
}
#[test]
fn parameter_states_default_functions() {
let state = MyState {};
assert_eq!(state.get_numeric("numeric"), Some(0.5));
assert_eq!(state.get_numeric("enum"), None);
assert_eq!(state.get_enum("numeric"), None);
assert_eq!(state.get_enum("enum"), Some(2));
assert_eq!(state.get_switch("switch"), Some(true));
assert_eq!(state.get_switch("numeric"), None);
}
#[test]
fn valid_curve() {
assert!(
PiecewiseLinearCurve::new(
(&[
PiecewiseLinearCurvePoint {
sample_offset: 0,
value: 0.5
},
PiecewiseLinearCurvePoint {
sample_offset: 3,
value: 0.4
},
PiecewiseLinearCurvePoint {
sample_offset: 4,
value: 0.3
}
])
.iter()
.cloned(),
10,
0.0..=1.0
)
.is_some()
)
}
#[test]
fn out_of_order_curve_points_rejected() {
assert!(
PiecewiseLinearCurve::new(
(&[
PiecewiseLinearCurvePoint {
sample_offset: 0,
value: 0.5
},
PiecewiseLinearCurvePoint {
sample_offset: 4,
value: 0.4
},
PiecewiseLinearCurvePoint {
sample_offset: 3,
value: 0.3
}
])
.iter()
.cloned(),
10,
0.0..=1.0
)
.is_none()
)
}
#[test]
fn empty_curves_rejected() {
assert!(PiecewiseLinearCurve::new((&[]).iter().cloned(), 10, 0.0..=1.0).is_none())
}
#[test]
fn zero_length_buffers_rejected() {
assert!(
PiecewiseLinearCurve::new(
(&[PiecewiseLinearCurvePoint {
sample_offset: 0,
value: 0.2
}])
.iter()
.cloned(),
0,
0.0..=1.0
)
.is_none()
)
}
#[test]
fn out_of_bounds_sample_counts_rejected() {
assert!(
PiecewiseLinearCurve::new(
(&[
PiecewiseLinearCurvePoint {
sample_offset: 0,
value: 0.2
},
PiecewiseLinearCurvePoint {
sample_offset: 12,
value: 0.3
}
])
.iter()
.cloned(),
10,
0.0..=1.0
)
.is_none()
)
}
#[test]
fn out_of_bounds_curve_values_rejected() {
assert!(
PiecewiseLinearCurve::new(
(&[
PiecewiseLinearCurvePoint {
sample_offset: 0,
value: 0.2
},
PiecewiseLinearCurvePoint {
sample_offset: 3,
value: 1.3
}
])
.iter()
.cloned(),
10,
0.0..=1.0
)
.is_none()
)
}
#[test]
fn curve_does_not_start_at_zero_rejected() {
assert!(
PiecewiseLinearCurve::new(
(&[
PiecewiseLinearCurvePoint {
sample_offset: 3,
value: 0.5
},
PiecewiseLinearCurvePoint {
sample_offset: 6,
value: 0.4
},
PiecewiseLinearCurvePoint {
sample_offset: 7,
value: 0.3
}
])
.iter()
.cloned(),
10,
0.0..=1.0
)
.is_none()
)
}
#[test]
fn curve_multiple_points_same_sample_rejected() {
assert!(
PiecewiseLinearCurve::new(
(&[
PiecewiseLinearCurvePoint {
sample_offset: 0,
value: 0.5
},
PiecewiseLinearCurvePoint {
sample_offset: 6,
value: 0.4
},
PiecewiseLinearCurvePoint {
sample_offset: 6,
value: 0.3
}
])
.iter()
.cloned(),
10,
0.0..=1.0
)
.is_none()
)
}
#[test]
fn const_hash_matches_fxhash() {
use super::fxhash32_str;
for s in [
"",
"a",
"ab",
"abc",
"gain",
"abcde",
"bypass",
"abcdefgh",
"my special switch",
] {
assert_eq!(
fxhash32_str(s),
fxhash::hash32(s),
"const hash mismatch for '{s}'"
);
}
}
const _: () = assert!(hash_id("gain").internal_hash != 0);
}