1use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
2
3use crate::info::ParamInfo;
4use crate::sample::Float;
5use crate::smooth::{Smoother, SmoothingStyle};
6
7pub struct AtomicF64 {
9 bits: AtomicU64,
10}
11
12impl AtomicF64 {
13 pub fn new(value: f64) -> Self {
14 Self {
15 bits: AtomicU64::new(value.to_bits()),
16 }
17 }
18
19 #[inline]
20 pub fn load(&self) -> f64 {
21 f64::from_bits(self.bits.load(Ordering::Relaxed))
22 }
23
24 #[inline]
25 pub fn store(&self, value: f64) {
26 self.bits.store(value.to_bits(), Ordering::Relaxed);
27 }
28}
29
30pub struct FloatParam {
32 pub info: ParamInfo,
33 value: AtomicF64,
34 pub smoother: Smoother,
35}
36
37impl FloatParam {
38 #[must_use]
39 pub fn new(info: ParamInfo, smoothing: SmoothingStyle) -> Self {
40 let default = info.default_plain;
41 let smoother = Smoother::new(smoothing);
42 smoother.snap(default);
43 Self {
44 info,
45 value: AtomicF64::new(default),
46 smoother,
47 }
48 }
49
50 #[inline]
52 pub fn set_value(&self, v: f64) {
53 self.value.store(v);
54 }
55
56 #[doc(hidden)]
62 #[inline]
63 pub fn raw_target(&self) -> f64 {
64 self.value.load()
65 }
66
67 #[doc(hidden)]
70 #[inline]
71 pub fn raw_smoothed_next(&self) -> f32 {
72 let target = self.value.load();
73 self.smoother.next(target)
74 }
75
76 #[doc(hidden)]
79 #[inline]
80 pub fn raw_smoothed_current(&self) -> f32 {
81 self.smoother.current()
82 }
83
84 #[doc(hidden)]
88 #[inline]
89 pub fn raw_smoothed_next_block<const N: usize>(&self) -> [f32; N] {
90 let target = self.value.load();
91 self.smoother.next_block::<N>(target)
92 }
93
94 #[doc(hidden)]
99 #[inline]
100 pub fn raw_smoothed_next_into(&self, out: &mut [f32]) {
101 let target = self.value.load();
102 self.smoother.next_into(target, out);
103 }
104
105 #[doc(hidden)]
110 #[inline]
111 pub fn raw_smoothed_next_after(&self, n_samples: usize) -> f32 {
112 let target = self.value.load();
113 self.smoother.next_after(target, n_samples)
114 }
115
116 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
120 #[inline]
121 pub fn value_usize(&self) -> usize {
122 let v = self.value.load().round();
123 if v <= 0.0 { 0 } else { v as usize }
124 }
125
126 #[allow(clippy::cast_possible_truncation)]
129 #[inline]
130 pub fn value_i32(&self) -> i32 {
131 self.value.load().round() as i32
132 }
133
134 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
137 #[inline]
138 pub fn value_u8(&self) -> u8 {
139 let v = self.value.load().round();
140 if v <= 0.0 {
141 0
142 } else if v >= 255.0 {
143 255
144 } else {
145 v as u8
146 }
147 }
148
149 #[inline]
168 #[must_use]
169 pub fn is_smoothing(&self) -> bool {
170 !self.smoother.is_converged(self.value.load())
171 }
172
173 pub fn id(&self) -> u32 {
175 self.info.id
176 }
177}
178
179pub trait FloatParamReadF32 {
197 #[must_use]
199 fn read(&self) -> f32;
200
201 #[must_use]
215 #[deprecated(
216 since = "0.53.0",
217 note = "use `read_into(&mut scratch[..n])` instead; \
218 `read_block::<N>` advances the smoother by N regardless \
219 of how many samples the caller consumes, which steps the \
220 value at the next block boundary when the host's block \
221 size isn't a multiple of N"
222 )]
223 fn read_block<const N: usize>(&self) -> [f32; N];
224
225 fn read_into(&self, out: &mut [f32]);
242
243 #[must_use]
253 fn read_after(&self, n_samples: usize) -> f32;
254
255 #[must_use]
257 fn current(&self) -> f32;
258
259 #[must_use]
263 fn value(&self) -> f32;
264}
265
266pub trait FloatParamReadF64 {
269 #[must_use]
270 fn read(&self) -> f64;
271 #[must_use]
275 #[deprecated(
276 since = "0.53.0",
277 note = "use `read_into(&mut scratch[..n])` instead; \
278 `read_block::<N>` advances the smoother by N regardless \
279 of how many samples the caller consumes"
280 )]
281 fn read_block<const N: usize>(&self) -> [f64; N];
282 fn read_into(&self, out: &mut [f64]);
285 #[must_use]
288 fn read_after(&self, n_samples: usize) -> f64;
289 #[must_use]
290 fn current(&self) -> f64;
291 #[must_use]
292 fn value(&self) -> f64;
293}
294
295impl FloatParamReadF32 for FloatParam {
296 #[inline]
297 fn read(&self) -> f32 {
298 self.raw_smoothed_next()
299 }
300
301 #[inline]
302 fn read_block<const N: usize>(&self) -> [f32; N] {
303 self.raw_smoothed_next_block::<N>()
304 }
305
306 #[inline]
307 fn read_into(&self, out: &mut [f32]) {
308 self.raw_smoothed_next_into(out);
309 }
310
311 #[inline]
312 fn read_after(&self, n_samples: usize) -> f32 {
313 self.raw_smoothed_next_after(n_samples)
314 }
315
316 #[inline]
317 fn current(&self) -> f32 {
318 self.raw_smoothed_current()
319 }
320
321 #[inline]
322 fn value(&self) -> f32 {
323 f32::from_f64(self.raw_target())
324 }
325}
326
327impl FloatParamReadF64 for FloatParam {
328 #[inline]
329 fn read(&self) -> f64 {
330 f64::from(self.raw_smoothed_next())
331 }
332
333 #[inline]
334 fn read_block<const N: usize>(&self) -> [f64; N] {
335 let block = self.raw_smoothed_next_block::<N>();
336 let mut out = [0.0_f64; N];
337 for (i, &v) in block.iter().enumerate() {
338 out[i] = f64::from(v);
339 }
340 out
341 }
342
343 #[inline]
344 fn read_into(&self, out: &mut [f64]) {
345 const SCRATCH: usize = 1024;
351 let mut scratch = [0.0_f32; SCRATCH];
352 let mut remaining = out;
353 while !remaining.is_empty() {
354 let take = remaining.len().min(SCRATCH);
355 self.raw_smoothed_next_into(&mut scratch[..take]);
356 for (dst, &src) in remaining[..take].iter_mut().zip(&scratch[..take]) {
357 *dst = f64::from(src);
358 }
359 remaining = &mut remaining[take..];
360 }
361 }
362
363 #[inline]
364 fn read_after(&self, n_samples: usize) -> f64 {
365 f64::from(self.raw_smoothed_next_after(n_samples))
366 }
367
368 #[inline]
369 fn current(&self) -> f64 {
370 f64::from(self.raw_smoothed_current())
371 }
372
373 #[inline]
374 fn value(&self) -> f64 {
375 self.raw_target()
376 }
377}
378
379pub struct BoolParam {
381 pub info: ParamInfo,
382 value: AtomicBool,
383}
384
385impl BoolParam {
386 #[must_use]
393 pub fn new(info: ParamInfo) -> Self {
394 let default = match info.default_plain {
395 0.0 => false,
396 1.0 => true,
397 other => panic!(
398 "BoolParam '{}' default {} must be exactly 0.0 (false) \
399 or 1.0 (true) - bool params have no halfway value",
400 info.name, other,
401 ),
402 };
403 Self {
404 info,
405 value: AtomicBool::new(default),
406 }
407 }
408
409 pub fn value(&self) -> bool {
410 self.value.load(Ordering::Relaxed)
411 }
412
413 pub fn set_value(&self, v: bool) {
414 self.value.store(v, Ordering::Relaxed);
415 }
416
417 pub fn id(&self) -> u32 {
418 self.info.id
419 }
420}
421
422pub struct IntParam {
424 pub info: ParamInfo,
425 value: AtomicI64,
426}
427
428impl IntParam {
429 #[allow(
443 clippy::float_cmp,
444 clippy::cast_possible_truncation,
445 clippy::cast_precision_loss
446 )]
447 #[must_use]
448 pub fn new(info: ParamInfo) -> Self {
449 let default = info.default_plain;
450 assert!(
451 default.is_finite(),
452 "IntParam '{}' default {} is not finite",
453 info.name,
454 default,
455 );
456 let truncated = default as i64;
457 assert!(
458 truncated as f64 == default,
459 "IntParam '{}' default {} doesn't round-trip through i64 \
460 - supply an integer-valued default in the derive attribute",
461 info.name,
462 default,
463 );
464 let (lo, hi) = (info.range.min() as i64, info.range.max() as i64);
465 assert!(
466 truncated >= lo && truncated <= hi,
467 "IntParam '{}' default {} is outside range [{}, {}]",
468 info.name,
469 truncated,
470 lo,
471 hi,
472 );
473 Self {
474 info,
475 value: AtomicI64::new(truncated),
476 }
477 }
478
479 pub fn value(&self) -> i64 {
480 self.value.load(Ordering::Relaxed)
481 }
482
483 #[allow(clippy::cast_precision_loss)]
486 #[inline]
487 pub fn value_f32(&self) -> f32 {
488 self.value.load(Ordering::Relaxed) as f32
489 }
490
491 #[allow(clippy::cast_precision_loss)]
493 #[inline]
494 pub fn value_f64(&self) -> f64 {
495 self.value.load(Ordering::Relaxed) as f64
496 }
497
498 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
501 #[inline]
502 pub fn value_usize(&self) -> usize {
503 let v = self.value.load(Ordering::Relaxed);
504 if v <= 0 { 0 } else { v as usize }
505 }
506
507 #[allow(clippy::cast_possible_truncation)]
509 #[inline]
510 pub fn value_i32(&self) -> i32 {
511 self.value
512 .load(Ordering::Relaxed)
513 .clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32
514 }
515
516 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
518 #[inline]
519 pub fn value_u8(&self) -> u8 {
520 self.value.load(Ordering::Relaxed).clamp(0, 255) as u8
521 }
522
523 pub fn set_value(&self, v: i64) {
524 self.value.store(v, Ordering::Relaxed);
525 }
526
527 pub fn id(&self) -> u32 {
528 self.info.id
529 }
530}
531
532pub trait ParamEnum: crate::__private::Sealed + Clone + Copy + Send + Sync + 'static {
534 fn from_index(index: usize) -> Self;
535 fn to_index(&self) -> usize;
536 fn name(&self) -> &'static str;
537 fn variant_count() -> usize;
538 fn variant_names() -> &'static [&'static str];
539}
540
541pub struct EnumParam<E: ParamEnum> {
543 pub info: ParamInfo,
544 value: AtomicU32,
545 _phantom: std::marker::PhantomData<E>,
546}
547
548impl<E: ParamEnum> EnumParam<E> {
549 #[allow(
561 clippy::float_cmp,
562 clippy::cast_possible_truncation,
563 clippy::cast_sign_loss
564 )]
565 #[must_use]
566 pub fn new(info: ParamInfo) -> Self {
567 let default = info.default_plain;
568 let count = E::variant_count();
569 assert!(
570 default.is_finite(),
571 "EnumParam '{}' default {} is not finite",
572 info.name,
573 default,
574 );
575 assert!(
576 default >= 0.0,
577 "EnumParam '{}' default {} is negative; enum variants are \
578 0-indexed",
579 info.name,
580 default,
581 );
582 let idx = default as u32;
583 assert!(
584 f64::from(idx) == default,
585 "EnumParam '{}' default {} is non-integer; supply a 0-indexed \
586 variant index",
587 info.name,
588 default,
589 );
590 assert!(
591 (idx as usize) < count,
592 "EnumParam '{}' default {} is out of range; only {} variant(s) \
593 defined",
594 info.name,
595 idx,
596 count,
597 );
598 Self {
599 info,
600 value: AtomicU32::new(idx),
601 _phantom: std::marker::PhantomData,
602 }
603 }
604
605 pub fn value(&self) -> E {
606 #[allow(clippy::cast_possible_truncation)]
609 let idx = self.value.load(Ordering::Relaxed) as usize;
610 E::from_index(idx)
611 }
612
613 pub fn set_value(&self, v: E) {
614 #[allow(clippy::cast_possible_truncation)]
618 let idx = v.to_index() as u32;
619 self.value.store(idx, Ordering::Relaxed);
620 }
621
622 pub fn set_index(&self, idx: u32) {
623 self.value.store(idx, Ordering::Relaxed);
624 }
625
626 pub fn index(&self) -> u32 {
627 self.value.load(Ordering::Relaxed)
628 }
629
630 pub fn id(&self) -> u32 {
631 self.info.id
632 }
633
634 #[must_use]
641 pub fn format_by_index(value: f64) -> String {
642 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
645 let idx = value.round() as usize;
646 E::from_index(idx).name().to_string()
647 }
648}
649
650pub struct MeterSlot {
670 #[doc(hidden)]
671 pub id: u32,
672}
673
674impl MeterSlot {
675 #[must_use]
676 pub fn id(&self) -> u32 {
677 self.id
678 }
679}
680
681impl From<MeterSlot> for u32 {
682 fn from(m: MeterSlot) -> u32 {
683 m.id
684 }
685}
686
687impl From<&MeterSlot> for u32 {
688 fn from(m: &MeterSlot) -> u32 {
689 m.id
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use crate::info::{ParamFlags, ParamUnit, ParamValueKind};
697 use crate::range::ParamRange;
698
699 fn info(name: &'static str, range: ParamRange, default_plain: f64) -> ParamInfo {
700 ParamInfo {
701 id: 0,
702 name,
703 short_name: name,
704 group: "",
705 range,
706 default_plain,
707 flags: ParamFlags::AUTOMATABLE,
708 unit: ParamUnit::None,
709 kind: ParamValueKind::Float,
710 }
711 }
712
713 #[derive(Clone, Copy)]
714 enum E4 {
715 A,
716 B,
717 C,
718 D,
719 }
720 impl crate::__private::Sealed for E4 {}
721 impl ParamEnum for E4 {
722 fn from_index(i: usize) -> Self {
723 match i {
724 0 => Self::A,
725 1 => Self::B,
726 2 => Self::C,
727 _ => Self::D,
728 }
729 }
730 fn to_index(&self) -> usize {
731 *self as usize
732 }
733 fn name(&self) -> &'static str {
734 match self {
735 Self::A => "A",
736 Self::B => "B",
737 Self::C => "C",
738 Self::D => "D",
739 }
740 }
741 fn variant_count() -> usize {
742 4
743 }
744 fn variant_names() -> &'static [&'static str] {
745 &["A", "B", "C", "D"]
746 }
747 }
748
749 #[test]
750 fn enum_param_accepts_in_range_default() {
751 let p: EnumParam<E4> = EnumParam::new(info("Mode", ParamRange::Enum { count: 4 }, 2.0));
752 assert_eq!(p.index(), 2);
753 }
754
755 #[test]
756 #[should_panic(expected = "negative")]
757 fn enum_param_rejects_negative_default() {
758 let _: EnumParam<E4> = EnumParam::new(info("Mode", ParamRange::Enum { count: 4 }, -1.0));
759 }
760
761 #[test]
762 #[should_panic(expected = "out of range")]
763 fn enum_param_rejects_overflow_default() {
764 let _: EnumParam<E4> = EnumParam::new(info("Mode", ParamRange::Enum { count: 4 }, 99.0));
765 }
766
767 #[test]
768 #[should_panic(expected = "non-integer")]
769 fn enum_param_rejects_fractional_default() {
770 let _: EnumParam<E4> = EnumParam::new(info("Mode", ParamRange::Enum { count: 4 }, 1.5));
771 }
772
773 #[test]
774 fn int_param_accepts_negative_default() {
775 let p = IntParam::new(info("N", ParamRange::Discrete { min: -10, max: 10 }, -3.0));
776 assert_eq!(p.value(), -3);
777 }
778
779 #[test]
780 #[should_panic(expected = "round-trip")]
781 fn int_param_rejects_fractional_default() {
782 let _ = IntParam::new(info("N", ParamRange::Discrete { min: 0, max: 10 }, 1.5));
783 }
784
785 #[test]
786 #[should_panic(expected = "outside range")]
787 fn int_param_rejects_out_of_range_default() {
788 let _ = IntParam::new(info("N", ParamRange::Discrete { min: 0, max: 5 }, 10.0));
789 }
790}