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