Skip to main content

wavecraft_bridge/
in_memory_host.rs

1//! In-memory ParameterHost implementation for dev tools and tests.
2
3use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use crate::{BridgeError, ParameterHost};
7use wavecraft_protocol::{AudioRuntimeStatus, MeterFrame, OscilloscopeFrame, ParameterInfo};
8
9/// Provides metering data for an in-memory host.
10pub trait MeterProvider: Send + Sync {
11    /// Return the latest meter frame, if available.
12    fn get_meter_frame(&self) -> Option<MeterFrame>;
13}
14
15/// Provides oscilloscope frame data for an in-memory host.
16pub trait OscilloscopeProvider: Send + Sync {
17    /// Return the latest oscilloscope frame, if available.
18    fn get_oscilloscope_frame(&self) -> Option<OscilloscopeFrame>;
19}
20
21/// In-memory host for storing parameter values and optional meter data.
22///
23/// This is intended for development tools (like the CLI dev server) and tests.
24pub struct InMemoryParameterHost {
25    parameters: RwLock<Vec<ParameterInfo>>,
26    values: RwLock<HashMap<String, f32>>,
27    meter_provider: Option<Arc<dyn MeterProvider>>,
28    oscilloscope_provider: Option<Arc<dyn OscilloscopeProvider>>,
29}
30
31impl InMemoryParameterHost {
32    /// Create a new in-memory host with the given parameter metadata.
33    pub fn new(parameters: Vec<ParameterInfo>) -> Self {
34        let values = parameters
35            .iter()
36            .map(|p| (p.id.clone(), p.default))
37            .collect();
38
39        Self {
40            parameters: RwLock::new(parameters),
41            values: RwLock::new(values),
42            meter_provider: None,
43            oscilloscope_provider: None,
44        }
45    }
46
47    /// Create a new in-memory host with a meter provider.
48    pub fn with_meter_provider(
49        parameters: Vec<ParameterInfo>,
50        meter_provider: Arc<dyn MeterProvider>,
51    ) -> Self {
52        let mut host = Self::new(parameters);
53        host.meter_provider = Some(meter_provider);
54        host
55    }
56
57    /// Create a new in-memory host with an oscilloscope provider.
58    pub fn with_oscilloscope_provider(
59        parameters: Vec<ParameterInfo>,
60        oscilloscope_provider: Arc<dyn OscilloscopeProvider>,
61    ) -> Self {
62        let mut host = Self::new(parameters);
63        host.oscilloscope_provider = Some(oscilloscope_provider);
64        host
65    }
66
67    /// Create a new in-memory host with both meter and oscilloscope providers.
68    pub fn with_providers(
69        parameters: Vec<ParameterInfo>,
70        meter_provider: Option<Arc<dyn MeterProvider>>,
71        oscilloscope_provider: Option<Arc<dyn OscilloscopeProvider>>,
72    ) -> Self {
73        let mut host = Self::new(parameters);
74        host.meter_provider = meter_provider;
75        host.oscilloscope_provider = oscilloscope_provider;
76        host
77    }
78
79    /// Replace all parameters with new metadata from a fresh build.
80    ///
81    /// This method is used during hot-reload to update parameter definitions
82    /// while preserving existing parameter values where possible. Parameters
83    /// with matching IDs retain their current values; new parameters get
84    /// their default values; removed parameters are dropped.
85    ///
86    /// # Thread Safety
87    ///
88    /// This method acquires write locks on both the parameters and values maps.
89    /// If a lock is poisoned (from a previous panic), it recovers gracefully
90    /// by clearing the poisoned lock and continuing.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if both lock recovery attempts fail.
95    pub fn replace_parameters(&self, new_params: Vec<ParameterInfo>) -> Result<(), String> {
96        // Acquire values lock with poison recovery
97        let mut values = match self.values.write() {
98            Ok(guard) => guard,
99            Err(poisoned) => {
100                eprintln!("⚠ Recovering from poisoned values lock");
101                poisoned.into_inner()
102            }
103        };
104
105        // Build new values map, preserving existing values where IDs match
106        let mut new_values = HashMap::new();
107        for param in &new_params {
108            let value = values.get(&param.id).copied().unwrap_or(param.default);
109            new_values.insert(param.id.clone(), value);
110        }
111
112        *values = new_values;
113        drop(values); // Release values lock before acquiring parameters lock
114
115        // Acquire parameters lock with poison recovery
116        let mut params = match self.parameters.write() {
117            Ok(guard) => guard,
118            Err(poisoned) => {
119                eprintln!("⚠ Recovering from poisoned parameters lock");
120                poisoned.into_inner()
121            }
122        };
123
124        *params = new_params;
125        Ok(())
126    }
127
128    fn current_value(&self, id: &str, default: f32) -> f32 {
129        self.values
130            .read()
131            .ok()
132            .and_then(|values| values.get(id).copied())
133            .unwrap_or(default)
134    }
135
136    fn materialize_parameter(&self, param: &ParameterInfo) -> ParameterInfo {
137        ParameterInfo {
138            id: param.id.clone(),
139            name: param.name.clone(),
140            param_type: param.param_type,
141            value: self.current_value(&param.id, param.default),
142            default: param.default,
143            min: param.min,
144            max: param.max,
145            unit: param.unit.clone(),
146            group: param.group.clone(),
147            variants: param.variants.clone(),
148        }
149    }
150}
151
152impl ParameterHost for InMemoryParameterHost {
153    fn get_parameter(&self, id: &str) -> Option<ParameterInfo> {
154        let parameters = self.parameters.read().ok()?;
155        let param = parameters.iter().find(|p| p.id == id)?;
156
157        Some(self.materialize_parameter(param))
158    }
159
160    fn set_parameter(&self, id: &str, value: f32) -> Result<(), BridgeError> {
161        let parameters = self.parameters.read().ok();
162        let param_exists = parameters
163            .as_ref()
164            .map(|p| p.iter().any(|param| param.id == id))
165            .unwrap_or(false);
166
167        if !param_exists {
168            return Err(BridgeError::ParameterNotFound(id.to_string()));
169        }
170
171        let Some(param) = parameters
172            .as_ref()
173            .and_then(|p| p.iter().find(|param| param.id == id))
174        else {
175            return Err(BridgeError::ParameterNotFound(id.to_string()));
176        };
177
178        if !(param.min..=param.max).contains(&value) {
179            return Err(BridgeError::ParameterOutOfRange {
180                id: id.to_string(),
181                value,
182            });
183        }
184
185        if let Ok(mut values) = self.values.write() {
186            values.insert(id.to_string(), value);
187        }
188
189        Ok(())
190    }
191
192    fn get_all_parameters(&self) -> Vec<ParameterInfo> {
193        let parameters = match self.parameters.read() {
194            Ok(guard) => guard,
195            Err(_) => return Vec::new(), // Return empty on poisoned lock
196        };
197
198        parameters
199            .iter()
200            .map(|param| self.materialize_parameter(param))
201            .collect()
202    }
203
204    fn get_meter_frame(&self) -> Option<MeterFrame> {
205        self.meter_provider
206            .as_ref()
207            .and_then(|provider| provider.get_meter_frame())
208    }
209
210    fn get_oscilloscope_frame(&self) -> Option<OscilloscopeFrame> {
211        self.oscilloscope_provider
212            .as_ref()
213            .and_then(|provider| provider.get_oscilloscope_frame())
214    }
215
216    fn request_resize(&self, _width: u32, _height: u32) -> bool {
217        false
218    }
219
220    fn get_audio_status(&self) -> Option<AudioRuntimeStatus> {
221        None
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use wavecraft_protocol::ParameterType;
229
230    struct StaticMeterProvider {
231        frame: MeterFrame,
232    }
233
234    struct StaticOscilloscopeProvider {
235        frame: OscilloscopeFrame,
236    }
237
238    impl MeterProvider for StaticMeterProvider {
239        fn get_meter_frame(&self) -> Option<MeterFrame> {
240            Some(self.frame)
241        }
242    }
243
244    impl OscilloscopeProvider for StaticOscilloscopeProvider {
245        fn get_oscilloscope_frame(&self) -> Option<OscilloscopeFrame> {
246            Some(self.frame.clone())
247        }
248    }
249
250    fn test_params() -> Vec<ParameterInfo> {
251        vec![
252            ParameterInfo {
253                id: "gain".to_string(),
254                name: "Gain".to_string(),
255                param_type: ParameterType::Float,
256                value: 0.5,
257                default: 0.5,
258                min: 0.0,
259                max: 1.0,
260                unit: Some("dB".to_string()),
261                group: Some("Input".to_string()),
262                variants: None,
263            },
264            ParameterInfo {
265                id: "mix".to_string(),
266                name: "Mix".to_string(),
267                param_type: ParameterType::Float,
268                value: 1.0,
269                default: 1.0,
270                min: 0.0,
271                max: 1.0,
272                unit: Some("%".to_string()),
273                group: None,
274                variants: None,
275            },
276        ]
277    }
278
279    #[test]
280    fn test_get_parameter() {
281        let host = InMemoryParameterHost::new(test_params());
282
283        let param = host.get_parameter("gain").expect("should find gain");
284        assert_eq!(param.id, "gain");
285        assert_eq!(param.name, "Gain");
286        assert!((param.value - 0.5).abs() < f32::EPSILON);
287    }
288
289    #[test]
290    fn test_set_parameter() {
291        let host = InMemoryParameterHost::new(test_params());
292
293        host.set_parameter("gain", 0.75).expect("should set gain");
294
295        let param = host.get_parameter("gain").expect("should find gain");
296        assert!((param.value - 0.75).abs() < f32::EPSILON);
297    }
298
299    #[test]
300    fn test_set_parameter_out_of_range() {
301        let host = InMemoryParameterHost::new(test_params());
302
303        let result = host.set_parameter("gain", 1.5);
304        assert!(result.is_err());
305
306        let result = host.set_parameter("gain", -0.1);
307        assert!(result.is_err());
308    }
309
310    #[test]
311    fn test_get_all_parameters() {
312        let host = InMemoryParameterHost::new(test_params());
313
314        let params = host.get_all_parameters();
315        assert_eq!(params.len(), 2);
316        assert!(params.iter().any(|p| p.id == "gain"));
317        assert!(params.iter().any(|p| p.id == "mix"));
318    }
319
320    #[test]
321    fn test_get_meter_frame() {
322        let frame = MeterFrame {
323            peak_l: 0.7,
324            rms_l: 0.5,
325            peak_r: 0.6,
326            rms_r: 0.4,
327            timestamp: 0,
328        };
329        let provider = Arc::new(StaticMeterProvider { frame });
330        let host = InMemoryParameterHost::with_meter_provider(test_params(), provider);
331
332        let read = host.get_meter_frame().expect("should have meter frame");
333        assert!((read.peak_l - 0.7).abs() < f32::EPSILON);
334        assert!((read.rms_r - 0.4).abs() < f32::EPSILON);
335    }
336
337    #[test]
338    fn test_get_oscilloscope_frame() {
339        let frame = OscilloscopeFrame {
340            points_l: vec![0.1; 1024],
341            points_r: vec![0.2; 1024],
342            sample_rate: 48_000.0,
343            timestamp: 99,
344            no_signal: false,
345            trigger_mode: wavecraft_protocol::OscilloscopeTriggerMode::RisingZeroCrossing,
346        };
347        let provider = Arc::new(StaticOscilloscopeProvider { frame });
348        let host = InMemoryParameterHost::with_oscilloscope_provider(test_params(), provider);
349
350        let read = host
351            .get_oscilloscope_frame()
352            .expect("should have oscilloscope frame");
353        assert_eq!(read.points_l.len(), 1024);
354        assert_eq!(read.points_r.len(), 1024);
355        assert_eq!(read.timestamp, 99);
356    }
357
358    #[test]
359    fn test_replace_parameters_preserves_values() {
360        let host = InMemoryParameterHost::new(test_params());
361
362        // Set custom values
363        host.set_parameter("gain", 0.75).expect("should set gain");
364        host.set_parameter("mix", 0.5).expect("should set mix");
365
366        // Add a new parameter
367        let new_params = vec![
368            ParameterInfo {
369                id: "gain".to_string(),
370                name: "Gain".to_string(),
371                param_type: ParameterType::Float,
372                value: 0.5,
373                default: 0.5,
374                min: 0.0,
375                max: 1.0,
376                unit: Some("dB".to_string()),
377                group: Some("Input".to_string()),
378                variants: None,
379            },
380            ParameterInfo {
381                id: "mix".to_string(),
382                name: "Mix".to_string(),
383                param_type: ParameterType::Float,
384                value: 1.0,
385                default: 1.0,
386                min: 0.0,
387                max: 1.0,
388                unit: Some("%".to_string()),
389                group: None,
390                variants: None,
391            },
392            ParameterInfo {
393                id: "freq".to_string(),
394                name: "Frequency".to_string(),
395                param_type: ParameterType::Float,
396                value: 440.0,
397                default: 440.0,
398                min: 20.0,
399                max: 20_000.0,
400                unit: Some("Hz".to_string()),
401                group: None,
402                variants: None,
403            },
404        ];
405
406        host.replace_parameters(new_params)
407            .expect("should replace parameters");
408
409        // Existing parameters should preserve their values
410        let gain = host.get_parameter("gain").expect("should find gain");
411        assert!((gain.value - 0.75).abs() < f32::EPSILON);
412
413        let mix = host.get_parameter("mix").expect("should find mix");
414        assert!((mix.value - 0.5).abs() < f32::EPSILON);
415
416        // New parameter should have default value
417        let freq = host.get_parameter("freq").expect("should find freq");
418        assert!((freq.value - 440.0).abs() < f32::EPSILON);
419    }
420
421    #[test]
422    fn test_replace_parameters_removes_old() {
423        let host = InMemoryParameterHost::new(test_params());
424
425        // Replace with fewer parameters
426        let new_params = vec![ParameterInfo {
427            id: "gain".to_string(),
428            name: "Gain".to_string(),
429            param_type: ParameterType::Float,
430            value: 0.5,
431            default: 0.5,
432            min: 0.0,
433            max: 1.0,
434            unit: Some("dB".to_string()),
435            group: Some("Input".to_string()),
436            variants: None,
437        }];
438
439        host.replace_parameters(new_params)
440            .expect("should replace parameters");
441
442        // Old parameter should be gone
443        assert!(host.get_parameter("mix").is_none());
444
445        // Kept parameter should still be accessible
446        assert!(host.get_parameter("gain").is_some());
447    }
448
449    #[test]
450    fn test_set_parameter_uses_declared_range_not_normalized_range() {
451        let host = InMemoryParameterHost::new(vec![ParameterInfo {
452            id: "test_tone_frequency".to_string(),
453            name: "Frequency".to_string(),
454            param_type: ParameterType::Float,
455            value: 440.0,
456            default: 440.0,
457            min: 20.0,
458            max: 20_000.0,
459            unit: Some("Hz".to_string()),
460            group: Some("Test Tone".to_string()),
461            variants: None,
462        }]);
463
464        host.set_parameter("test_tone_frequency", 2_000.0)
465            .expect("frequency in declared range should be accepted");
466
467        let freq = host
468            .get_parameter("test_tone_frequency")
469            .expect("frequency should exist");
470        assert!((freq.value - 2_000.0).abs() < f32::EPSILON);
471
472        let too_low = host.set_parameter("test_tone_frequency", 10.0);
473        assert!(too_low.is_err(), "value below min should be rejected");
474
475        let too_high = host.set_parameter("test_tone_frequency", 30_000.0);
476        assert!(too_high.is_err(), "value above max should be rejected");
477    }
478}