phytrace_sdk/models/domains/
audio.rs

1//! Audio Domain - Microphones, speakers, and sound detection.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use validator::Validate;
6
7use crate::models::enums::{SensorStatus, SoundType};
8
9/// Audio domain containing microphone, speaker, and sound detection information.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct AudioDomain {
12    /// Microphones
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub microphones: Option<Vec<Microphone>>,
15
16    /// Speakers
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub speakers: Option<Vec<Speaker>>,
19
20    /// Sound detections
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub sound_detection: Option<Vec<SoundDetection>>,
23
24    /// Voice output
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub voice_output: Option<VoiceOutput>,
27
28    /// Ambient noise level (dB)
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub ambient_noise_db: Option<f64>,
31}
32
33/// Microphone information.
34#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
35pub struct Microphone {
36    /// Microphone ID
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub mic_id: Option<String>,
39
40    /// Status
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub status: Option<SensorStatus>,
43
44    /// Current level (dB)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub level_db: Option<f64>,
47
48    /// Whether recording
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub is_recording: Option<bool>,
51
52    /// Sample rate (Hz)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub sample_rate_hz: Option<u32>,
55}
56
57/// Speaker information.
58#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
59pub struct Speaker {
60    /// Speaker ID
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub speaker_id: Option<String>,
63
64    /// Status
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub status: Option<SensorStatus>,
67
68    /// Whether playing
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub is_playing: Option<bool>,
71
72    /// Volume level (0-100)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    #[validate(range(min = 0.0, max = 100.0))]
75    pub volume_pct: Option<f64>,
76}
77
78/// Sound detection event.
79#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
80pub struct SoundDetection {
81    /// Detection ID
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub detection_id: Option<String>,
84
85    /// Sound type
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub sound_type: Option<SoundType>,
88
89    /// Confidence (0-1)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    #[validate(range(min = 0.0, max = 1.0))]
92    pub confidence: Option<f64>,
93
94    /// Direction (degrees from front)
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub direction_deg: Option<f64>,
97
98    /// Distance estimate (m)
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub distance_m: Option<f64>,
101
102    /// Level (dB)
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub level_db: Option<f64>,
105
106    /// Timestamp
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub timestamp: Option<DateTime<Utc>>,
109
110    /// Description
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub description: Option<String>,
113}
114
115/// Voice output status.
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct VoiceOutput {
118    /// Whether speaking
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub is_speaking: Option<bool>,
121
122    /// Current utterance
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub current_text: Option<String>,
125
126    /// TTS engine
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub tts_engine: Option<String>,
129
130    /// Language
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub language: Option<String>,
133
134    /// Voice name
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub voice: Option<String>,
137}
138
139impl AudioDomain {
140    /// Add a microphone.
141    pub fn with_microphone(mut self, mic: Microphone) -> Self {
142        let mics = self.microphones.get_or_insert_with(Vec::new);
143        mics.push(mic);
144        self
145    }
146
147    /// Add a speaker.
148    pub fn with_speaker(mut self, speaker: Speaker) -> Self {
149        let speakers = self.speakers.get_or_insert_with(Vec::new);
150        speakers.push(speaker);
151        self
152    }
153
154    /// Add a sound detection.
155    pub fn with_detection(mut self, detection: SoundDetection) -> Self {
156        let detections = self.sound_detection.get_or_insert_with(Vec::new);
157        detections.push(detection);
158        self
159    }
160
161    /// Set ambient noise.
162    pub fn with_ambient_noise(mut self, db: f64) -> Self {
163        self.ambient_noise_db = Some(db);
164        self
165    }
166}
167
168impl SoundDetection {
169    /// Create a sound detection.
170    pub fn new(sound_type: SoundType, confidence: f64) -> Self {
171        Self {
172            detection_id: Some(uuid::Uuid::new_v4().to_string()),
173            sound_type: Some(sound_type),
174            confidence: Some(confidence),
175            timestamp: Some(Utc::now()),
176            ..Default::default()
177        }
178    }
179
180    /// Set direction.
181    pub fn with_direction(mut self, deg: f64) -> Self {
182        self.direction_deg = Some(deg);
183        self
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_sound_detection() {
193        let detection = SoundDetection::new(SoundType::Speech, 0.9).with_direction(45.0);
194
195        assert_eq!(detection.sound_type, Some(SoundType::Speech));
196        assert!(detection.timestamp.is_some());
197    }
198
199    // ==========================================================================
200    // AudioDomain Tests
201    // ==========================================================================
202
203    #[test]
204    fn test_audio_domain_default() {
205        let audio = AudioDomain::default();
206        assert!(audio.microphones.is_none());
207        assert!(audio.speakers.is_none());
208        assert!(audio.sound_detection.is_none());
209        assert!(audio.voice_output.is_none());
210        assert!(audio.ambient_noise_db.is_none());
211    }
212
213    #[test]
214    fn test_audio_domain_with_microphone() {
215        let mic = Microphone {
216            mic_id: Some("mic-001".to_string()),
217            status: Some(SensorStatus::Ok),
218            level_db: Some(-20.0),
219            is_recording: Some(true),
220            sample_rate_hz: Some(44100),
221        };
222
223        let audio = AudioDomain::default().with_microphone(mic);
224        assert!(audio.microphones.is_some());
225        assert_eq!(audio.microphones.as_ref().unwrap().len(), 1);
226        assert_eq!(
227            audio.microphones.as_ref().unwrap()[0].mic_id,
228            Some("mic-001".to_string())
229        );
230    }
231
232    #[test]
233    fn test_audio_domain_with_multiple_microphones() {
234        let mic1 = Microphone {
235            mic_id: Some("mic-001".to_string()),
236            ..Default::default()
237        };
238        let mic2 = Microphone {
239            mic_id: Some("mic-002".to_string()),
240            ..Default::default()
241        };
242
243        let audio = AudioDomain::default()
244            .with_microphone(mic1)
245            .with_microphone(mic2);
246
247        assert_eq!(audio.microphones.as_ref().unwrap().len(), 2);
248    }
249
250    #[test]
251    fn test_audio_domain_with_speaker() {
252        let speaker = Speaker {
253            speaker_id: Some("spk-001".to_string()),
254            status: Some(SensorStatus::Ok),
255            is_playing: Some(true),
256            volume_pct: Some(75.0),
257        };
258
259        let audio = AudioDomain::default().with_speaker(speaker);
260        assert!(audio.speakers.is_some());
261        assert_eq!(audio.speakers.as_ref().unwrap()[0].volume_pct, Some(75.0));
262    }
263
264    #[test]
265    fn test_audio_domain_with_detection() {
266        let detection = SoundDetection::new(SoundType::Alarm, 0.95);
267        let audio = AudioDomain::default().with_detection(detection);
268
269        assert!(audio.sound_detection.is_some());
270        assert_eq!(
271            audio.sound_detection.as_ref().unwrap()[0].sound_type,
272            Some(SoundType::Alarm)
273        );
274    }
275
276    #[test]
277    fn test_audio_domain_with_ambient_noise() {
278        let audio = AudioDomain::default().with_ambient_noise(65.5);
279        assert_eq!(audio.ambient_noise_db, Some(65.5));
280    }
281
282    #[test]
283    fn test_audio_domain_chained_builders() {
284        let audio = AudioDomain::default()
285            .with_microphone(Microphone::default())
286            .with_speaker(Speaker::default())
287            .with_detection(SoundDetection::new(SoundType::Speech, 0.8))
288            .with_ambient_noise(50.0);
289
290        assert!(audio.microphones.is_some());
291        assert!(audio.speakers.is_some());
292        assert!(audio.sound_detection.is_some());
293        assert!(audio.ambient_noise_db.is_some());
294    }
295
296    // ==========================================================================
297    // Microphone Tests
298    // ==========================================================================
299
300    #[test]
301    fn test_microphone_default() {
302        let mic = Microphone::default();
303        assert!(mic.mic_id.is_none());
304        assert!(mic.status.is_none());
305        assert!(mic.level_db.is_none());
306        assert!(mic.is_recording.is_none());
307        assert!(mic.sample_rate_hz.is_none());
308    }
309
310    #[test]
311    fn test_microphone_full() {
312        let mic = Microphone {
313            mic_id: Some("mic-front".to_string()),
314            status: Some(SensorStatus::Ok),
315            level_db: Some(-15.5),
316            is_recording: Some(true),
317            sample_rate_hz: Some(48000),
318        };
319
320        assert_eq!(mic.mic_id, Some("mic-front".to_string()));
321        assert_eq!(mic.status, Some(SensorStatus::Ok));
322        assert_eq!(mic.level_db, Some(-15.5));
323        assert_eq!(mic.is_recording, Some(true));
324        assert_eq!(mic.sample_rate_hz, Some(48000));
325    }
326
327    #[test]
328    fn test_microphone_serialization() {
329        let mic = Microphone {
330            mic_id: Some("mic-001".to_string()),
331            level_db: Some(-20.0),
332            ..Default::default()
333        };
334
335        let json = serde_json::to_string(&mic).unwrap();
336        assert!(json.contains("mic-001"));
337        assert!(json.contains("-20"));
338
339        let deserialized: Microphone = serde_json::from_str(&json).unwrap();
340        assert_eq!(deserialized.mic_id, Some("mic-001".to_string()));
341    }
342
343    // ==========================================================================
344    // Speaker Tests
345    // ==========================================================================
346
347    #[test]
348    fn test_speaker_default() {
349        let speaker = Speaker::default();
350        assert!(speaker.speaker_id.is_none());
351        assert!(speaker.status.is_none());
352        assert!(speaker.is_playing.is_none());
353        assert!(speaker.volume_pct.is_none());
354    }
355
356    #[test]
357    fn test_speaker_full() {
358        let speaker = Speaker {
359            speaker_id: Some("spk-main".to_string()),
360            status: Some(SensorStatus::Ok),
361            is_playing: Some(true),
362            volume_pct: Some(80.0),
363        };
364
365        assert_eq!(speaker.volume_pct, Some(80.0));
366        assert_eq!(speaker.is_playing, Some(true));
367    }
368
369    #[test]
370    fn test_speaker_serialization() {
371        let speaker = Speaker {
372            speaker_id: Some("spk-001".to_string()),
373            volume_pct: Some(50.0),
374            ..Default::default()
375        };
376
377        let json = serde_json::to_string(&speaker).unwrap();
378        let deserialized: Speaker = serde_json::from_str(&json).unwrap();
379        assert_eq!(deserialized.volume_pct, Some(50.0));
380    }
381
382    // ==========================================================================
383    // SoundDetection Tests
384    // ==========================================================================
385
386    #[test]
387    fn test_sound_detection_new() {
388        let detection = SoundDetection::new(SoundType::Alarm, 0.85);
389
390        assert!(detection.detection_id.is_some());
391        assert_eq!(detection.sound_type, Some(SoundType::Alarm));
392        assert_eq!(detection.confidence, Some(0.85));
393        assert!(detection.timestamp.is_some());
394    }
395
396    #[test]
397    fn test_sound_detection_with_direction() {
398        let detection = SoundDetection::new(SoundType::Speech, 0.9).with_direction(135.0);
399
400        assert_eq!(detection.direction_deg, Some(135.0));
401    }
402
403    #[test]
404    fn test_sound_detection_default() {
405        let detection = SoundDetection::default();
406        assert!(detection.detection_id.is_none());
407        assert!(detection.sound_type.is_none());
408        assert!(detection.confidence.is_none());
409        assert!(detection.direction_deg.is_none());
410        assert!(detection.distance_m.is_none());
411        assert!(detection.level_db.is_none());
412        assert!(detection.timestamp.is_none());
413        assert!(detection.description.is_none());
414    }
415
416    #[test]
417    fn test_sound_detection_full() {
418        let detection = SoundDetection {
419            detection_id: Some("det-001".to_string()),
420            sound_type: Some(SoundType::Machine),
421            confidence: Some(0.92),
422            direction_deg: Some(270.0),
423            distance_m: Some(5.5),
424            level_db: Some(72.0),
425            timestamp: Some(Utc::now()),
426            description: Some("Conveyor belt noise".to_string()),
427        };
428
429        assert_eq!(detection.distance_m, Some(5.5));
430        assert_eq!(detection.level_db, Some(72.0));
431    }
432
433    #[test]
434    fn test_sound_detection_serialization() {
435        let detection = SoundDetection::new(SoundType::Speech, 0.9);
436        let json = serde_json::to_string(&detection).unwrap();
437        let deserialized: SoundDetection = serde_json::from_str(&json).unwrap();
438
439        assert_eq!(deserialized.sound_type, Some(SoundType::Speech));
440        assert_eq!(deserialized.confidence, Some(0.9));
441    }
442
443    // ==========================================================================
444    // VoiceOutput Tests
445    // ==========================================================================
446
447    #[test]
448    fn test_voice_output_default() {
449        let voice = VoiceOutput::default();
450        assert!(voice.is_speaking.is_none());
451        assert!(voice.current_text.is_none());
452        assert!(voice.tts_engine.is_none());
453        assert!(voice.language.is_none());
454        assert!(voice.voice.is_none());
455    }
456
457    #[test]
458    fn test_voice_output_full() {
459        let voice = VoiceOutput {
460            is_speaking: Some(true),
461            current_text: Some("Hello, how can I help you?".to_string()),
462            tts_engine: Some("pico".to_string()),
463            language: Some("en-US".to_string()),
464            voice: Some("default".to_string()),
465        };
466
467        assert_eq!(voice.is_speaking, Some(true));
468        assert_eq!(
469            voice.current_text,
470            Some("Hello, how can I help you?".to_string())
471        );
472    }
473
474    #[test]
475    fn test_voice_output_serialization() {
476        let voice = VoiceOutput {
477            is_speaking: Some(false),
478            language: Some("en-GB".to_string()),
479            ..Default::default()
480        };
481
482        let json = serde_json::to_string(&voice).unwrap();
483        let deserialized: VoiceOutput = serde_json::from_str(&json).unwrap();
484        assert_eq!(deserialized.language, Some("en-GB".to_string()));
485    }
486
487    // ==========================================================================
488    // AudioDomain Serialization Tests
489    // ==========================================================================
490
491    #[test]
492    fn test_audio_domain_serialization_roundtrip() {
493        let audio = AudioDomain::default()
494            .with_microphone(Microphone {
495                mic_id: Some("mic-001".to_string()),
496                level_db: Some(-25.0),
497                ..Default::default()
498            })
499            .with_ambient_noise(55.0);
500
501        let json = serde_json::to_string(&audio).unwrap();
502        let deserialized: AudioDomain = serde_json::from_str(&json).unwrap();
503
504        assert_eq!(deserialized.ambient_noise_db, Some(55.0));
505        assert!(deserialized.microphones.is_some());
506        assert_eq!(
507            deserialized.microphones.as_ref().unwrap()[0].level_db,
508            Some(-25.0)
509        );
510    }
511}