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