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
21#[cfg(feature = "audio")]
22const INPUT_TRIM_LEVEL_PARAM_ID: &str = "input_trim_level";
23#[cfg(feature = "audio")]
24const LEGACY_INPUT_GAIN_LEVEL_PARAM_ID: &str = "input_gain_level";
25
26pub struct DevServerHost {
38 inner: InMemoryParameterHost,
39 latest_meter_frame: Arc<RwLock<Option<MeterFrame>>>,
40 latest_oscilloscope_frame: Arc<RwLock<Option<OscilloscopeFrame>>>,
41 audio_status: Arc<RwLock<AudioRuntimeStatus>>,
42 #[cfg(feature = "audio")]
43 param_bridge: Option<Arc<AtomicParameterBridge>>,
44}
45
46struct SharedState {
47 latest_meter_frame: Arc<RwLock<Option<MeterFrame>>>,
48 latest_oscilloscope_frame: Arc<RwLock<Option<OscilloscopeFrame>>>,
49 audio_status: Arc<RwLock<AudioRuntimeStatus>>,
50}
51
52impl DevServerHost {
53 fn initialize_shared_state() -> SharedState {
54 let latest_meter_frame = Arc::new(RwLock::new(None));
55 let latest_oscilloscope_frame = Arc::new(RwLock::new(None));
56 let audio_status = Arc::new(RwLock::new(AudioRuntimeStatus {
57 phase: AudioRuntimePhase::Disabled,
58 diagnostic: None,
59 sample_rate: None,
60 buffer_size: None,
61 updated_at_ms: now_millis(),
62 }));
63
64 SharedState {
65 latest_meter_frame,
66 latest_oscilloscope_frame,
67 audio_status,
68 }
69 }
70
71 #[cfg_attr(feature = "audio", allow(dead_code))]
80 pub fn new(parameters: Vec<ParameterInfo>) -> Self {
81 let inner = InMemoryParameterHost::new(parameters);
82 let shared_state = Self::initialize_shared_state();
83
84 Self {
85 inner,
86 latest_meter_frame: shared_state.latest_meter_frame,
87 latest_oscilloscope_frame: shared_state.latest_oscilloscope_frame,
88 audio_status: shared_state.audio_status,
89 #[cfg(feature = "audio")]
90 param_bridge: None,
91 }
92 }
93
94 #[cfg(feature = "audio")]
99 pub fn with_param_bridge(
100 parameters: Vec<ParameterInfo>,
101 bridge: Arc<AtomicParameterBridge>,
102 ) -> Self {
103 let inner = InMemoryParameterHost::new(parameters);
104 let shared_state = Self::initialize_shared_state();
105
106 Self {
107 inner,
108 latest_meter_frame: shared_state.latest_meter_frame,
109 latest_oscilloscope_frame: shared_state.latest_oscilloscope_frame,
110 audio_status: shared_state.audio_status,
111 param_bridge: Some(bridge),
112 }
113 }
114
115 pub fn replace_parameters(&self, new_params: Vec<ParameterInfo>) -> Result<(), String> {
126 self.inner.replace_parameters(new_params)?;
127
128 #[cfg(feature = "audio")]
129 if let Some(ref bridge) = self.param_bridge {
130 for parameter in self.inner.get_all_parameters() {
131 bridge.write(¶meter.id, parameter.value);
132
133 if parameter.id == INPUT_TRIM_LEVEL_PARAM_ID {
136 bridge.write(LEGACY_INPUT_GAIN_LEVEL_PARAM_ID, parameter.value);
137 } else if parameter.id == LEGACY_INPUT_GAIN_LEVEL_PARAM_ID {
138 bridge.write(INPUT_TRIM_LEVEL_PARAM_ID, parameter.value);
139 }
140 }
141 }
142
143 Ok(())
144 }
145
146 pub fn set_latest_meter_frame(&self, update: &MeterUpdateNotification) {
148 let mut meter = self
149 .latest_meter_frame
150 .write()
151 .expect("latest_meter_frame lock poisoned");
152 *meter = Some(MeterFrame {
153 peak_l: update.left_peak,
154 peak_r: update.right_peak,
155 rms_l: update.left_rms,
156 rms_r: update.right_rms,
157 timestamp: update.timestamp_us,
158 });
159 }
160
161 pub fn set_latest_oscilloscope_frame(&self, frame: OscilloscopeFrame) {
163 let mut oscilloscope = self
164 .latest_oscilloscope_frame
165 .write()
166 .expect("latest_oscilloscope_frame lock poisoned");
167 *oscilloscope = Some(frame);
168 }
169
170 pub fn set_audio_status(&self, status: AudioRuntimeStatus) {
172 let mut current = self
173 .audio_status
174 .write()
175 .expect("audio_status lock poisoned");
176 *current = status;
177 }
178}
179
180impl ParameterHost for DevServerHost {
181 fn get_parameter(&self, id: &str) -> Option<ParameterInfo> {
182 self.inner.get_parameter(id)
183 }
184
185 fn set_parameter(&self, id: &str, value: f32) -> Result<(), BridgeError> {
186 let result = self.inner.set_parameter(id, value);
187
188 #[cfg(feature = "audio")]
190 if result.is_ok()
191 && let Some(ref bridge) = self.param_bridge
192 {
193 bridge.write(id, value);
194
195 if id == INPUT_TRIM_LEVEL_PARAM_ID {
200 bridge.write(LEGACY_INPUT_GAIN_LEVEL_PARAM_ID, value);
201 } else if id == LEGACY_INPUT_GAIN_LEVEL_PARAM_ID {
202 bridge.write(INPUT_TRIM_LEVEL_PARAM_ID, value);
203 }
204 }
205
206 result
207 }
208
209 fn get_all_parameters(&self) -> Vec<ParameterInfo> {
210 self.inner.get_all_parameters()
211 }
212
213 fn get_meter_frame(&self) -> Option<MeterFrame> {
214 *self
215 .latest_meter_frame
216 .read()
217 .expect("latest_meter_frame lock poisoned")
218 }
219
220 fn get_oscilloscope_frame(&self) -> Option<OscilloscopeFrame> {
221 self.latest_oscilloscope_frame
222 .read()
223 .expect("latest_oscilloscope_frame lock poisoned")
224 .clone()
225 }
226
227 fn request_resize(&self, width: u32, height: u32) -> bool {
228 self.inner.request_resize(width, height)
229 }
230
231 fn get_audio_status(&self) -> Option<AudioRuntimeStatus> {
232 Some(
233 self.audio_status
234 .read()
235 .expect("audio_status lock poisoned")
236 .clone(),
237 )
238 }
239}
240
241fn now_millis() -> u64 {
242 SystemTime::now()
243 .duration_since(UNIX_EPOCH)
244 .map_or(0, |duration| duration.as_millis() as u64)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use wavecraft_protocol::ParameterType;
251
252 fn test_params() -> Vec<ParameterInfo> {
253 vec![
254 ParameterInfo {
255 id: "gain".to_string(),
256 name: "Gain".to_string(),
257 param_type: ParameterType::Float,
258 value: 0.5,
259 default: 0.5,
260 min: 0.0,
261 max: 1.0,
262 unit: Some("dB".to_string()),
263 group: Some("Input".to_string()),
264 variants: None,
265 },
266 ParameterInfo {
267 id: "mix".to_string(),
268 name: "Mix".to_string(),
269 param_type: ParameterType::Float,
270 value: 1.0,
271 default: 1.0,
272 min: 0.0,
273 max: 1.0,
274 unit: Some("%".to_string()),
275 group: None,
276 variants: None,
277 },
278 ]
279 }
280
281 #[test]
282 fn test_get_parameter() {
283 let host = DevServerHost::new(test_params());
284
285 let param = host.get_parameter("gain").expect("should find gain");
286 assert_eq!(param.id, "gain");
287 assert_eq!(param.name, "Gain");
288 assert!((param.value - 0.5).abs() < f32::EPSILON);
289 }
290
291 #[test]
292 fn test_get_parameter_not_found() {
293 let host = DevServerHost::new(test_params());
294 assert!(host.get_parameter("nonexistent").is_none());
295 }
296
297 #[test]
298 fn test_set_parameter() {
299 let host = DevServerHost::new(test_params());
300
301 host.set_parameter("gain", 0.75).expect("should set gain");
302
303 let param = host.get_parameter("gain").expect("should find gain");
304 assert!((param.value - 0.75).abs() < f32::EPSILON);
305 }
306
307 #[test]
308 fn test_set_parameter_invalid_id() {
309 let host = DevServerHost::new(test_params());
310 let result = host.set_parameter("invalid", 0.5);
311 assert!(result.is_err());
312 }
313
314 #[test]
315 fn test_set_parameter_out_of_range() {
316 let host = DevServerHost::new(test_params());
317
318 let result = host.set_parameter("gain", 1.5);
319 assert!(result.is_err());
320
321 let result = host.set_parameter("gain", -0.1);
322 assert!(result.is_err());
323 }
324
325 #[test]
326 fn test_get_all_parameters() {
327 let host = DevServerHost::new(test_params());
328
329 let params = host.get_all_parameters();
330 assert_eq!(params.len(), 2);
331 assert!(params.iter().any(|p| p.id == "gain"));
332 assert!(params.iter().any(|p| p.id == "mix"));
333 }
334
335 #[test]
336 fn test_get_meter_frame() {
337 let host = DevServerHost::new(test_params());
338 assert!(host.get_meter_frame().is_none());
340
341 host.set_latest_meter_frame(&MeterUpdateNotification {
342 timestamp_us: 42,
343 left_peak: 0.9,
344 left_rms: 0.4,
345 right_peak: 0.8,
346 right_rms: 0.3,
347 });
348
349 let frame = host
350 .get_meter_frame()
351 .expect("meter frame should be populated after update");
352 assert!((frame.peak_l - 0.9).abs() < f32::EPSILON);
353 assert!((frame.rms_r - 0.3).abs() < f32::EPSILON);
354 assert_eq!(frame.timestamp, 42);
355 }
356
357 #[test]
358 fn test_audio_status_roundtrip() {
359 let host = DevServerHost::new(test_params());
360
361 let status = AudioRuntimeStatus {
362 phase: AudioRuntimePhase::RunningInputOnly,
363 diagnostic: None,
364 sample_rate: Some(44100.0),
365 buffer_size: Some(512),
366 updated_at_ms: 100,
367 };
368
369 host.set_audio_status(status.clone());
370
371 let stored = host
372 .get_audio_status()
373 .expect("audio status should always be present in dev host");
374 assert_eq!(stored.phase, status.phase);
375 assert_eq!(stored.buffer_size, status.buffer_size);
376 }
377
378 #[test]
379 fn test_get_oscilloscope_frame() {
380 let host = DevServerHost::new(test_params());
381 assert!(host.get_oscilloscope_frame().is_none());
382
383 host.set_latest_oscilloscope_frame(OscilloscopeFrame {
384 points_l: vec![0.1; 1024],
385 points_r: vec![0.2; 1024],
386 sample_rate: 48_000.0,
387 timestamp: 777,
388 no_signal: false,
389 trigger_mode: wavecraft_protocol::OscilloscopeTriggerMode::RisingZeroCrossing,
390 });
391
392 let frame = host
393 .get_oscilloscope_frame()
394 .expect("oscilloscope frame should be populated");
395 assert_eq!(frame.points_l.len(), 1024);
396 assert_eq!(frame.points_r.len(), 1024);
397 assert_eq!(frame.timestamp, 777);
398 }
399
400 #[tokio::test(flavor = "current_thread")]
401 async fn test_set_audio_status_inside_runtime_does_not_panic() {
402 let host = DevServerHost::new(test_params());
403
404 host.set_audio_status(AudioRuntimeStatus {
405 phase: AudioRuntimePhase::Initializing,
406 diagnostic: None,
407 sample_rate: Some(48000.0),
408 buffer_size: Some(256),
409 updated_at_ms: 200,
410 });
411
412 let stored = host
413 .get_audio_status()
414 .expect("audio status should always be present in dev host");
415 assert_eq!(stored.phase, AudioRuntimePhase::Initializing);
416 assert_eq!(stored.buffer_size, Some(256));
417 }
418}