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