1use rill_core::math::Transcendental;
7use rill_core::traits::ProcessResult;
8use rill_core::traits::{ActionContext, Algorithm, AlgorithmCategory, AlgorithmMetadata};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum MappingStrategy {
13 Linear,
15 Exponential { exponent: f32 },
17 Logarithmic,
19 Inverted,
21}
22
23impl MappingStrategy {
24 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#[derive(Debug, Clone)]
70pub struct ControlMapper<T: Transcendental> {
71 min: T,
73 max: T,
75 strategy: MappingStrategy,
77 value: T,
79}
80
81impl<T: Transcendental> ControlMapper<T> {
82 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 pub fn set_range(&mut self, min: T, max: T) {
94 self.min = min;
95 self.max = max;
96 }
97
98 pub fn set_strategy(&mut self, strategy: MappingStrategy) {
100 self.strategy = strategy;
101 }
102
103 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 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); 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}