Skip to main content

rill_core_dsp/
mapping.rs

1//! ControlMapper — maps normalized [0, 1\] control values to parameter ranges.
2//!
3//! Provides `MappingStrategy` to select the mapping curve and `ControlMapper<T>`
4//! which implements `Algorithm<T>`.
5
6use rill_core::math::Transcendental;
7use rill_core::traits::ProcessResult;
8use rill_core::traits::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
9
10/// Mapping strategy for translating normalized [0, 1\] values to a parameter range.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum MappingStrategy {
13    /// Linear mapping: `min + value * (max - min)`
14    Linear,
15    /// Exponential mapping: `min + value^exp * (max - min)`
16    Exponential {
17        /// Exponent for the exponential curve. Values > 1 emphasize the upper
18        /// end; values < 1 emphasize the lower end.
19        exponent: f32,
20    },
21    /// Logarithmic mapping: `min + log(1 + value * (e - 1)) / log(e) * (max - min)`
22    Logarithmic,
23    /// Inverted linear mapping: `max - value * (max - min)`
24    Inverted,
25}
26
27impl MappingStrategy {
28    /// Map a normalized value `x` in [0, 1\] to [min, max] using this strategy.
29    pub fn map<T: Transcendental>(&self, x: T, min: T, max: T) -> T {
30        let xf: f32 = x.to_f32();
31        let minf: f32 = min.to_f32();
32        let maxf: f32 = max.to_f32();
33        let range = maxf - minf;
34        let result = match self {
35            MappingStrategy::Linear => minf + xf * range,
36            MappingStrategy::Exponential { exponent } => minf + xf.powf(*exponent) * range,
37            MappingStrategy::Logarithmic => {
38                let one = 1.0f32;
39                let mapped =
40                    (one + xf * (core::f32::consts::E - one)).ln() / core::f32::consts::E.ln();
41                minf + mapped * range
42            }
43            MappingStrategy::Inverted => maxf - xf * range,
44        };
45        T::from_f32(result)
46    }
47}
48
49/// Maps an incoming normalized control value [0, 1\] to a parameter range
50/// using a `MappingStrategy`.
51///
52/// Implements `Algorithm<T>`. The input (if present) is treated as the
53/// normalized value; when `input` is `None` (source mode), the value
54/// received via `apply_command()` is used instead.
55///
56/// # Example
57/// ```rust
58/// use rill_core_dsp::mapping::{ControlMapper, MappingStrategy};
59/// use rill_core::traits::Algorithm;
60/// use rill_core::time::ClockTick;
61/// use rill_core::traits::ActionContext;
62///
63/// let mut mapper = ControlMapper::new(20.0, 20000.0, MappingStrategy::Exponential { exponent: 2.0 });
64/// let tick = ClockTick::default();
65/// let ctx = ActionContext::new(&tick);
66///
67/// // Use apply_command to set the incoming value
68/// mapper.apply_command(0.5);    // halfway in normalized range
69/// let mut output = [0.0f32; 1];
70/// mapper.process(None, &mut output).unwrap();
71/// // output maps 0.5 exponentially between 20..20000
72/// ```
73#[derive(Debug, Clone)]
74pub struct ControlMapper<T: Transcendental> {
75    /// Minimum of the output range
76    min: T,
77    /// Maximum of the output range
78    max: T,
79    /// Mapping strategy
80    strategy: MappingStrategy,
81    /// Current incoming normalized value
82    value: T,
83}
84
85impl<T: Transcendental> ControlMapper<T> {
86    /// Create a new `ControlMapper`.
87    pub fn new(min: T, max: T, strategy: MappingStrategy) -> Self {
88        Self {
89            min,
90            max,
91            strategy,
92            value: T::ZERO,
93        }
94    }
95
96    /// Update the mapping range.
97    pub fn set_range(&mut self, min: T, max: T) {
98        self.min = min;
99        self.max = max;
100    }
101
102    /// Update the mapping strategy.
103    pub fn set_strategy(&mut self, strategy: MappingStrategy) {
104        self.strategy = strategy;
105    }
106
107    /// Get the current mapped value (without processing).
108    pub fn current_mapped(&self) -> T {
109        self.strategy.map(self.value, self.min, self.max)
110    }
111}
112
113impl<T: Transcendental> Algorithm<T> for ControlMapper<T> {
114    fn process(&mut self, input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
115        for (i, sample) in output.iter_mut().enumerate() {
116            // If input is available, use it as the normalized value.
117            // Otherwise, use the value set by apply_command.
118            let normalized = match input {
119                Some(buf) => {
120                    if i < buf.len() {
121                        buf[i]
122                    } else {
123                        self.value
124                    }
125                }
126                None => self.value,
127            };
128            *sample = self.strategy.map(normalized, self.min, self.max);
129        }
130        Ok(())
131    }
132
133    fn apply_command(&mut self, value: T) {
134        self.value = value;
135    }
136
137    fn init(&mut self, _sample_rate: f32) {}
138
139    fn reset(&mut self) {
140        self.value = T::ZERO;
141    }
142
143    fn metadata(&self) -> AlgorithmMetadata {
144        AlgorithmMetadata {
145            name: "ControlMapper",
146            category: AlgorithmCategory::Utility,
147            description: "Maps normalized [0,1] control values to a parameter range",
148            author: "Rill",
149            version: "0.1.0",
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_linear_mapping() {
160        let mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
161        assert!((mapper.current_mapped() - 0.0).abs() < 1e-6);
162    }
163
164    #[test]
165    fn test_mapping_strategies() {
166        let mut mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
167        mapper.apply_command(0.5);
168        let mut out = [0.0f32];
169        mapper.process(None, &mut out).unwrap();
170        assert!((out[0] - 50.0).abs() < 1e-6);
171
172        mapper.set_strategy(MappingStrategy::Inverted);
173        mapper.apply_command(0.5);
174        mapper.process(None, &mut out).unwrap();
175        assert!((out[0] - 50.0).abs() < 1e-6);
176
177        mapper.set_strategy(MappingStrategy::Exponential { exponent: 2.0 });
178        mapper.apply_command(0.5); // 0.5^2 = 0.25, 0..100 => 25
179        mapper.process(None, &mut out).unwrap();
180        assert!((out[0] - 25.0).abs() < 1e-6);
181    }
182
183    #[test]
184    fn test_mapping_with_input() {
185        let mut mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
186        let input = [0.25f32, 0.75f32];
187        let mut output = [0.0f32; 2];
188        mapper.process(Some(&input), &mut output).unwrap();
189        assert!((output[0] - 25.0).abs() < 1e-6);
190        assert!((output[1] - 75.0).abs() < 1e-6);
191    }
192
193    #[test]
194    fn test_log_mapping_bounds() {
195        let mut mapper = ControlMapper::new(20.0f32, 20000.0, MappingStrategy::Logarithmic);
196        mapper.apply_command(0.0);
197        let mut out = [0.0f32];
198        mapper.process(None, &mut out).unwrap();
199        assert!((out[0] - 20.0).abs() < 1.0);
200
201        mapper.apply_command(1.0);
202        mapper.process(None, &mut out).unwrap();
203        assert!((out[0] - 20000.0).abs() < 1.0);
204    }
205}