1#[derive(Debug, Clone, Copy, PartialEq, Default)]
23pub enum ParameterScale {
24 #[default]
28 Linear,
29
30 Logarithmic,
36
37 Exponential {
43 curve: f32,
45 },
46
47 Toggle,
51
52 Integer,
56}
57
58#[derive(Debug, Clone)]
63pub struct ParameterRange {
64 pub min: f32,
65 pub max: f32,
66 pub default: f32,
67 pub scale: ParameterScale,
68}
69
70impl ParameterRange {
71 pub fn new(min: f32, max: f32, default: f32, scale: ParameterScale) -> Self {
72 debug_assert!(max > min, "max must be greater than min");
73
74 Self {
75 min,
76 max,
77 default: clamp(default, min, max),
78 scale,
79 }
80 }
81
82 pub fn linear(min: f32, max: f32, default: f32) -> Self {
83 Self::new(min, max, default, ParameterScale::Linear)
84 }
85
86 pub fn logarithmic(min: f32, max: f32, default: f32) -> Self {
92 debug_assert!(min > 0.0, "logarithmic scale requires min > 0");
93 Self::new(min, max, default, ParameterScale::Logarithmic)
94 }
95
96 pub fn exponential(min: f32, max: f32, default: f32, curve: f32) -> Self {
98 Self::new(min, max, default, ParameterScale::Exponential { curve })
99 }
100
101 pub fn toggle(off_value: f32, on_value: f32, default_on: bool) -> Self {
102 Self::new(
103 off_value,
104 on_value,
105 if default_on { on_value } else { off_value },
106 ParameterScale::Toggle,
107 )
108 }
109
110 pub fn integer(min: i32, max: i32, default: i32) -> Self {
111 Self::new(
112 min as f32,
113 max as f32,
114 default as f32,
115 ParameterScale::Integer,
116 )
117 }
118
119 #[inline]
120 pub fn normalize(&self, value: f32) -> f32 {
121 let value = clamp(value, self.min, self.max);
122 let range = self.max - self.min;
123
124 if range <= 0.0 {
125 return 0.0;
126 }
127
128 match self.scale {
129 ParameterScale::Linear => (value - self.min) / range,
130
131 ParameterScale::Logarithmic => {
132 if self.min <= 0.0 {
133 (value - self.min) / range
134 } else {
135 let log_min = libm::logf(self.min);
136 let log_max = libm::logf(self.max);
137 (libm::logf(value) - log_min) / (log_max - log_min)
138 }
139 }
140
141 ParameterScale::Exponential { curve } => {
142 let linear = (value - self.min) / range;
143 if curve <= 0.0 || curve == 1.0 {
144 linear
145 } else {
146 libm::powf(linear, 1.0 / curve)
147 }
148 }
149
150 ParameterScale::Toggle => {
151 if value >= (self.min + self.max) / 2.0 {
152 1.0
153 } else {
154 0.0
155 }
156 }
157
158 ParameterScale::Integer => {
159 let int_value = libm::roundf(value);
160 (int_value - self.min) / range
161 }
162 }
163 }
164
165 #[inline]
166 pub fn denormalize(&self, normalized: f32) -> f32 {
167 let normalized = clamp(normalized, 0.0, 1.0);
168 let range = self.max - self.min;
169
170 match self.scale {
171 ParameterScale::Linear => self.min + normalized * range,
172
173 ParameterScale::Logarithmic => {
174 if self.min <= 0.0 {
175 self.min + normalized * range
176 } else {
177 let log_min = libm::logf(self.min);
178 let log_max = libm::logf(self.max);
179 libm::expf(log_min + normalized * (log_max - log_min))
180 }
181 }
182
183 ParameterScale::Exponential { curve } => {
184 let shaped = if curve <= 0.0 || curve == 1.0 {
185 normalized
186 } else {
187 libm::powf(normalized, curve)
188 };
189 self.min + shaped * range
190 }
191
192 ParameterScale::Toggle => {
193 if normalized >= 0.5 {
194 self.max
195 } else {
196 self.min
197 }
198 }
199
200 ParameterScale::Integer => {
201 let continuous = self.min + normalized * range;
202 libm::roundf(continuous)
203 }
204 }
205 }
206
207 #[inline]
208 pub fn clamp(&self, value: f32) -> f32 {
209 clamp(value, self.min, self.max)
210 }
211
212 #[inline]
213 pub fn default_normalized(&self) -> f32 {
214 self.normalize(self.default)
215 }
216
217 #[inline]
218 pub fn contains(&self, value: f32) -> bool {
219 value >= self.min && value <= self.max
220 }
221
222 #[inline]
223 pub fn span(&self) -> f32 {
224 self.max - self.min
225 }
226
227 #[inline]
228 pub fn db_to_linear(db: f32) -> f32 {
229 libm::powf(10.0, db / 20.0)
230 }
231
232 #[inline]
234 pub fn linear_to_db(linear: f32) -> f32 {
235 if linear <= 0.0 {
236 f32::NEG_INFINITY
237 } else {
238 20.0 * libm::log10f(linear)
239 }
240 }
241}
242
243impl Default for ParameterRange {
244 fn default() -> Self {
245 Self::linear(0.0, 1.0, 0.5)
246 }
247}
248
249#[inline]
251fn clamp(value: f32, min: f32, max: f32) -> f32 {
252 if value < min {
253 min
254 } else if value > max {
255 max
256 } else {
257 value
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 fn approx_eq(a: f32, b: f32) -> bool {
266 let abs_diff = (a - b).abs();
267 let max_val = a.abs().max(b.abs());
268
269 if max_val < 1.0 {
270 abs_diff < 0.0001
271 } else {
272 abs_diff / max_val < 0.00001
273 }
274 }
275
276 #[test]
277 fn test_linear_normalize_denormalize() {
278 let range = ParameterRange::linear(0.0, 100.0, 50.0);
279
280 assert!(approx_eq(range.normalize(0.0), 0.0));
281 assert!(approx_eq(range.normalize(50.0), 0.5));
282 assert!(approx_eq(range.normalize(100.0), 1.0));
283
284 assert!(approx_eq(range.denormalize(0.0), 0.0));
285 assert!(approx_eq(range.denormalize(0.5), 50.0));
286 assert!(approx_eq(range.denormalize(1.0), 100.0));
287 }
288
289 #[test]
290 fn test_linear_roundtrip() {
291 let range = ParameterRange::linear(-10.0, 10.0, 0.0);
292
293 for value in [-10.0, -5.0, 0.0, 5.0, 10.0] {
294 let normalized = range.normalize(value);
295 let back = range.denormalize(normalized);
296 assert!(approx_eq(value, back), "Roundtrip failed for {}", value);
297 }
298 }
299
300 #[test]
301 fn test_logarithmic_normalize_denormalize() {
302 let range = ParameterRange::logarithmic(20.0, 20000.0, 1000.0);
303
304 let mid = range.denormalize(0.5);
305 let expected_mid = libm::sqrtf(20.0 * 20000.0);
306 assert!(
307 approx_eq(mid, expected_mid),
308 "Expected ~{}, got {}",
309 expected_mid,
310 mid
311 );
312
313 assert!(approx_eq(range.denormalize(0.0), 20.0));
314 assert!(approx_eq(range.denormalize(1.0), 20000.0));
315 }
316
317 #[test]
318 fn test_logarithmic_roundtrip() {
319 let range = ParameterRange::logarithmic(20.0, 20000.0, 1000.0);
320
321 for value in [20.0, 100.0, 1000.0, 10000.0, 20000.0] {
322 let normalized = range.normalize(value);
323 let back = range.denormalize(normalized);
324 assert!(
325 (value - back).abs() / value < 0.001,
326 "Roundtrip failed for {}: got {}",
327 value,
328 back
329 );
330 }
331 }
332
333 #[test]
334 fn test_exponential_curve() {
335 let range = ParameterRange::exponential(0.0, 1.0, 0.5, 2.0);
336
337 assert!(approx_eq(range.denormalize(0.5), 0.25));
338 assert!(approx_eq(range.denormalize(0.0), 0.0));
339 assert!(approx_eq(range.denormalize(1.0), 1.0));
340 }
341
342 #[test]
343 fn test_toggle() {
344 let range = ParameterRange::toggle(0.0, 1.0, false);
345
346 assert!(approx_eq(range.denormalize(0.0), 0.0));
347 assert!(approx_eq(range.denormalize(0.49), 0.0));
348 assert!(approx_eq(range.denormalize(0.5), 1.0));
349 assert!(approx_eq(range.denormalize(1.0), 1.0));
350
351 assert!(approx_eq(range.normalize(0.0), 0.0));
352 assert!(approx_eq(range.normalize(1.0), 1.0));
353 }
354
355 #[test]
356 fn test_integer() {
357 let range = ParameterRange::integer(0, 10, 5);
358
359 assert!(approx_eq(range.denormalize(0.0), 0.0));
360 assert!(approx_eq(range.denormalize(0.5), 5.0));
361 assert!(approx_eq(range.denormalize(1.0), 10.0));
362
363 assert!(approx_eq(range.denormalize(0.15), 2.0));
364 assert!(approx_eq(range.denormalize(0.35), 4.0));
365 }
366
367 #[test]
368 fn test_clamp() {
369 let range = ParameterRange::linear(0.0, 100.0, 50.0);
370
371 assert!(approx_eq(range.clamp(-10.0), 0.0));
372 assert!(approx_eq(range.clamp(50.0), 50.0));
373 assert!(approx_eq(range.clamp(110.0), 100.0));
374 }
375
376 #[test]
377 fn test_default_normalized() {
378 let range = ParameterRange::linear(0.0, 100.0, 25.0);
379 assert!(approx_eq(range.default_normalized(), 0.25));
380 }
381
382 #[test]
383 fn test_db_conversion() {
384 assert!(approx_eq(ParameterRange::db_to_linear(0.0), 1.0));
385
386 let minus_6db = ParameterRange::db_to_linear(-6.0);
387 assert!(
388 (minus_6db - 0.5).abs() < 0.02,
389 "Expected ~0.5, got {}",
390 minus_6db
391 );
392
393 let plus_6db = ParameterRange::db_to_linear(6.0);
394 assert!(
395 (plus_6db - 2.0).abs() < 0.05,
396 "Expected ~2.0, got {}",
397 plus_6db
398 );
399
400 let db = -12.0;
401 let linear = ParameterRange::db_to_linear(db);
402 let back = ParameterRange::linear_to_db(linear);
403 assert!(
404 approx_eq(db, back),
405 "dB roundtrip failed: {} -> {}",
406 db,
407 back
408 );
409 }
410
411 #[test]
412 fn test_contains() {
413 let range = ParameterRange::linear(0.0, 100.0, 50.0);
414
415 assert!(range.contains(0.0));
416 assert!(range.contains(50.0));
417 assert!(range.contains(100.0));
418 assert!(!range.contains(-1.0));
419 assert!(!range.contains(101.0));
420 }
421}