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}