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
15const PARAM_ORDERING: Ordering = Ordering::SeqCst;
18
19pub struct AtomicParameterBridge {
26 params: HashMap<String, Arc<AtomicF32>>,
27 ordered_params: Vec<Arc<AtomicF32>>,
28}
29
30impl AtomicParameterBridge {
31 pub fn new(parameters: &[ParameterInfo]) -> Self {
35 let mut params = HashMap::with_capacity(parameters.len());
36 let mut ordered_params = Vec::with_capacity(parameters.len());
37
38 for parameter in parameters {
39 let atomic = Arc::new(AtomicF32::new(parameter.default));
40 params.insert(parameter.id.clone(), Arc::clone(&atomic));
41 ordered_params.push(atomic);
42 }
43
44 Self {
45 params,
46 ordered_params,
47 }
48 }
49
50 pub fn parameter_count(&self) -> usize {
52 self.ordered_params.len()
53 }
54
55 pub fn write(&self, id: &str, value: f32) {
61 if let Some(atomic) = self.lookup_param(id) {
62 atomic.store(value, PARAM_ORDERING);
63 }
64 }
65
66 pub fn read(&self, id: &str) -> Option<f32> {
71 self.lookup_param(id)
72 .map(|atomic| atomic.load(PARAM_ORDERING))
73 }
74
75 pub fn copy_all_to(&self, output: &mut [f32]) -> usize {
81 let count = output.len().min(self.ordered_params.len());
82 for (idx, atomic) in self.ordered_params.iter().take(count).enumerate() {
83 output[idx] = atomic.load(PARAM_ORDERING);
84 }
85 count
86 }
87
88 fn lookup_param(&self, id: &str) -> Option<&Arc<AtomicF32>> {
89 self.params.get(id)
90 }
91}
92
93const _: () = {
98 fn _assert_send_sync<T: Send + Sync>() {}
99 fn _check() {
100 _assert_send_sync::<AtomicParameterBridge>();
101 }
102};
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use wavecraft_protocol::ParameterType;
108
109 fn test_params() -> Vec<ParameterInfo> {
110 vec![
111 ParameterInfo {
112 id: "gain".to_string(),
113 name: "Gain".to_string(),
114 param_type: ParameterType::Float,
115 value: 0.5,
116 default: 0.5,
117 min: 0.0,
118 max: 1.0,
119 unit: Some("dB".to_string()),
120 group: Some("Input".to_string()),
121 variants: None,
122 },
123 ParameterInfo {
124 id: "mix".to_string(),
125 name: "Mix".to_string(),
126 param_type: ParameterType::Float,
127 value: 1.0,
128 default: 1.0,
129 min: 0.0,
130 max: 1.0,
131 unit: Some("%".to_string()),
132 group: None,
133 variants: None,
134 },
135 ]
136 }
137
138 #[test]
139 fn test_default_values() {
140 let bridge = AtomicParameterBridge::new(&test_params());
141
142 let gain = bridge.read("gain").expect("gain should exist");
143 assert!(
144 (gain - 0.5).abs() < f32::EPSILON,
145 "gain default should be 0.5"
146 );
147
148 let mix = bridge.read("mix").expect("mix should exist");
149 assert!(
150 (mix - 1.0).abs() < f32::EPSILON,
151 "mix default should be 1.0"
152 );
153 }
154
155 #[test]
156 fn test_write_and_read() {
157 let bridge = AtomicParameterBridge::new(&test_params());
158
159 bridge.write("gain", 0.75);
160 let gain = bridge.read("gain").expect("gain should exist");
161 assert!(
162 (gain - 0.75).abs() < f32::EPSILON,
163 "gain should be updated to 0.75"
164 );
165 }
166
167 #[test]
168 fn test_read_unknown_param() {
169 let bridge = AtomicParameterBridge::new(&test_params());
170 assert!(
171 bridge.read("nonexistent").is_none(),
172 "unknown param should return None"
173 );
174 }
175
176 #[test]
177 fn test_write_unknown_param_is_noop() {
178 let bridge = AtomicParameterBridge::new(&test_params());
179 bridge.write("nonexistent", 0.5);
181 }
182
183 #[test]
184 fn test_concurrent_write_read() {
185 use std::sync::Arc;
186 use std::thread;
187
188 let bridge = Arc::new(AtomicParameterBridge::new(&test_params()));
189
190 let writer = {
191 let bridge = Arc::clone(&bridge);
192 thread::spawn(move || {
193 for i in 0..1000 {
194 bridge.write("gain", i as f32 / 1000.0);
195 }
196 })
197 };
198
199 let reader = {
200 let bridge = Arc::clone(&bridge);
201 thread::spawn(move || {
202 for _ in 0..1000 {
203 let val = bridge.read("gain");
204 assert!(val.is_some(), "gain should always be readable");
205 let v = val.unwrap();
206 assert!(
207 (0.0..=1.0).contains(&v) || v == 0.5,
208 "value should be in range"
209 );
210 }
211 })
212 };
213
214 writer.join().expect("writer thread should not panic");
215 reader.join().expect("reader thread should not panic");
216 }
217
218 #[test]
219 fn test_copy_all_to_preserves_parameter_order() {
220 let bridge = AtomicParameterBridge::new(&test_params());
221 bridge.write("gain", 0.75);
222 bridge.write("mix", 0.25);
223
224 let mut values = [0.0_f32; 2];
225 let copied = bridge.copy_all_to(&mut values);
226
227 assert_eq!(copied, 2);
228 assert!((values[0] - 0.75).abs() < f32::EPSILON);
229 assert!((values[1] - 0.25).abs() < f32::EPSILON);
230 }
231
232 #[test]
233 fn test_parameter_count_matches_metadata_len() {
234 let params = test_params();
235 let bridge = AtomicParameterBridge::new(¶ms);
236 assert_eq!(bridge.parameter_count(), params.len());
237 }
238}