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 {
17 exponent: f32,
20 },
21 Logarithmic,
23 Inverted,
25}
26
27impl MappingStrategy {
28 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#[derive(Debug, Clone)]
74pub struct ControlMapper<T: Transcendental> {
75 min: T,
77 max: T,
79 strategy: MappingStrategy,
81 value: T,
83}
84
85impl<T: Transcendental> ControlMapper<T> {
86 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 pub fn set_range(&mut self, min: T, max: T) {
98 self.min = min;
99 self.max = max;
100 }
101
102 pub fn set_strategy(&mut self, strategy: MappingStrategy) {
104 self.strategy = strategy;
105 }
106
107 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 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); 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}