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