1#[cfg(feature = "audio")]
9use std::sync::Arc;
10use std::sync::RwLock;
11use std::time::{SystemTime, UNIX_EPOCH};
12use wavecraft_bridge::{BridgeError, InMemoryParameterHost, ParameterHost};
13use wavecraft_protocol::{
14 AudioRuntimePhase, AudioRuntimeStatus, MeterFrame, MeterUpdateNotification, OscilloscopeFrame,
15 ParameterInfo,
16};
17
18#[cfg(feature = "audio")]
19use crate::audio::atomic_params::AtomicParameterBridge;
20
21pub struct DevServerHost {
33 inner: InMemoryParameterHost,
34 latest_meter_frame: Arc<RwLock<Option<MeterFrame>>>,
35 latest_oscilloscope_frame: Arc<RwLock<Option<OscilloscopeFrame>>>,
36 audio_status: Arc<RwLock<AudioRuntimeStatus>>,
37 #[cfg(feature = "audio")]
38 param_bridge: Option<Arc<AtomicParameterBridge>>,
39}
40
41impl DevServerHost {
42 #[cfg_attr(feature = "audio", allow(dead_code))]
51 pub fn new(parameters: Vec<ParameterInfo>) -> Self {
52 let inner = InMemoryParameterHost::new(parameters);
53 let latest_meter_frame = Arc::new(RwLock::new(None));
54 let latest_oscilloscope_frame = Arc::new(RwLock::new(None));
55 let audio_status = Arc::new(RwLock::new(AudioRuntimeStatus {
56 phase: AudioRuntimePhase::Disabled,
57 diagnostic: None,
58 sample_rate: None,
59 buffer_size: None,
60 updated_at_ms: now_millis(),
61 }));
62
63 Self {
64 inner,
65 latest_meter_frame,
66 latest_oscilloscope_frame,
67 audio_status,
68 #[cfg(feature = "audio")]
69 param_bridge: None,
70 }
71 }
72
73 #[cfg(feature = "audio")]
78 pub fn with_param_bridge(
79 parameters: Vec<ParameterInfo>,
80 bridge: Arc<AtomicParameterBridge>,
81 ) -> Self {
82 let inner = InMemoryParameterHost::new(parameters);
83 let latest_meter_frame = Arc::new(RwLock::new(None));
84 let latest_oscilloscope_frame = Arc::new(RwLock::new(None));
85 let audio_status = Arc::new(RwLock::new(AudioRuntimeStatus {
86 phase: AudioRuntimePhase::Disabled,
87 diagnostic: None,
88 sample_rate: None,
89 buffer_size: None,
90 updated_at_ms: now_millis(),
91 }));
92
93 Self {
94 inner,
95 latest_meter_frame,
96 latest_oscilloscope_frame,
97 audio_status,
98 param_bridge: Some(bridge),
99 }
100 }
101
102 pub fn replace_parameters(&self, new_params: Vec<ParameterInfo>) -> Result<(), String> {
113 self.inner.replace_parameters(new_params)
114 }
115
116 pub fn set_latest_meter_frame(&self, update: &MeterUpdateNotification) {
118 let mut meter = self
119 .latest_meter_frame
120 .write()
121 .expect("latest_meter_frame lock poisoned");
122 *meter = Some(MeterFrame {
123 peak_l: update.left_peak,
124 peak_r: update.right_peak,
125 rms_l: update.left_rms,
126 rms_r: update.right_rms,
127 timestamp: update.timestamp_us,
128 });
129 }
130
131 pub fn set_latest_oscilloscope_frame(&self, frame: OscilloscopeFrame) {
133 let mut oscilloscope = self
134 .latest_oscilloscope_frame
135 .write()
136 .expect("latest_oscilloscope_frame lock poisoned");
137 *oscilloscope = Some(frame);
138 }
139
140 pub fn set_audio_status(&self, status: AudioRuntimeStatus) {
142 let mut current = self
143 .audio_status
144 .write()
145 .expect("audio_status lock poisoned");
146 *current = status;
147 }
148}
149
150impl ParameterHost for DevServerHost {
151 fn get_parameter(&self, id: &str) -> Option<ParameterInfo> {
152 self.inner.get_parameter(id)
153 }
154
155 fn set_parameter(&self, id: &str, value: f32) -> Result<(), BridgeError> {
156 let result = self.inner.set_parameter(id, value);
157
158 #[cfg(feature = "audio")]
160 if result.is_ok()
161 && let Some(ref bridge) = self.param_bridge
162 {
163 bridge.write(id, value);
164 }
165
166 result
167 }
168
169 fn get_all_parameters(&self) -> Vec<ParameterInfo> {
170 self.inner.get_all_parameters()
171 }
172
173 fn get_meter_frame(&self) -> Option<MeterFrame> {
174 self.latest_meter_frame
175 .read()
176 .expect("latest_meter_frame lock poisoned")
177 .clone()
178 }
179
180 fn get_oscilloscope_frame(&self) -> Option<OscilloscopeFrame> {
181 self.latest_oscilloscope_frame
182 .read()
183 .expect("latest_oscilloscope_frame lock poisoned")
184 .clone()
185 }
186
187 fn request_resize(&self, _width: u32, _height: u32) -> bool {
188 self.inner.request_resize(_width, _height)
189 }
190
191 fn get_audio_status(&self) -> Option<AudioRuntimeStatus> {
192 Some(
193 self.audio_status
194 .read()
195 .expect("audio_status lock poisoned")
196 .clone(),
197 )
198 }
199}
200
201fn now_millis() -> u64 {
202 SystemTime::now()
203 .duration_since(UNIX_EPOCH)
204 .map_or(0, |duration| duration.as_millis() as u64)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use wavecraft_protocol::ParameterType;
211
212 fn test_params() -> Vec<ParameterInfo> {
213 vec![
214 ParameterInfo {
215 id: "gain".to_string(),
216 name: "Gain".to_string(),
217 param_type: ParameterType::Float,
218 value: 0.5,
219 default: 0.5,
220 min: 0.0,
221 max: 1.0,
222 unit: Some("dB".to_string()),
223 group: Some("Input".to_string()),
224 variants: None,
225 },
226 ParameterInfo {
227 id: "mix".to_string(),
228 name: "Mix".to_string(),
229 param_type: ParameterType::Float,
230 value: 1.0,
231 default: 1.0,
232 min: 0.0,
233 max: 1.0,
234 unit: Some("%".to_string()),
235 group: None,
236 variants: None,
237 },
238 ]
239 }
240
241 #[test]
242 fn test_get_parameter() {
243 let host = DevServerHost::new(test_params());
244
245 let param = host.get_parameter("gain").expect("should find gain");
246 assert_eq!(param.id, "gain");
247 assert_eq!(param.name, "Gain");
248 assert!((param.value - 0.5).abs() < f32::EPSILON);
249 }
250
251 #[test]
252 fn test_get_parameter_not_found() {
253 let host = DevServerHost::new(test_params());
254 assert!(host.get_parameter("nonexistent").is_none());
255 }
256
257 #[test]
258 fn test_set_parameter() {
259 let host = DevServerHost::new(test_params());
260
261 host.set_parameter("gain", 0.75).expect("should set gain");
262
263 let param = host.get_parameter("gain").expect("should find gain");
264 assert!((param.value - 0.75).abs() < f32::EPSILON);
265 }
266
267 #[test]
268 fn test_set_parameter_invalid_id() {
269 let host = DevServerHost::new(test_params());
270 let result = host.set_parameter("invalid", 0.5);
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn test_set_parameter_out_of_range() {
276 let host = DevServerHost::new(test_params());
277
278 let result = host.set_parameter("gain", 1.5);
279 assert!(result.is_err());
280
281 let result = host.set_parameter("gain", -0.1);
282 assert!(result.is_err());
283 }
284
285 #[test]
286 fn test_get_all_parameters() {
287 let host = DevServerHost::new(test_params());
288
289 let params = host.get_all_parameters();
290 assert_eq!(params.len(), 2);
291 assert!(params.iter().any(|p| p.id == "gain"));
292 assert!(params.iter().any(|p| p.id == "mix"));
293 }
294
295 #[test]
296 fn test_get_meter_frame() {
297 let host = DevServerHost::new(test_params());
298 assert!(host.get_meter_frame().is_none());
300
301 host.set_latest_meter_frame(&MeterUpdateNotification {
302 timestamp_us: 42,
303 left_peak: 0.9,
304 left_rms: 0.4,
305 right_peak: 0.8,
306 right_rms: 0.3,
307 });
308
309 let frame = host
310 .get_meter_frame()
311 .expect("meter frame should be populated after update");
312 assert!((frame.peak_l - 0.9).abs() < f32::EPSILON);
313 assert!((frame.rms_r - 0.3).abs() < f32::EPSILON);
314 assert_eq!(frame.timestamp, 42);
315 }
316
317 #[test]
318 fn test_audio_status_roundtrip() {
319 let host = DevServerHost::new(test_params());
320
321 let status = AudioRuntimeStatus {
322 phase: AudioRuntimePhase::RunningInputOnly,
323 diagnostic: None,
324 sample_rate: Some(44100.0),
325 buffer_size: Some(512),
326 updated_at_ms: 100,
327 };
328
329 host.set_audio_status(status.clone());
330
331 let stored = host
332 .get_audio_status()
333 .expect("audio status should always be present in dev host");
334 assert_eq!(stored.phase, status.phase);
335 assert_eq!(stored.buffer_size, status.buffer_size);
336 }
337
338 #[test]
339 fn test_get_oscilloscope_frame() {
340 let host = DevServerHost::new(test_params());
341 assert!(host.get_oscilloscope_frame().is_none());
342
343 host.set_latest_oscilloscope_frame(OscilloscopeFrame {
344 points_l: vec![0.1; 1024],
345 points_r: vec![0.2; 1024],
346 sample_rate: 48_000.0,
347 timestamp: 777,
348 no_signal: false,
349 trigger_mode: wavecraft_protocol::OscilloscopeTriggerMode::RisingZeroCrossing,
350 });
351
352 let frame = host
353 .get_oscilloscope_frame()
354 .expect("oscilloscope frame should be populated");
355 assert_eq!(frame.points_l.len(), 1024);
356 assert_eq!(frame.points_r.len(), 1024);
357 assert_eq!(frame.timestamp, 777);
358 }
359
360 #[tokio::test(flavor = "current_thread")]
361 async fn test_set_audio_status_inside_runtime_does_not_panic() {
362 let host = DevServerHost::new(test_params());
363
364 host.set_audio_status(AudioRuntimeStatus {
365 phase: AudioRuntimePhase::Initializing,
366 diagnostic: None,
367 sample_rate: Some(48000.0),
368 buffer_size: Some(256),
369 updated_at_ms: 200,
370 });
371
372 let stored = host
373 .get_audio_status()
374 .expect("audio status should always be present in dev host");
375 assert_eq!(stored.phase, AudioRuntimePhase::Initializing);
376 assert_eq!(stored.buffer_size, Some(256));
377 }
378}