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