Skip to main content

wavecraft_dev_server/
host.rs

1//! Development server host implementing ParameterHost trait
2//!
3//! This module provides a ParameterHost implementation for the embedded
4//! development server. It stores parameter values in memory and forwards
5//! parameter changes to an optional AtomicParameterBridge for lock-free
6//! audio-thread access.
7
8#[cfg(feature = "audio")]
9use std::sync::Arc;
10use wavecraft_bridge::{BridgeError, InMemoryParameterHost, ParameterHost};
11use wavecraft_protocol::{MeterFrame, ParameterInfo};
12
13#[cfg(feature = "audio")]
14use crate::audio::atomic_params::AtomicParameterBridge;
15
16/// Development server host for browser-based UI testing
17///
18/// This implementation stores parameter values locally and optionally
19/// forwards updates to an `AtomicParameterBridge` for lock-free reads
20/// on the audio thread. Meter data is provided externally via the audio
21/// server's meter channel (not generated synthetically).
22///
23/// # Thread Safety
24///
25/// Parameter state is protected by RwLock (in `InMemoryParameterHost`).
26/// The `AtomicParameterBridge` uses lock-free atomics for audio thread.
27pub struct DevServerHost {
28    inner: InMemoryParameterHost,
29    #[cfg(feature = "audio")]
30    param_bridge: Option<Arc<AtomicParameterBridge>>,
31}
32
33impl DevServerHost {
34    /// Create a new dev server host with parameter metadata.
35    ///
36    /// # Arguments
37    ///
38    /// * `parameters` - Parameter metadata loaded from the plugin FFI
39    ///
40    /// Used by tests and the non-audio build path. When `audio` is
41    /// enabled (default), production code uses `with_param_bridge()` instead.
42    #[cfg_attr(feature = "audio", allow(dead_code))]
43    pub fn new(parameters: Vec<ParameterInfo>) -> Self {
44        let inner = InMemoryParameterHost::new(parameters);
45
46        Self {
47            inner,
48            #[cfg(feature = "audio")]
49            param_bridge: None,
50        }
51    }
52
53    /// Create a new dev server host with an `AtomicParameterBridge`.
54    ///
55    /// When a bridge is provided, `set_parameter()` will write updates
56    /// to both the inner store and the bridge (for audio-thread reads).
57    #[cfg(feature = "audio")]
58    pub fn with_param_bridge(
59        parameters: Vec<ParameterInfo>,
60        bridge: Arc<AtomicParameterBridge>,
61    ) -> Self {
62        let inner = InMemoryParameterHost::new(parameters);
63
64        Self {
65            inner,
66            param_bridge: Some(bridge),
67        }
68    }
69
70    /// Replace all parameters with new metadata from a hot-reload.
71    ///
72    /// Preserves values for parameters with matching IDs. New parameters
73    /// get their default values. This is used by the hot-reload pipeline
74    /// to update parameter definitions without restarting the server.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if parameter replacement fails (e.g., unrecoverable
79    /// lock poisoning).
80    pub fn replace_parameters(&self, new_params: Vec<ParameterInfo>) -> Result<(), String> {
81        self.inner.replace_parameters(new_params)
82    }
83}
84
85impl ParameterHost for DevServerHost {
86    fn get_parameter(&self, id: &str) -> Option<ParameterInfo> {
87        self.inner.get_parameter(id)
88    }
89
90    fn set_parameter(&self, id: &str, value: f32) -> Result<(), BridgeError> {
91        let result = self.inner.set_parameter(id, value);
92
93        // Forward to atomic bridge for audio-thread access (lock-free)
94        #[cfg(feature = "audio")]
95        if result.is_ok() && let Some(ref bridge) = self.param_bridge {
96            bridge.write(id, value);
97        }
98
99        result
100    }
101
102    fn get_all_parameters(&self) -> Vec<ParameterInfo> {
103        self.inner.get_all_parameters()
104    }
105
106    fn get_meter_frame(&self) -> Option<MeterFrame> {
107        // Meters are now provided externally via the audio server's meter
108        // channel → WebSocket broadcast. No synthetic generation.
109        None
110    }
111
112    fn request_resize(&self, _width: u32, _height: u32) -> bool {
113        self.inner.request_resize(_width, _height)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use wavecraft_protocol::ParameterType;
121
122    fn test_params() -> Vec<ParameterInfo> {
123        vec![
124            ParameterInfo {
125                id: "gain".to_string(),
126                name: "Gain".to_string(),
127                param_type: ParameterType::Float,
128                value: 0.5,
129                default: 0.5,
130                unit: Some("dB".to_string()),
131                group: Some("Input".to_string()),
132            },
133            ParameterInfo {
134                id: "mix".to_string(),
135                name: "Mix".to_string(),
136                param_type: ParameterType::Float,
137                value: 1.0,
138                default: 1.0,
139                unit: Some("%".to_string()),
140                group: None,
141            },
142        ]
143    }
144
145    #[test]
146    fn test_get_parameter() {
147        let host = DevServerHost::new(test_params());
148
149        let param = host.get_parameter("gain").expect("should find gain");
150        assert_eq!(param.id, "gain");
151        assert_eq!(param.name, "Gain");
152        assert!((param.value - 0.5).abs() < f32::EPSILON);
153    }
154
155    #[test]
156    fn test_get_parameter_not_found() {
157        let host = DevServerHost::new(test_params());
158        assert!(host.get_parameter("nonexistent").is_none());
159    }
160
161    #[test]
162    fn test_set_parameter() {
163        let host = DevServerHost::new(test_params());
164
165        host.set_parameter("gain", 0.75).expect("should set gain");
166
167        let param = host.get_parameter("gain").expect("should find gain");
168        assert!((param.value - 0.75).abs() < f32::EPSILON);
169    }
170
171    #[test]
172    fn test_set_parameter_invalid_id() {
173        let host = DevServerHost::new(test_params());
174        let result = host.set_parameter("invalid", 0.5);
175        assert!(result.is_err());
176    }
177
178    #[test]
179    fn test_set_parameter_out_of_range() {
180        let host = DevServerHost::new(test_params());
181
182        let result = host.set_parameter("gain", 1.5);
183        assert!(result.is_err());
184
185        let result = host.set_parameter("gain", -0.1);
186        assert!(result.is_err());
187    }
188
189    #[test]
190    fn test_get_all_parameters() {
191        let host = DevServerHost::new(test_params());
192
193        let params = host.get_all_parameters();
194        assert_eq!(params.len(), 2);
195        assert!(params.iter().any(|p| p.id == "gain"));
196        assert!(params.iter().any(|p| p.id == "mix"));
197    }
198
199    #[test]
200    fn test_get_meter_frame() {
201        let host = DevServerHost::new(test_params());
202        // Meters are now provided externally — no synthetic generation
203        assert!(host.get_meter_frame().is_none());
204    }
205}