use rill_core::math::Transcendental;
use rill_core::traits::ProcessResult;
use rill_core::traits::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MappingStrategy {
Linear,
Exponential {
exponent: f32,
},
Logarithmic,
Inverted,
}
impl MappingStrategy {
pub fn map<T: Transcendental>(&self, x: T, min: T, max: T) -> T {
let xf: f32 = x.to_f32();
let minf: f32 = min.to_f32();
let maxf: f32 = max.to_f32();
let range = maxf - minf;
let result = match self {
MappingStrategy::Linear => minf + xf * range,
MappingStrategy::Exponential { exponent } => minf + xf.powf(*exponent) * range,
MappingStrategy::Logarithmic => {
let one = 1.0f32;
let mapped =
(one + xf * (core::f32::consts::E - one)).ln() / core::f32::consts::E.ln();
minf + mapped * range
}
MappingStrategy::Inverted => maxf - xf * range,
};
T::from_f32(result)
}
}
#[derive(Debug, Clone)]
pub struct ControlMapper<T: Transcendental> {
min: T,
max: T,
strategy: MappingStrategy,
value: T,
}
impl<T: Transcendental> ControlMapper<T> {
pub fn new(min: T, max: T, strategy: MappingStrategy) -> Self {
Self {
min,
max,
strategy,
value: T::ZERO,
}
}
pub fn set_range(&mut self, min: T, max: T) {
self.min = min;
self.max = max;
}
pub fn set_strategy(&mut self, strategy: MappingStrategy) {
self.strategy = strategy;
}
pub fn current_mapped(&self) -> T {
self.strategy.map(self.value, self.min, self.max)
}
}
impl<T: Transcendental> Algorithm<T> for ControlMapper<T> {
fn process(&mut self, input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
for (i, sample) in output.iter_mut().enumerate() {
let normalized = match input {
Some(buf) => {
if i < buf.len() {
buf[i]
} else {
self.value
}
}
None => self.value,
};
*sample = self.strategy.map(normalized, self.min, self.max);
}
Ok(())
}
fn apply_command(&mut self, value: T) {
self.value = value;
}
fn init(&mut self, _sample_rate: f32) {}
fn reset(&mut self) {
self.value = T::ZERO;
}
fn metadata(&self) -> AlgorithmMetadata {
AlgorithmMetadata {
name: "ControlMapper",
category: AlgorithmCategory::Utility,
description: "Maps normalized [0,1] control values to a parameter range",
author: "Rill",
version: "0.1.0",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_mapping() {
let mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
assert!((mapper.current_mapped() - 0.0).abs() < 1e-6);
}
#[test]
fn test_mapping_strategies() {
let mut mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
mapper.apply_command(0.5);
let mut out = [0.0f32];
mapper.process(None, &mut out).unwrap();
assert!((out[0] - 50.0).abs() < 1e-6);
mapper.set_strategy(MappingStrategy::Inverted);
mapper.apply_command(0.5);
mapper.process(None, &mut out).unwrap();
assert!((out[0] - 50.0).abs() < 1e-6);
mapper.set_strategy(MappingStrategy::Exponential { exponent: 2.0 });
mapper.apply_command(0.5); mapper.process(None, &mut out).unwrap();
assert!((out[0] - 25.0).abs() < 1e-6);
}
#[test]
fn test_mapping_with_input() {
let mut mapper = ControlMapper::new(0.0f32, 100.0, MappingStrategy::Linear);
let input = [0.25f32, 0.75f32];
let mut output = [0.0f32; 2];
mapper.process(Some(&input), &mut output).unwrap();
assert!((output[0] - 25.0).abs() < 1e-6);
assert!((output[1] - 75.0).abs() < 1e-6);
}
#[test]
fn test_log_mapping_bounds() {
let mut mapper = ControlMapper::new(20.0f32, 20000.0, MappingStrategy::Logarithmic);
mapper.apply_command(0.0);
let mut out = [0.0f32];
mapper.process(None, &mut out).unwrap();
assert!((out[0] - 20.0).abs() < 1.0);
mapper.apply_command(1.0);
mapper.process(None, &mut out).unwrap();
assert!((out[0] - 20000.0).abs() < 1.0);
}
}