Skip to main content

wavecraft_dev_server/audio/
atomic_params.rs

1//! Lock-free parameter bridge for audio thread access.
2//!
3//! Provides `AtomicParameterBridge` — a collection of `AtomicF32` values keyed
4//! by parameter ID. The WebSocket thread writes parameter updates via `store()`,
5//! and the audio thread reads them via `load()` with zero allocations and zero
6//! locks. Relaxed ordering is sufficient because parameter updates are not
7//! synchronization points; a one-block delay in propagation is acceptable.
8
9use atomic_float::AtomicF32;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::Ordering;
13use wavecraft_protocol::ParameterInfo;
14
15// Dev-mode correctness first: use SeqCst to minimize visibility surprises
16// across callback and websocket threads while debugging parameter propagation.
17const PARAM_ORDERING: Ordering = Ordering::SeqCst;
18
19/// Lock-free bridge for passing parameter values from the WebSocket thread
20/// to the audio thread.
21///
22/// Constructed once at startup with one `Arc<AtomicF32>` per parameter. The
23/// inner `HashMap` is never mutated after construction — only the atomic
24/// values change. This makes reads fully lock-free and real-time safe.
25pub struct AtomicParameterBridge {
26    params: HashMap<String, Arc<AtomicF32>>,
27    ordered_params: Vec<Arc<AtomicF32>>,
28}
29
30impl AtomicParameterBridge {
31    /// Create a new bridge from parameter metadata.
32    ///
33    /// Each parameter gets an `AtomicF32` initialized to its default value.
34    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    /// Returns the dense plain-value parameter count in generation order.
51    pub fn parameter_count(&self) -> usize {
52        self.ordered_params.len()
53    }
54
55    /// Write a parameter value (called from WebSocket thread).
56    ///
57    /// Uses `Ordering::Relaxed` — no synchronization guarantee beyond
58    /// eventual visibility. The audio thread will see the update at the
59    /// next block boundary.
60    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    /// Read a parameter value (called from audio thread — RT-safe).
67    ///
68    /// Returns `None` if the parameter ID is unknown. Uses
69    /// `Ordering::Relaxed` — single atomic load, no allocation.
70    pub fn read(&self, id: &str) -> Option<f32> {
71        self.lookup_param(id)
72            .map(|atomic| atomic.load(PARAM_ORDERING))
73    }
74
75    /// Copy all parameters into `output` in stable generation order.
76    ///
77    /// Returns the number of copied values (`min(output.len(), parameter_count())`).
78    ///
79    /// This is real-time safe and allocation-free, suitable for audio callback use.
80    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
93// Compile-time assertion: AtomicParameterBridge is Send + Sync.
94// The HashMap is immutable after construction, and all values are
95// accessed through atomic operations. Arc<AtomicF32> is Send + Sync,
96// so Rust auto-derives these traits correctly.
97const _: () = {
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        // Should not panic
180        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(&params);
236        assert_eq!(bridge.parameter_count(), params.len());
237    }
238}