use alloc::format;
use alloc::string::String;
#[cfg(feature = "wasm")]
use alloc::string::ToString;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::port::GraphModule;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ValueFormat {
Decimal { places: u8 },
Frequency,
Time,
Decibels,
Percent,
NoteName,
Ratio,
}
impl Default for ValueFormat {
fn default() -> Self {
ValueFormat::Decimal { places: 2 }
}
}
impl ValueFormat {
pub fn format(&self, value: f64) -> String {
match self {
ValueFormat::Decimal { places } => {
format!("{:.prec$}", value, prec = *places as usize)
}
ValueFormat::Frequency => {
if value >= 1000.0 {
format!("{:.2} kHz", value / 1000.0)
} else {
format!("{:.1} Hz", value)
}
}
ValueFormat::Time => {
if value >= 1.0 {
format!("{:.2} s", value)
} else {
format!("{:.1} ms", value * 1000.0)
}
}
ValueFormat::Decibels => {
format!("{:.1} dB", value)
}
ValueFormat::Percent => {
format!("{:.0}%", value * 100.0)
}
ValueFormat::NoteName => {
let midi_note = libm::Libm::<f64>::round((value * 12.0) + 60.0) as i32;
let note_names = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
let note = note_names[(midi_note.rem_euclid(12)) as usize];
let octave = (midi_note / 12) - 1;
format!("{}{}", note, octave)
}
ValueFormat::Ratio => {
if value >= 1.0 {
format!("{:.1}:1", value)
} else if value > 0.0 {
format!("1:{:.1}", 1.0 / value)
} else {
"0:1".into()
}
}
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ParamCurve {
#[default]
Linear,
Exponential,
Logarithmic,
Stepped { steps: u32 },
}
impl ParamCurve {
pub fn apply(&self, normalized: f64, min: f64, max: f64) -> f64 {
let n = normalized.clamp(0.0, 1.0);
match self {
ParamCurve::Linear => min + n * (max - min),
ParamCurve::Exponential => {
if min <= 0.0 {
n * max
} else {
min * libm::Libm::<f64>::pow(max / min, n)
}
}
ParamCurve::Logarithmic => {
let log_min = if min > 0.0 {
libm::Libm::<f64>::log10(min)
} else {
0.0
};
let log_max = libm::Libm::<f64>::log10(max.max(0.001));
libm::Libm::<f64>::pow(10.0, log_min + n * (log_max - log_min))
}
ParamCurve::Stepped { steps } => {
let step_size = (max - min) / (*steps as f64);
let step_index = libm::Libm::<f64>::floor(n * (*steps as f64)) as u32;
min + (step_index.min(*steps - 1) as f64) * step_size
}
}
}
pub fn normalize(&self, value: f64, min: f64, max: f64) -> f64 {
if (max - min).abs() < 1e-10 {
return 0.0;
}
match self {
ParamCurve::Linear => ((value - min) / (max - min)).clamp(0.0, 1.0),
ParamCurve::Exponential => {
if min <= 0.0 || value <= 0.0 {
((value - min) / (max - min)).clamp(0.0, 1.0)
} else {
let log_ratio =
libm::Libm::<f64>::log(value / min) / libm::Libm::<f64>::log(max / min);
log_ratio.clamp(0.0, 1.0)
}
}
ParamCurve::Logarithmic => {
let log_min = if min > 0.0 {
libm::Libm::<f64>::log10(min)
} else {
0.0
};
let log_max = libm::Libm::<f64>::log10(max.max(0.001));
let log_val = libm::Libm::<f64>::log10(value.max(0.001));
((log_val - log_min) / (log_max - log_min)).clamp(0.0, 1.0)
}
ParamCurve::Stepped { steps } => {
let step_size = (max - min) / (*steps as f64);
let step_index = libm::Libm::<f64>::round((value - min) / step_size) as u32;
(step_index as f64 / *steps as f64).clamp(0.0, 1.0)
}
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[serde(rename_all = "snake_case")]
pub enum ControlType {
#[default]
Knob,
Slider,
Toggle,
Select,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ParamInfo {
pub id: String,
pub name: String,
pub value: f64,
pub min: f64,
pub max: f64,
pub default: f64,
pub curve: ParamCurve,
pub control: ControlType,
pub unit: Option<String>,
pub format: ValueFormat,
}
impl ParamInfo {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
value: 0.5,
min: 0.0,
max: 1.0,
default: 0.5,
curve: ParamCurve::Linear,
control: ControlType::Knob,
unit: None,
format: ValueFormat::default(),
}
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.min = min;
self.max = max;
self
}
pub fn with_default(mut self, default: f64) -> Self {
self.default = default;
self.value = default;
self
}
pub fn with_value(mut self, value: f64) -> Self {
self.value = value;
self
}
pub fn with_curve(mut self, curve: ParamCurve) -> Self {
self.curve = curve;
self
}
pub fn with_control(mut self, control: ControlType) -> Self {
self.control = control;
self
}
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = Some(unit.into());
self
}
pub fn with_format(mut self, format: ValueFormat) -> Self {
self.format = format;
self
}
pub fn frequency(id: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(id, name)
.with_range(20.0, 20000.0)
.with_default(1000.0)
.with_curve(ParamCurve::Exponential)
.with_unit("Hz")
.with_format(ValueFormat::Frequency)
}
pub fn time(id: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(id, name)
.with_range(0.001, 10.0)
.with_default(0.1)
.with_curve(ParamCurve::Exponential)
.with_unit("s")
.with_format(ValueFormat::Time)
}
pub fn decibels(id: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(id, name)
.with_range(-60.0, 12.0)
.with_default(0.0)
.with_curve(ParamCurve::Linear)
.with_unit("dB")
.with_format(ValueFormat::Decibels)
}
pub fn percent(id: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(id, name)
.with_range(0.0, 1.0)
.with_default(0.5)
.with_curve(ParamCurve::Linear)
.with_format(ValueFormat::Percent)
}
pub fn toggle(id: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(id, name)
.with_range(0.0, 1.0)
.with_default(0.0)
.with_curve(ParamCurve::Stepped { steps: 2 })
.with_control(ControlType::Toggle)
}
pub fn select(id: impl Into<String>, name: impl Into<String>, options: u32) -> Self {
Self::new(id, name)
.with_range(0.0, (options - 1) as f64)
.with_default(0.0)
.with_curve(ParamCurve::Stepped { steps: options })
.with_control(ControlType::Select)
.with_format(ValueFormat::Decimal { places: 0 })
}
pub fn normalized(&self) -> f64 {
self.curve.normalize(self.value, self.min, self.max)
}
pub fn set_normalized(&mut self, normalized: f64) {
self.value = self.curve.apply(normalized, self.min, self.max);
}
pub fn format_value(&self) -> String {
self.format.format(self.value)
}
}
pub trait ModuleIntrospection: GraphModule {
fn param_infos(&self) -> Vec<ParamInfo> {
Vec::new()
}
fn get_param_info(&self, id: &str) -> Option<ParamInfo> {
self.param_infos().into_iter().find(|p| p.id == id)
}
fn set_param_by_id(&mut self, _id: &str, _value: f64) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_format_decimal() {
let fmt = ValueFormat::Decimal { places: 2 };
assert_eq!(fmt.format(1.23456), "1.23");
}
#[test]
fn test_value_format_frequency() {
let fmt = ValueFormat::Frequency;
assert_eq!(fmt.format(440.0), "440.0 Hz");
assert_eq!(fmt.format(2500.0), "2.50 kHz");
}
#[test]
fn test_value_format_time() {
let fmt = ValueFormat::Time;
assert_eq!(fmt.format(0.1), "100.0 ms");
assert_eq!(fmt.format(2.5), "2.50 s");
}
#[test]
fn test_value_format_decibels() {
let fmt = ValueFormat::Decibels;
assert_eq!(fmt.format(-12.0), "-12.0 dB");
}
#[test]
fn test_value_format_percent() {
let fmt = ValueFormat::Percent;
assert_eq!(fmt.format(0.5), "50%");
assert_eq!(fmt.format(1.0), "100%");
}
#[test]
fn test_value_format_note_name() {
let fmt = ValueFormat::NoteName;
assert_eq!(fmt.format(0.0), "C4"); assert_eq!(fmt.format(1.0), "C5"); }
#[test]
fn test_value_format_ratio() {
let fmt = ValueFormat::Ratio;
assert_eq!(fmt.format(2.0), "2.0:1");
assert_eq!(fmt.format(0.5), "1:2.0");
}
#[test]
fn test_param_curve_linear() {
let curve = ParamCurve::Linear;
assert!((curve.apply(0.0, 0.0, 100.0) - 0.0).abs() < 0.01);
assert!((curve.apply(0.5, 0.0, 100.0) - 50.0).abs() < 0.01);
assert!((curve.apply(1.0, 0.0, 100.0) - 100.0).abs() < 0.01);
}
#[test]
fn test_param_curve_exponential() {
let curve = ParamCurve::Exponential;
let val = curve.apply(0.5, 20.0, 20000.0);
let expected = (20.0_f64 * 20000.0).sqrt();
assert!((val - expected).abs() < 1.0);
}
#[test]
fn test_param_curve_stepped() {
let curve = ParamCurve::Stepped { steps: 4 };
assert!((curve.apply(0.0, 0.0, 3.0) - 0.0).abs() < 0.01); assert!((curve.apply(0.25, 0.0, 3.0) - 0.75).abs() < 0.01); assert!((curve.apply(0.5, 0.0, 3.0) - 1.5).abs() < 0.01); assert!((curve.apply(0.75, 0.0, 3.0) - 2.25).abs() < 0.01); }
#[test]
fn test_param_curve_normalize_linear() {
let curve = ParamCurve::Linear;
assert!((curve.normalize(50.0, 0.0, 100.0) - 0.5).abs() < 0.01);
}
#[test]
fn test_param_info_creation() {
let param = ParamInfo::new("freq", "Frequency")
.with_range(20.0, 20000.0)
.with_default(440.0)
.with_curve(ParamCurve::Exponential)
.with_unit("Hz")
.with_format(ValueFormat::Frequency);
assert_eq!(param.id, "freq");
assert_eq!(param.name, "Frequency");
assert_eq!(param.min, 20.0);
assert_eq!(param.max, 20000.0);
assert_eq!(param.default, 440.0);
assert_eq!(param.value, 440.0);
assert_eq!(param.unit, Some("Hz".to_string()));
}
#[test]
fn test_param_info_frequency_preset() {
let param = ParamInfo::frequency("cutoff", "Cutoff");
assert_eq!(param.min, 20.0);
assert_eq!(param.max, 20000.0);
assert_eq!(param.curve, ParamCurve::Exponential);
}
#[test]
fn test_param_info_toggle_preset() {
let param = ParamInfo::toggle("sync", "Hard Sync");
assert_eq!(param.control, ControlType::Toggle);
assert!(matches!(param.curve, ParamCurve::Stepped { steps: 2 }));
}
#[test]
fn test_param_info_select_preset() {
let param = ParamInfo::select("waveform", "Waveform", 4);
assert_eq!(param.control, ControlType::Select);
assert!(matches!(param.curve, ParamCurve::Stepped { steps: 4 }));
}
#[test]
fn test_param_info_normalized() {
let mut param = ParamInfo::new("test", "Test")
.with_range(0.0, 100.0)
.with_value(50.0);
assert!((param.normalized() - 0.5).abs() < 0.01);
param.set_normalized(0.25);
assert!((param.value - 25.0).abs() < 0.01);
}
#[test]
fn test_param_info_format_value() {
let param = ParamInfo::frequency("freq", "Frequency").with_value(1000.0);
assert_eq!(param.format_value(), "1.00 kHz");
}
#[test]
fn test_param_info_serialization() {
let param = ParamInfo::frequency("cutoff", "Cutoff");
let json = serde_json::to_string(¶m).unwrap();
assert!(json.contains("cutoff"));
assert!(json.contains("exponential"));
let parsed: ParamInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, "cutoff");
}
}