beamer_core/
param_range.rs

1//! Range mapping for parameter normalization.
2//!
3//! This module provides traits and implementations for mapping between
4//! plain parameter values (in natural units like Hz, dB, ms) and normalized
5//! values (0.0 to 1.0) used for host communication.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use beamer_core::param_range::{RangeMapper, LinearMapper, LogMapper};
11//!
12//! // Linear mapping for most parameters
13//! let linear = LinearMapper::new(0.0..=100.0);
14//! assert_eq!(linear.normalize(50.0), 0.5);
15//! assert_eq!(linear.denormalize(0.5), 50.0);
16//!
17//! // Logarithmic mapping for frequency parameters
18//! let log = LogMapper::new(20.0..=20000.0);
19//! // 632 Hz is roughly the geometric mean of 20 and 20000
20//! assert!((log.denormalize(0.5) - 632.0).abs() < 1.0);
21//! ```
22
23use std::ops::RangeInclusive;
24
25/// Trait for mapping between plain values and normalized values.
26///
27/// Implementations must be thread-safe (`Send + Sync`) for use in
28/// audio plugin parameters.
29pub trait RangeMapper: Send + Sync {
30    /// Convert a plain value to normalized (0.0-1.0).
31    ///
32    /// Values outside the range are clamped.
33    fn normalize(&self, plain: f64) -> f64;
34
35    /// Convert a normalized value (0.0-1.0) to plain.
36    ///
37    /// Values outside 0.0-1.0 are clamped.
38    fn denormalize(&self, normalized: f64) -> f64;
39
40    /// Get the plain value range as (min, max).
41    fn range(&self) -> (f64, f64);
42
43    /// Get the default normalized value for a given plain default.
44    fn default_normalized(&self, plain_default: f64) -> f64 {
45        self.normalize(plain_default)
46    }
47}
48
49/// Linear range mapping.
50///
51/// Maps values linearly between the range endpoints.
52/// Suitable for most parameters where perceptual response is linear.
53///
54/// # Example
55///
56/// ```ignore
57/// let mapper = LinearMapper::new(-60.0..=12.0);
58/// assert_eq!(mapper.denormalize(0.0), -60.0);
59/// assert_eq!(mapper.denormalize(1.0), 12.0);
60/// ```
61#[derive(Debug, Clone)]
62pub struct LinearMapper {
63    min: f64,
64    max: f64,
65}
66
67impl LinearMapper {
68    /// Create a new linear mapper with the given range.
69    pub fn new(range: RangeInclusive<f64>) -> Self {
70        Self {
71            min: *range.start(),
72            max: *range.end(),
73        }
74    }
75}
76
77impl RangeMapper for LinearMapper {
78    fn normalize(&self, plain: f64) -> f64 {
79        if (self.max - self.min).abs() < f64::EPSILON {
80            return 0.5;
81        }
82        ((plain - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
83    }
84
85    fn denormalize(&self, normalized: f64) -> f64 {
86        let normalized = normalized.clamp(0.0, 1.0);
87        self.min + normalized * (self.max - self.min)
88    }
89
90    fn range(&self) -> (f64, f64) {
91        (self.min, self.max)
92    }
93}
94
95/// Logarithmic range mapping.
96///
97/// Maps values logarithmically between the range endpoints.
98/// Suitable for frequency parameters where perceptual response is logarithmic.
99///
100/// # Panics
101///
102/// Panics if the range contains zero or negative values, as logarithm
103/// is undefined for non-positive numbers.
104///
105/// # Example
106///
107/// ```ignore
108/// let mapper = LogMapper::new(20.0..=20000.0);
109/// // Geometric mean is at normalized 0.5
110/// let mid = mapper.denormalize(0.5);
111/// assert!((mid - 632.45).abs() < 1.0); // sqrt(20 * 20000)
112/// ```
113#[derive(Debug, Clone)]
114pub struct LogMapper {
115    min: f64,
116    max: f64,
117    min_log: f64,
118    max_log: f64,
119}
120
121impl LogMapper {
122    /// Create a new logarithmic mapper with the given range.
123    ///
124    /// # Panics
125    ///
126    /// Panics if:
127    /// - The range start is not positive (log requires positive values)
128    /// - The range end is not greater than the range start
129    pub fn new(range: RangeInclusive<f64>) -> Self {
130        let min = *range.start();
131        let max = *range.end();
132        assert!(
133            min > 0.0,
134            "LogMapper requires positive range start, got min={}",
135            min
136        );
137        assert!(
138            max > min,
139            "LogMapper requires max > min, got min={}, max={}",
140            min, max
141        );
142        Self {
143            min,
144            max,
145            min_log: min.ln(),
146            max_log: max.ln(),
147        }
148    }
149}
150
151impl RangeMapper for LogMapper {
152    fn normalize(&self, plain: f64) -> f64 {
153        if (self.max_log - self.min_log).abs() < f64::EPSILON {
154            return 0.5;
155        }
156        let plain = plain.max(self.min); // Clamp to positive
157        let plain_log = plain.ln();
158        ((plain_log - self.min_log) / (self.max_log - self.min_log)).clamp(0.0, 1.0)
159    }
160
161    fn denormalize(&self, normalized: f64) -> f64 {
162        let normalized = normalized.clamp(0.0, 1.0);
163        (self.min_log + normalized * (self.max_log - self.min_log)).exp()
164    }
165
166    fn range(&self) -> (f64, f64) {
167        (self.min, self.max)
168    }
169}