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/// Lock-free bridge for passing parameter values from the WebSocket thread
16/// to the audio thread.
17///
18/// Constructed once at startup with one `Arc<AtomicF32>` per parameter. The
19/// inner `HashMap` is never mutated after construction — only the atomic
20/// values change. This makes reads fully lock-free and real-time safe.
21pub struct AtomicParameterBridge {
22    params: HashMap<String, Arc<AtomicF32>>,
23}
24
25impl AtomicParameterBridge {
26    /// Create a new bridge from parameter metadata.
27    ///
28    /// Each parameter gets an `AtomicF32` initialized to its default value.
29    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    /// Write a parameter value (called from WebSocket thread).
38    ///
39    /// Uses `Ordering::Relaxed` — no synchronization guarantee beyond
40    /// eventual visibility. The audio thread will see the update at the
41    /// next block boundary.
42    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    /// Read a parameter value (called from audio thread — RT-safe).
49    ///
50    /// Returns `None` if the parameter ID is unknown. Uses
51    /// `Ordering::Relaxed` — single atomic load, no allocation.
52    pub fn read(&self, id: &str) -> Option<f32> {
53        self.params.get(id).map(|a| a.load(Ordering::Relaxed))
54    }
55}
56
57// Compile-time assertion: AtomicParameterBridge is Send + Sync.
58// The HashMap is immutable after construction, and all values are
59// accessed through atomic operations. Arc<AtomicF32> is Send + Sync,
60// so Rust auto-derives these traits correctly.
61const _: () = {
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        // Should not panic
138        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}