1use alloc::format;
7use alloc::string::String;
8#[cfg(feature = "wasm")]
9use alloc::string::ToString;
10use alloc::vec::Vec;
11use serde::{Deserialize, Serialize};
12
13use crate::port::GraphModule;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
22#[serde(rename_all = "snake_case", tag = "type")]
23pub enum ValueFormat {
24 Decimal { places: u8 },
26 Frequency,
28 Time,
30 Decibels,
32 Percent,
34 NoteName,
36 Ratio,
38}
39
40impl Default for ValueFormat {
41 fn default() -> Self {
42 ValueFormat::Decimal { places: 2 }
43 }
44}
45
46impl ValueFormat {
47 pub fn format(&self, value: f64) -> String {
49 match self {
50 ValueFormat::Decimal { places } => {
51 format!("{:.prec$}", value, prec = *places as usize)
52 }
53 ValueFormat::Frequency => {
54 if value >= 1000.0 {
55 format!("{:.2} kHz", value / 1000.0)
56 } else {
57 format!("{:.1} Hz", value)
58 }
59 }
60 ValueFormat::Time => {
61 if value >= 1.0 {
62 format!("{:.2} s", value)
63 } else {
64 format!("{:.1} ms", value * 1000.0)
65 }
66 }
67 ValueFormat::Decibels => {
68 format!("{:.1} dB", value)
69 }
70 ValueFormat::Percent => {
71 format!("{:.0}%", value * 100.0)
72 }
73 ValueFormat::NoteName => {
74 let midi_note = libm::Libm::<f64>::round((value * 12.0) + 60.0) as i32;
76 let note_names = [
77 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
78 ];
79 let note = note_names[(midi_note.rem_euclid(12)) as usize];
80 let octave = (midi_note / 12) - 1;
81 format!("{}{}", note, octave)
82 }
83 ValueFormat::Ratio => {
84 if value >= 1.0 {
85 format!("{:.1}:1", value)
86 } else if value > 0.0 {
87 format!("1:{:.1}", 1.0 / value)
88 } else {
89 "0:1".into()
90 }
91 }
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
102#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
103#[serde(rename_all = "snake_case", tag = "type")]
104pub enum ParamCurve {
105 #[default]
107 Linear,
108 Exponential,
110 Logarithmic,
112 Stepped { steps: u32 },
114}
115
116impl ParamCurve {
117 pub fn apply(&self, normalized: f64, min: f64, max: f64) -> f64 {
119 let n = normalized.clamp(0.0, 1.0);
120 match self {
121 ParamCurve::Linear => min + n * (max - min),
122 ParamCurve::Exponential => {
123 if min <= 0.0 {
124 n * max
125 } else {
126 min * libm::Libm::<f64>::pow(max / min, n)
127 }
128 }
129 ParamCurve::Logarithmic => {
130 let log_min = if min > 0.0 {
132 libm::Libm::<f64>::log10(min)
133 } else {
134 0.0
135 };
136 let log_max = libm::Libm::<f64>::log10(max.max(0.001));
137 libm::Libm::<f64>::pow(10.0, log_min + n * (log_max - log_min))
138 }
139 ParamCurve::Stepped { steps } => {
140 let step_size = (max - min) / (*steps as f64);
141 let step_index = libm::Libm::<f64>::floor(n * (*steps as f64)) as u32;
142 min + (step_index.min(*steps - 1) as f64) * step_size
143 }
144 }
145 }
146
147 pub fn normalize(&self, value: f64, min: f64, max: f64) -> f64 {
149 if (max - min).abs() < 1e-10 {
150 return 0.0;
151 }
152
153 match self {
154 ParamCurve::Linear => ((value - min) / (max - min)).clamp(0.0, 1.0),
155 ParamCurve::Exponential => {
156 if min <= 0.0 || value <= 0.0 {
157 ((value - min) / (max - min)).clamp(0.0, 1.0)
158 } else {
159 let log_ratio =
160 libm::Libm::<f64>::log(value / min) / libm::Libm::<f64>::log(max / min);
161 log_ratio.clamp(0.0, 1.0)
162 }
163 }
164 ParamCurve::Logarithmic => {
165 let log_min = if min > 0.0 {
166 libm::Libm::<f64>::log10(min)
167 } else {
168 0.0
169 };
170 let log_max = libm::Libm::<f64>::log10(max.max(0.001));
171 let log_val = libm::Libm::<f64>::log10(value.max(0.001));
172 ((log_val - log_min) / (log_max - log_min)).clamp(0.0, 1.0)
173 }
174 ParamCurve::Stepped { steps } => {
175 let step_size = (max - min) / (*steps as f64);
176 let step_index = libm::Libm::<f64>::round((value - min) / step_size) as u32;
177 (step_index as f64 / *steps as f64).clamp(0.0, 1.0)
178 }
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
189#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
190#[serde(rename_all = "snake_case")]
191pub enum ControlType {
192 #[default]
194 Knob,
195 Slider,
197 Toggle,
199 Select,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
209#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
210#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
211pub struct ParamInfo {
212 pub id: String,
214 pub name: String,
216 pub value: f64,
218 pub min: f64,
220 pub max: f64,
222 pub default: f64,
224 pub curve: ParamCurve,
226 pub control: ControlType,
228 pub unit: Option<String>,
230 pub format: ValueFormat,
232}
233
234impl ParamInfo {
235 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
237 Self {
238 id: id.into(),
239 name: name.into(),
240 value: 0.5,
241 min: 0.0,
242 max: 1.0,
243 default: 0.5,
244 curve: ParamCurve::Linear,
245 control: ControlType::Knob,
246 unit: None,
247 format: ValueFormat::default(),
248 }
249 }
250
251 pub fn with_range(mut self, min: f64, max: f64) -> Self {
253 self.min = min;
254 self.max = max;
255 self
256 }
257
258 pub fn with_default(mut self, default: f64) -> Self {
260 self.default = default;
261 self.value = default;
262 self
263 }
264
265 pub fn with_value(mut self, value: f64) -> Self {
267 self.value = value;
268 self
269 }
270
271 pub fn with_curve(mut self, curve: ParamCurve) -> Self {
273 self.curve = curve;
274 self
275 }
276
277 pub fn with_control(mut self, control: ControlType) -> Self {
279 self.control = control;
280 self
281 }
282
283 pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
285 self.unit = Some(unit.into());
286 self
287 }
288
289 pub fn with_format(mut self, format: ValueFormat) -> Self {
291 self.format = format;
292 self
293 }
294
295 pub fn frequency(id: impl Into<String>, name: impl Into<String>) -> Self {
297 Self::new(id, name)
298 .with_range(20.0, 20000.0)
299 .with_default(1000.0)
300 .with_curve(ParamCurve::Exponential)
301 .with_unit("Hz")
302 .with_format(ValueFormat::Frequency)
303 }
304
305 pub fn time(id: impl Into<String>, name: impl Into<String>) -> Self {
307 Self::new(id, name)
308 .with_range(0.001, 10.0)
309 .with_default(0.1)
310 .with_curve(ParamCurve::Exponential)
311 .with_unit("s")
312 .with_format(ValueFormat::Time)
313 }
314
315 pub fn decibels(id: impl Into<String>, name: impl Into<String>) -> Self {
317 Self::new(id, name)
318 .with_range(-60.0, 12.0)
319 .with_default(0.0)
320 .with_curve(ParamCurve::Linear)
321 .with_unit("dB")
322 .with_format(ValueFormat::Decibels)
323 }
324
325 pub fn percent(id: impl Into<String>, name: impl Into<String>) -> Self {
327 Self::new(id, name)
328 .with_range(0.0, 1.0)
329 .with_default(0.5)
330 .with_curve(ParamCurve::Linear)
331 .with_format(ValueFormat::Percent)
332 }
333
334 pub fn toggle(id: impl Into<String>, name: impl Into<String>) -> Self {
336 Self::new(id, name)
337 .with_range(0.0, 1.0)
338 .with_default(0.0)
339 .with_curve(ParamCurve::Stepped { steps: 2 })
340 .with_control(ControlType::Toggle)
341 }
342
343 pub fn select(id: impl Into<String>, name: impl Into<String>, options: u32) -> Self {
345 Self::new(id, name)
346 .with_range(0.0, (options - 1) as f64)
347 .with_default(0.0)
348 .with_curve(ParamCurve::Stepped { steps: options })
349 .with_control(ControlType::Select)
350 .with_format(ValueFormat::Decimal { places: 0 })
351 }
352
353 pub fn normalized(&self) -> f64 {
355 self.curve.normalize(self.value, self.min, self.max)
356 }
357
358 pub fn set_normalized(&mut self, normalized: f64) {
360 self.value = self.curve.apply(normalized, self.min, self.max);
361 }
362
363 pub fn format_value(&self) -> String {
365 self.format.format(self.value)
366 }
367}
368
369pub trait ModuleIntrospection: GraphModule {
378 fn param_infos(&self) -> Vec<ParamInfo> {
383 Vec::new()
384 }
385
386 fn get_param_info(&self, id: &str) -> Option<ParamInfo> {
388 self.param_infos().into_iter().find(|p| p.id == id)
389 }
390
391 fn set_param_by_id(&mut self, _id: &str, _value: f64) -> bool {
395 false
396 }
397}
398
399#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_value_format_decimal() {
409 let fmt = ValueFormat::Decimal { places: 2 };
410 assert_eq!(fmt.format(1.23456), "1.23");
411 }
412
413 #[test]
414 fn test_value_format_frequency() {
415 let fmt = ValueFormat::Frequency;
416 assert_eq!(fmt.format(440.0), "440.0 Hz");
417 assert_eq!(fmt.format(2500.0), "2.50 kHz");
418 }
419
420 #[test]
421 fn test_value_format_time() {
422 let fmt = ValueFormat::Time;
423 assert_eq!(fmt.format(0.1), "100.0 ms");
424 assert_eq!(fmt.format(2.5), "2.50 s");
425 }
426
427 #[test]
428 fn test_value_format_decibels() {
429 let fmt = ValueFormat::Decibels;
430 assert_eq!(fmt.format(-12.0), "-12.0 dB");
431 }
432
433 #[test]
434 fn test_value_format_percent() {
435 let fmt = ValueFormat::Percent;
436 assert_eq!(fmt.format(0.5), "50%");
437 assert_eq!(fmt.format(1.0), "100%");
438 }
439
440 #[test]
441 fn test_value_format_note_name() {
442 let fmt = ValueFormat::NoteName;
443 assert_eq!(fmt.format(0.0), "C4"); assert_eq!(fmt.format(1.0), "C5"); }
446
447 #[test]
448 fn test_value_format_ratio() {
449 let fmt = ValueFormat::Ratio;
450 assert_eq!(fmt.format(2.0), "2.0:1");
451 assert_eq!(fmt.format(0.5), "1:2.0");
452 }
453
454 #[test]
455 fn test_param_curve_linear() {
456 let curve = ParamCurve::Linear;
457 assert!((curve.apply(0.0, 0.0, 100.0) - 0.0).abs() < 0.01);
458 assert!((curve.apply(0.5, 0.0, 100.0) - 50.0).abs() < 0.01);
459 assert!((curve.apply(1.0, 0.0, 100.0) - 100.0).abs() < 0.01);
460 }
461
462 #[test]
463 fn test_param_curve_exponential() {
464 let curve = ParamCurve::Exponential;
465 let val = curve.apply(0.5, 20.0, 20000.0);
466 let expected = (20.0_f64 * 20000.0).sqrt();
468 assert!((val - expected).abs() < 1.0);
469 }
470
471 #[test]
472 fn test_param_curve_stepped() {
473 let curve = ParamCurve::Stepped { steps: 4 };
474 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); }
481
482 #[test]
483 fn test_param_curve_normalize_linear() {
484 let curve = ParamCurve::Linear;
485 assert!((curve.normalize(50.0, 0.0, 100.0) - 0.5).abs() < 0.01);
486 }
487
488 #[test]
489 fn test_param_info_creation() {
490 let param = ParamInfo::new("freq", "Frequency")
491 .with_range(20.0, 20000.0)
492 .with_default(440.0)
493 .with_curve(ParamCurve::Exponential)
494 .with_unit("Hz")
495 .with_format(ValueFormat::Frequency);
496
497 assert_eq!(param.id, "freq");
498 assert_eq!(param.name, "Frequency");
499 assert_eq!(param.min, 20.0);
500 assert_eq!(param.max, 20000.0);
501 assert_eq!(param.default, 440.0);
502 assert_eq!(param.value, 440.0);
503 assert_eq!(param.unit, Some("Hz".to_string()));
504 }
505
506 #[test]
507 fn test_param_info_frequency_preset() {
508 let param = ParamInfo::frequency("cutoff", "Cutoff");
509 assert_eq!(param.min, 20.0);
510 assert_eq!(param.max, 20000.0);
511 assert_eq!(param.curve, ParamCurve::Exponential);
512 }
513
514 #[test]
515 fn test_param_info_toggle_preset() {
516 let param = ParamInfo::toggle("sync", "Hard Sync");
517 assert_eq!(param.control, ControlType::Toggle);
518 assert!(matches!(param.curve, ParamCurve::Stepped { steps: 2 }));
519 }
520
521 #[test]
522 fn test_param_info_select_preset() {
523 let param = ParamInfo::select("waveform", "Waveform", 4);
524 assert_eq!(param.control, ControlType::Select);
525 assert!(matches!(param.curve, ParamCurve::Stepped { steps: 4 }));
526 }
527
528 #[test]
529 fn test_param_info_normalized() {
530 let mut param = ParamInfo::new("test", "Test")
531 .with_range(0.0, 100.0)
532 .with_value(50.0);
533
534 assert!((param.normalized() - 0.5).abs() < 0.01);
535
536 param.set_normalized(0.25);
537 assert!((param.value - 25.0).abs() < 0.01);
538 }
539
540 #[test]
541 fn test_param_info_format_value() {
542 let param = ParamInfo::frequency("freq", "Frequency").with_value(1000.0);
543 assert_eq!(param.format_value(), "1.00 kHz");
544 }
545
546 #[test]
547 fn test_param_info_serialization() {
548 let param = ParamInfo::frequency("cutoff", "Cutoff");
549 let json = serde_json::to_string(¶m).unwrap();
550 assert!(json.contains("cutoff"));
551 assert!(json.contains("exponential"));
552
553 let parsed: ParamInfo = serde_json::from_str(&json).unwrap();
554 assert_eq!(parsed.id, "cutoff");
555 }
556}