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 {
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, &ctx).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(
115        &mut self,
116        input: Option<&[T]>,
117        output: &mut [T],
118        _ctx: &ActionContext,
119    ) -> ProcessResult<()> {
120        for (i, sample) in output.iter_mut().enumerate() {
121            // If input is available, use it as the normalized value.
122            // Otherwise, use the value set by apply_command.
123            let normalized = match input {
124                Some(buf) => {
125                    if i < buf.len() {
126                        buf[i]
127                    } else {
128                        self.value
129                    }
130                }
131                None => self.value,
132            };
133            *sample = self.strategy.map(normalized, self.min, self.max);
134        }
135        Ok(())
136    }
137
138    fn apply_command(&mut self, value: T) {
139        self.value = value;
140    }
141
142    fn init(&mut self, _sample_rate: f32) {}
143
144    fn reset(&mut self) {
145        self.value = T::ZERO;
146    }
147
148    fn metadata(&self) -> AlgorithmMetadata {
149        AlgorithmMetadata {
150            name: "ControlMapper",
151            category: AlgorithmCategory::Utility,
152            description: "Maps normalized [0,1] control values to a parameter range",
153            author: "Rill",
154            version: "0.1.0",
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use rill_core::time::ClockTick;
163
164    #[test]
165    fn test_linear_mapping() {
166        let mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
167        assert!((mapper.current_mapped() - 0.0).abs() < 1e-6);
168    }
169
170    #[test]
171    fn test_mapping_strategies() {
172        let tick = ClockTick::default();
173        let ctx = ActionContext::new(&tick);
174
175        let mut mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
176        mapper.apply_command(0.5);
177        let mut out = [0.0f32];
178        mapper.process(None, &mut out, &ctx).unwrap();
179        assert!((out[0] - 50.0).abs() < 1e-6);
180
181        mapper.set_strategy(MappingStrategy::Inverted);
182        mapper.apply_command(0.5);
183        mapper.process(None, &mut out, &ctx).unwrap();
184        assert!((out[0] - 50.0).abs() < 1e-6);
185
186        mapper.set_strategy(MappingStrategy::Exponential { exponent: 2.0 });
187        mapper.apply_command(0.5); // 0.5^2 = 0.25, 0..100 => 25
188        mapper.process(None, &mut out, &ctx).unwrap();
189        assert!((out[0] - 25.0).abs() < 1e-6);
190    }
191
192    #[test]
193    fn test_mapping_with_input() {
194        let tick = ClockTick::default();
195        let ctx = ActionContext::new(&tick);
196
197        let mut mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
198        let input = [0.25f32, 0.75f32];
199        let mut output = [0.0f32; 2];
200        mapper.process(Some(&input), &mut output, &ctx).unwrap();
201        assert!((output[0] - 25.0).abs() < 1e-6);
202        assert!((output[1] - 75.0).abs() < 1e-6);
203    }
204
205    #[test]
206    fn test_log_mapping_bounds() {
207        let tick = ClockTick::default();
208        let ctx = ActionContext::new(&tick);
209
210        let mut mapper = ControlMapper::new(20.0f32, 20000.0, MappingStrategy::Logarithmic);
211        mapper.apply_command(0.0);
212        let mut out = [0.0f32];
213        mapper.process(None, &mut out, &ctx).unwrap();
214        assert!((out[0] - 20.0).abs() < 1.0);
215
216        mapper.apply_command(1.0);
217        mapper.process(None, &mut out, &ctx).unwrap();
218        assert!((out[0] - 20000.0).abs() < 1.0);
219    }
220}