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 variants: None,
86 },
87 ParameterInfo {
88 id: "mix".to_string(),
89 name: "Mix".to_string(),
90 param_type: ParameterType::Float,
91 value: 1.0,
92 default: 1.0,
93 min: 0.0,
94 max: 1.0,
95 unit: Some("%".to_string()),
96 group: None,
97 variants: None,
98 },
99 ]
100 }
101
102 #[test]
103 fn test_default_values() {
104 let bridge = AtomicParameterBridge::new(&test_params());
105
106 let gain = bridge.read("gain").expect("gain should exist");
107 assert!(
108 (gain - 0.5).abs() < f32::EPSILON,
109 "gain default should be 0.5"
110 );
111
112 let mix = bridge.read("mix").expect("mix should exist");
113 assert!(
114 (mix - 1.0).abs() < f32::EPSILON,
115 "mix default should be 1.0"
116 );
117 }
118
119 #[test]
120 fn test_write_and_read() {
121 let bridge = AtomicParameterBridge::new(&test_params());
122
123 bridge.write("gain", 0.75);
124 let gain = bridge.read("gain").expect("gain should exist");
125 assert!(
126 (gain - 0.75).abs() < f32::EPSILON,
127 "gain should be updated to 0.75"
128 );
129 }
130
131 #[test]
132 fn test_read_unknown_param() {
133 let bridge = AtomicParameterBridge::new(&test_params());
134 assert!(
135 bridge.read("nonexistent").is_none(),
136 "unknown param should return None"
137 );
138 }
139
140 #[test]
141 fn test_write_unknown_param_is_noop() {
142 let bridge = AtomicParameterBridge::new(&test_params());
143 bridge.write("nonexistent", 0.5);
145 }
146
147 #[test]
148 fn test_concurrent_write_read() {
149 use std::sync::Arc;
150 use std::thread;
151
152 let bridge = Arc::new(AtomicParameterBridge::new(&test_params()));
153
154 let writer = {
155 let bridge = Arc::clone(&bridge);
156 thread::spawn(move || {
157 for i in 0..1000 {
158 bridge.write("gain", i as f32 / 1000.0);
159 }
160 })
161 };
162
163 let reader = {
164 let bridge = Arc::clone(&bridge);
165 thread::spawn(move || {
166 for _ in 0..1000 {
167 let val = bridge.read("gain");
168 assert!(val.is_some(), "gain should always be readable");
169 let v = val.unwrap();
170 assert!(
171 (0.0..=1.0).contains(&v) || v == 0.5,
172 "value should be in range"
173 );
174 }
175 })
176 };
177
178 writer.join().expect("writer thread should not panic");
179 reader.join().expect("reader thread should not panic");
180 }
181}