wavecraft_dev_server/audio/
atomic_params.rs1use atomic_float::AtomicF32;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::Ordering;
13use wavecraft_protocol::ParameterInfo;
14
15pub struct AtomicParameterBridge {
22 params: HashMap<String, Arc<AtomicF32>>,
23}
24
25impl AtomicParameterBridge {
26 pub fn new(parameters: &[ParameterInfo]) -> Self {
30 let params = parameters
31 .iter()
32 .map(|p| (p.id.clone(), Arc::new(AtomicF32::new(p.default))))
33 .collect();
34 Self { params }
35 }
36
37 pub fn write(&self, id: &str, value: f32) {
43 if let Some(atomic) = self.params.get(id) {
44 atomic.store(value, Ordering::Relaxed);
45 }
46 }
47
48 pub fn read(&self, id: &str) -> Option<f32> {
53 self.params.get(id).map(|a| a.load(Ordering::Relaxed))
54 }
55}
56
57const _: () = {
62 fn _assert_send_sync<T: Send + Sync>() {}
63 fn _check() {
64 _assert_send_sync::<AtomicParameterBridge>();
65 }
66};
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use wavecraft_protocol::ParameterType;
72
73 fn test_params() -> Vec<ParameterInfo> {
74 vec![
75 ParameterInfo {
76 id: "gain".to_string(),
77 name: "Gain".to_string(),
78 param_type: ParameterType::Float,
79 value: 0.5,
80 default: 0.5,
81 min: 0.0,
82 max: 1.0,
83 unit: Some("dB".to_string()),
84 group: Some("Input".to_string()),
85 },
86 ParameterInfo {
87 id: "mix".to_string(),
88 name: "Mix".to_string(),
89 param_type: ParameterType::Float,
90 value: 1.0,
91 default: 1.0,
92 min: 0.0,
93 max: 1.0,
94 unit: Some("%".to_string()),
95 group: None,
96 },
97 ]
98 }
99
100 #[test]
101 fn test_default_values() {
102 let bridge = AtomicParameterBridge::new(&test_params());
103
104 let gain = bridge.read("gain").expect("gain should exist");
105 assert!(
106 (gain - 0.5).abs() < f32::EPSILON,
107 "gain default should be 0.5"
108 );
109
110 let mix = bridge.read("mix").expect("mix should exist");
111 assert!(
112 (mix - 1.0).abs() < f32::EPSILON,
113 "mix default should be 1.0"
114 );
115 }
116
117 #[test]
118 fn test_write_and_read() {
119 let bridge = AtomicParameterBridge::new(&test_params());
120
121 bridge.write("gain", 0.75);
122 let gain = bridge.read("gain").expect("gain should exist");
123 assert!(
124 (gain - 0.75).abs() < f32::EPSILON,
125 "gain should be updated to 0.75"
126 );
127 }
128
129 #[test]
130 fn test_read_unknown_param() {
131 let bridge = AtomicParameterBridge::new(&test_params());
132 assert!(
133 bridge.read("nonexistent").is_none(),
134 "unknown param should return None"
135 );
136 }
137
138 #[test]
139 fn test_write_unknown_param_is_noop() {
140 let bridge = AtomicParameterBridge::new(&test_params());
141 bridge.write("nonexistent", 0.5);
143 }
144
145 #[test]
146 fn test_concurrent_write_read() {
147 use std::sync::Arc;
148 use std::thread;
149
150 let bridge = Arc::new(AtomicParameterBridge::new(&test_params()));
151
152 let writer = {
153 let bridge = Arc::clone(&bridge);
154 thread::spawn(move || {
155 for i in 0..1000 {
156 bridge.write("gain", i as f32 / 1000.0);
157 }
158 })
159 };
160
161 let reader = {
162 let bridge = Arc::clone(&bridge);
163 thread::spawn(move || {
164 for _ in 0..1000 {
165 let val = bridge.read("gain");
166 assert!(val.is_some(), "gain should always be readable");
167 let v = val.unwrap();
168 assert!(
169 (0.0..=1.0).contains(&v) || v == 0.5,
170 "value should be in range"
171 );
172 }
173 })
174 };
175
176 writer.join().expect("writer thread should not panic");
177 reader.join().expect("reader thread should not panic");
178 }
179}