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                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        // Should not panic
144        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}