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