bbx_dsp/
parameter.rs

1//! Parameter modulation system.
2//!
3//! This module provides the [`Parameter`] type, which allows block parameters
4//! to be either constant values or modulated by other blocks (e.g., LFOs, envelopes).
5
6use crate::{block::BlockId, sample::Sample};
7
8/// A block parameter that can be constant or modulated.
9///
10/// Parameters allow block settings (like oscillator frequency or gain level)
11/// to be controlled dynamically by modulator blocks during processing.
12#[derive(Debug, Clone)]
13pub enum Parameter<S: Sample> {
14    /// A fixed value that doesn't change during processing.
15    Constant(S),
16
17    /// A value controlled by a modulator block.
18    /// The [`BlockId`] references the source modulator.
19    Modulated(BlockId),
20}
21
22impl<S: Sample> Parameter<S> {
23    /// Get the appropriate value for a `Parameter`.
24    ///
25    /// For modulated parameters, safely looks up the modulation value
26    /// and returns zero if the BlockId is out of bounds.
27    #[inline]
28    pub fn get_value(&self, modulation_values: &[S]) -> S {
29        match self {
30            Parameter::Constant(value) => *value,
31            Parameter::Modulated(block_id) => {
32                // Safe lookup to prevent panic in audio thread
33                modulation_values.get(block_id.0).copied().unwrap_or(S::ZERO)
34            }
35        }
36    }
37}
38
39/// Describes a modulation output provided by a modulator block.
40///
41/// Modulator blocks (LFOs, envelopes) declare their outputs using this type,
42/// specifying the output name and expected value range.
43#[derive(Debug, Clone)]
44pub struct ModulationOutput {
45    /// Human-readable name for this output (e.g., "amplitude", "frequency").
46    pub name: &'static str,
47
48    /// Minimum value this output can produce.
49    pub min_value: f64,
50
51    /// Maximum value this output can produce.
52    pub max_value: f64,
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_parameter_constant_f32() {
61        let param = Parameter::Constant(42.0_f32);
62        let modulation_values: Vec<f32> = vec![];
63        let value = param.get_value(&modulation_values);
64        assert!((value - 42.0).abs() < 1e-6);
65    }
66
67    #[test]
68    fn test_parameter_constant_f64() {
69        let param = Parameter::Constant(42.0_f64);
70        let modulation_values: Vec<f64> = vec![];
71        let value = param.get_value(&modulation_values);
72        assert!((value - 42.0).abs() < 1e-12);
73    }
74
75    #[test]
76    fn test_parameter_modulated_valid_index_f32() {
77        let param = Parameter::Modulated(BlockId(1));
78        let modulation_values: Vec<f32> = vec![10.0, 20.0, 30.0];
79        let value = param.get_value(&modulation_values);
80        assert!((value - 20.0).abs() < 1e-6);
81    }
82
83    #[test]
84    fn test_parameter_modulated_valid_index_f64() {
85        let param = Parameter::Modulated(BlockId(1));
86        let modulation_values: Vec<f64> = vec![10.0, 20.0, 30.0];
87        let value = param.get_value(&modulation_values);
88        assert!((value - 20.0).abs() < 1e-12);
89    }
90
91    #[test]
92    fn test_parameter_modulated_out_of_bounds_returns_zero_f32() {
93        let param = Parameter::Modulated(BlockId(10));
94        let modulation_values: Vec<f32> = vec![10.0, 20.0, 30.0];
95        let value = param.get_value(&modulation_values);
96        assert!(value.abs() < 1e-10, "Out of bounds should return zero");
97    }
98
99    #[test]
100    fn test_parameter_modulated_out_of_bounds_returns_zero_f64() {
101        let param = Parameter::Modulated(BlockId(10));
102        let modulation_values: Vec<f64> = vec![10.0, 20.0, 30.0];
103        let value = param.get_value(&modulation_values);
104        assert!(value.abs() < 1e-15, "Out of bounds should return zero");
105    }
106
107    #[test]
108    fn test_parameter_modulated_empty_array_returns_zero_f32() {
109        let param = Parameter::Modulated(BlockId(0));
110        let modulation_values: Vec<f32> = vec![];
111        let value = param.get_value(&modulation_values);
112        assert!(value.abs() < 1e-10, "Empty array should return zero");
113    }
114
115    #[test]
116    fn test_parameter_modulated_empty_array_returns_zero_f64() {
117        let param = Parameter::Modulated(BlockId(0));
118        let modulation_values: Vec<f64> = vec![];
119        let value = param.get_value(&modulation_values);
120        assert!(value.abs() < 1e-15, "Empty array should return zero");
121    }
122
123    #[test]
124    fn test_parameter_modulated_index_zero_f32() {
125        let param = Parameter::Modulated(BlockId(0));
126        let modulation_values: Vec<f32> = vec![99.0, 20.0, 30.0];
127        let value = param.get_value(&modulation_values);
128        assert!((value - 99.0).abs() < 1e-6);
129    }
130
131    #[test]
132    fn test_parameter_modulated_index_zero_f64() {
133        let param = Parameter::Modulated(BlockId(0));
134        let modulation_values: Vec<f64> = vec![99.0, 20.0, 30.0];
135        let value = param.get_value(&modulation_values);
136        assert!((value - 99.0).abs() < 1e-12);
137    }
138
139    #[test]
140    fn test_parameter_constant_negative_value_f32() {
141        let param = Parameter::Constant(-42.0_f32);
142        let modulation_values: Vec<f32> = vec![];
143        let value = param.get_value(&modulation_values);
144        assert!((value - (-42.0)).abs() < 1e-6);
145    }
146
147    #[test]
148    fn test_parameter_constant_zero_f32() {
149        let param = Parameter::Constant(0.0_f32);
150        let modulation_values: Vec<f32> = vec![];
151        let value = param.get_value(&modulation_values);
152        assert!(value.abs() < 1e-10);
153    }
154
155    #[test]
156    fn test_parameter_modulated_negative_value_f32() {
157        let param = Parameter::Modulated(BlockId(0));
158        let modulation_values: Vec<f32> = vec![-50.0];
159        let value = param.get_value(&modulation_values);
160        assert!((value - (-50.0)).abs() < 1e-6);
161    }
162
163    #[test]
164    fn test_modulation_output_creation() {
165        let output = ModulationOutput {
166            name: "test",
167            min_value: -1.0,
168            max_value: 1.0,
169        };
170        assert_eq!(output.name, "test");
171        assert!((output.min_value - (-1.0)).abs() < 1e-10);
172        assert!((output.max_value - 1.0).abs() < 1e-10);
173    }
174
175    #[test]
176    fn test_parameter_clone_f32() {
177        let param1 = Parameter::Constant(42.0_f32);
178        let param2 = param1.clone();
179
180        let modulation_values: Vec<f32> = vec![];
181        let value1 = param1.get_value(&modulation_values);
182        let value2 = param2.get_value(&modulation_values);
183
184        assert!((value1 - value2).abs() < 1e-10);
185    }
186
187    #[test]
188    fn test_parameter_modulated_clone_f32() {
189        let param1 = Parameter::Modulated::<f32>(BlockId(1));
190        let param2 = param1.clone();
191
192        let modulation_values: Vec<f32> = vec![10.0, 20.0];
193        let value1 = param1.get_value(&modulation_values);
194        let value2 = param2.get_value(&modulation_values);
195
196        assert!((value1 - value2).abs() < 1e-10);
197    }
198}