Skip to main content

jugar_probar/emulation/
audio.rs

1//! Audio Emulation for WASM Testing (PROBAR-SPEC-010)
2//!
3//! Mock `getUserMedia` with controlled audio for streaming ASR testing.
4//!
5//! ## Toyota Way Application:
6//! - **Poka-Yoke**: Type-safe audio source configuration prevents invalid audio
7//! - **Jidoka**: Automatic detection of audio injection failures
8//! - **Muda**: Eliminates need for real microphone in CI environments
9//!
10//! ## References:
11//! - [11] Radford et al. (2023) Whisper streaming patterns
12//! - [12] Sohn et al. (2015) VAD state machine testing
13
14use std::f32::consts::PI;
15
16/// Audio source types for injection (H4-H6 falsification)
17#[derive(Debug, Clone)]
18pub enum AudioSource {
19    /// Sine wave at specified frequency (Hz)
20    SineWave {
21        /// Frequency in Hz (must be > 0, capped at Nyquist)
22        frequency: f32,
23        /// Amplitude in range [0.0, 1.0]
24        amplitude: f32,
25    },
26
27    /// Speech-like audio (fundamental + harmonics)
28    /// Per Radford et al. [11], speech has characteristic harmonic structure
29    SpeechPattern {
30        /// Fundamental frequency (100-300 Hz typical for speech)
31        fundamental_hz: f32,
32        /// Harmonic amplitudes relative to fundamental (e.g., [0.5, 0.3, 0.2, 0.1])
33        harmonics: Vec<f32>,
34        /// Pitch variation in Hz (adds natural variation)
35        variation_hz: f32,
36    },
37
38    /// Silence with optional background noise
39    Silence {
40        /// Noise floor in dB (negative values, e.g., -60.0)
41        noise_floor_db: f32,
42    },
43
44    /// White noise (for VAD testing - should NOT be classified as speech)
45    WhiteNoise {
46        /// Amplitude in range [0.0, 1.0]
47        amplitude: f32,
48    },
49
50    /// Pre-recorded audio samples (for deterministic testing)
51    Samples {
52        /// Raw f32 samples in range [-1.0, 1.0]
53        data: Vec<f32>,
54        /// Sample rate of the data
55        sample_rate: u32,
56        /// Whether to loop when exhausted
57        loop_playback: bool,
58    },
59}
60
61impl Default for AudioSource {
62    fn default() -> Self {
63        Self::Silence {
64            noise_floor_db: -60.0,
65        }
66    }
67}
68
69/// Audio emulator configuration
70#[derive(Debug, Clone)]
71pub struct AudioEmulatorConfig {
72    /// Output sample rate (typically 16000 for ASR, 44100/48000 for general audio)
73    pub sample_rate: u32,
74    /// Number of channels (1 = mono, 2 = stereo)
75    pub channels: u8,
76    /// Buffer size in samples per callback
77    pub buffer_size: usize,
78}
79
80impl Default for AudioEmulatorConfig {
81    fn default() -> Self {
82        Self {
83            sample_rate: 16000, // Whisper expects 16kHz
84            channels: 1,        // Mono for ASR
85            buffer_size: 1024,
86        }
87    }
88}
89
90/// Audio emulator for injecting controlled audio into browser tests
91///
92/// ## Usage
93/// ```rust,ignore
94/// let audio = AudioEmulator::new(AudioSource::SpeechPattern {
95///     fundamental_hz: 150.0,
96///     harmonics: vec![0.5, 0.3, 0.2, 0.1],
97///     variation_hz: 20.0,
98/// });
99/// let samples = audio.generate_samples(3.0); // 3 seconds
100/// ```
101#[derive(Debug, Clone)]
102pub struct AudioEmulator {
103    source: AudioSource,
104    config: AudioEmulatorConfig,
105    /// Current phase for oscillator-based sources
106    phase: f32,
107    /// Sample counter for time tracking
108    sample_count: u64,
109    /// Random state for noise generation (deterministic seed)
110    rng_state: u64,
111}
112
113impl AudioEmulator {
114    /// Create a new audio emulator with the given source
115    #[must_use]
116    pub fn new(source: AudioSource) -> Self {
117        Self::with_config(source, AudioEmulatorConfig::default())
118    }
119
120    /// Create with custom configuration
121    #[must_use]
122    pub fn with_config(source: AudioSource, config: AudioEmulatorConfig) -> Self {
123        Self {
124            source,
125            config,
126            phase: 0.0,
127            sample_count: 0,
128            rng_state: 0x853c_49e6_748f_ea9b, // Fixed seed for determinism
129        }
130    }
131
132    /// Get the configured sample rate
133    #[must_use]
134    pub fn sample_rate(&self) -> u32 {
135        self.config.sample_rate
136    }
137
138    /// Get the number of samples generated so far
139    #[must_use]
140    pub fn samples_generated(&self) -> u64 {
141        self.sample_count
142    }
143
144    /// Generate samples for the specified duration in seconds
145    #[must_use]
146    pub fn generate_samples(&mut self, duration_seconds: f32) -> Vec<f32> {
147        let num_samples = (duration_seconds * self.config.sample_rate as f32) as usize;
148        self.generate_n_samples(num_samples)
149    }
150
151    /// Generate exactly N samples
152    #[must_use]
153    pub fn generate_n_samples(&mut self, num_samples: usize) -> Vec<f32> {
154        let mut samples = Vec::with_capacity(num_samples);
155        let sample_rate = self.config.sample_rate as f32;
156
157        for _ in 0..num_samples {
158            let sample = self.generate_single_sample(sample_rate);
159            samples.push(sample);
160            self.sample_count += 1;
161        }
162
163        samples
164    }
165
166    /// Generate a single sample
167    fn generate_single_sample(&mut self, sample_rate: f32) -> f32 {
168        match &self.source {
169            AudioSource::SineWave {
170                frequency,
171                amplitude,
172            } => {
173                let freq = frequency.clamp(0.001, sample_rate / 2.0);
174                let amp = amplitude.clamp(0.0, 1.0);
175                let sample = (self.phase * 2.0 * PI).sin() * amp;
176                self.phase += freq / sample_rate;
177                if self.phase >= 1.0 {
178                    self.phase -= 1.0;
179                }
180                sample
181            }
182
183            AudioSource::SpeechPattern {
184                fundamental_hz,
185                harmonics,
186                variation_hz,
187            } => {
188                let freq = fundamental_hz.clamp(20.0, sample_rate / 2.0);
189                let var = variation_hz.clamp(0.0, freq / 2.0);
190
191                // Add slow variation to fundamental (simulates natural pitch variation)
192                let time = self.sample_count as f32 / sample_rate;
193                let freq_with_variation = freq + var * (time * 5.0).sin();
194
195                // Generate fundamental
196                let mut sample = (self.phase * 2.0 * PI).sin();
197
198                // Add harmonics
199                for (i, &harmonic_amp) in harmonics.iter().enumerate() {
200                    let harmonic_num = (i + 2) as f32;
201                    let harmonic_freq = freq_with_variation * harmonic_num;
202                    if harmonic_freq < sample_rate / 2.0 {
203                        let harmonic_phase = self.phase * harmonic_num;
204                        sample += (harmonic_phase * 2.0 * PI).sin() * harmonic_amp.clamp(0.0, 1.0);
205                    }
206                }
207
208                // Normalize to prevent clipping
209                let total_amp = 1.0 + harmonics.iter().sum::<f32>();
210                sample /= total_amp.max(1.0);
211
212                // Advance phase
213                self.phase += freq_with_variation / sample_rate;
214                if self.phase >= 1.0 {
215                    self.phase -= 1.0;
216                }
217
218                sample.clamp(-1.0, 1.0)
219            }
220
221            AudioSource::Silence { noise_floor_db } => {
222                // Convert dB to linear amplitude
223                let amp = 10.0_f32.powf(noise_floor_db.clamp(-100.0, 0.0) / 20.0);
224                // Generate noise at that level
225                let noise = self.next_random_f32() * 2.0 - 1.0;
226                noise * amp
227            }
228
229            AudioSource::WhiteNoise { amplitude } => {
230                let amp = amplitude.clamp(0.0, 1.0);
231                let noise = self.next_random_f32() * 2.0 - 1.0;
232                noise * amp
233            }
234
235            AudioSource::Samples {
236                data,
237                sample_rate: _src_rate,
238                loop_playback,
239            } => {
240                if data.is_empty() {
241                    return 0.0;
242                }
243                let idx = self.sample_count as usize;
244                if idx < data.len() {
245                    data[idx].clamp(-1.0, 1.0)
246                } else if *loop_playback {
247                    data[idx % data.len()].clamp(-1.0, 1.0)
248                } else {
249                    0.0
250                }
251            }
252        }
253    }
254
255    /// Simple xorshift64 PRNG for deterministic noise
256    fn next_random_f32(&mut self) -> f32 {
257        self.rng_state ^= self.rng_state << 13;
258        self.rng_state ^= self.rng_state >> 7;
259        self.rng_state ^= self.rng_state << 17;
260        // Convert to [0, 1) range
261        (self.rng_state as f32) / (u64::MAX as f32)
262    }
263
264    /// Reset the emulator state (phase and sample counter)
265    pub fn reset(&mut self) {
266        self.phase = 0.0;
267        self.sample_count = 0;
268        self.rng_state = 0x853c_49e6_748f_ea9b;
269    }
270
271    /// Generate JavaScript code to inject into page for mocking getUserMedia
272    #[must_use]
273    pub fn generate_mock_js(&self, samples: &[f32]) -> String {
274        // Convert samples to JSON array
275        let samples_json: String = samples
276            .iter()
277            .map(|s| format!("{s:.6}"))
278            .collect::<Vec<_>>()
279            .join(",");
280
281        format!(
282            r#"
283(function() {{
284    const mockSamples = new Float32Array([{samples_json}]);
285    const sampleRate = {sample_rate};
286    let sampleIndex = 0;
287
288    // Create mock MediaStream
289    const audioContext = new AudioContext({{ sampleRate: sampleRate }});
290    const bufferSize = 1024;
291    const scriptNode = audioContext.createScriptProcessor(bufferSize, 1, 1);
292
293    scriptNode.onaudioprocess = function(e) {{
294        const output = e.outputBuffer.getChannelData(0);
295        for (let i = 0; i < bufferSize; i++) {{
296            if (sampleIndex < mockSamples.length) {{
297                output[i] = mockSamples[sampleIndex++];
298            }} else {{
299                output[i] = 0;
300            }}
301        }}
302    }};
303
304    const dest = audioContext.createMediaStreamDestination();
305    scriptNode.connect(dest);
306    scriptNode.connect(audioContext.destination);
307
308    // Override getUserMedia
309    const originalGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
310    navigator.mediaDevices.getUserMedia = async function(constraints) {{
311        if (constraints.audio) {{
312            return dest.stream;
313        }}
314        return originalGetUserMedia(constraints);
315    }};
316
317    window.__PROBAR_AUDIO_EMULATOR__ = {{
318        sampleIndex: () => sampleIndex,
319        reset: () => {{ sampleIndex = 0; }},
320        context: audioContext
321    }};
322}})();
323"#,
324            samples_json = samples_json,
325            sample_rate = self.config.sample_rate
326        )
327    }
328
329    /// Inject audio emulation into a CDP page
330    ///
331    /// Generates samples and injects them into the page, mocking `getUserMedia`.
332    ///
333    /// # Arguments
334    /// * `page` - The CDP page to inject into
335    /// * `duration_seconds` - Duration of audio to generate
336    ///
337    /// # Example
338    /// ```ignore
339    /// use jugar_probar::emulation::{AudioEmulator, AudioSource};
340    ///
341    /// let mut audio = AudioEmulator::new(AudioSource::SpeechPattern {
342    ///     fundamental_hz: 150.0,
343    ///     harmonics: vec![0.5, 0.3, 0.2],
344    ///     variation_hz: 20.0,
345    /// });
346    /// audio.inject_cdp(&page, 5.0).await?; // 5 seconds of audio
347    /// ```
348    #[cfg(feature = "browser")]
349    pub async fn inject_cdp(
350        &mut self,
351        page: &chromiumoxide::Page,
352        duration_seconds: f32,
353    ) -> Result<(), AudioEmulatorError> {
354        // Generate samples
355        let samples = self.generate_samples(duration_seconds);
356
357        // Generate and inject the mock JavaScript
358        let js = self.generate_mock_js(&samples);
359        page.evaluate(js.as_str()).await.map_err(|e| {
360            AudioEmulatorError::InjectionFailed(format!("CDP injection failed: {e}"))
361        })?;
362
363        Ok(())
364    }
365
366    /// Check if audio emulation is active on a CDP page
367    #[cfg(feature = "browser")]
368    pub async fn is_active_cdp(page: &chromiumoxide::Page) -> Result<bool, AudioEmulatorError> {
369        let result: bool = page
370            .evaluate("typeof window.__PROBAR_AUDIO_EMULATOR__ !== 'undefined'")
371            .await
372            .map_err(|e| AudioEmulatorError::InjectionFailed(format!("CDP check failed: {e}")))?
373            .into_value()
374            .unwrap_or(false);
375
376        Ok(result)
377    }
378
379    /// Get current sample index from CDP page (tracks playback progress)
380    #[cfg(feature = "browser")]
381    pub async fn get_sample_index_cdp(
382        page: &chromiumoxide::Page,
383    ) -> Result<u64, AudioEmulatorError> {
384        let result: f64 = page
385            .evaluate("window.__PROBAR_AUDIO_EMULATOR__?.sampleIndex() ?? 0")
386            .await
387            .map_err(|e| AudioEmulatorError::InjectionFailed(format!("CDP query failed: {e}")))?
388            .into_value()
389            .unwrap_or(0.0);
390
391        Ok(result as u64)
392    }
393}
394
395/// Error type for audio emulation
396#[derive(Debug, Clone)]
397pub enum AudioEmulatorError {
398    /// Injection failed
399    InjectionFailed(String),
400    /// Audio context not available
401    ContextNotAvailable,
402    /// Invalid configuration
403    InvalidConfig(String),
404}
405
406impl std::fmt::Display for AudioEmulatorError {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        match self {
409            Self::InjectionFailed(msg) => write!(f, "Audio injection failed: {msg}"),
410            Self::ContextNotAvailable => write!(f, "Audio context not available"),
411            Self::InvalidConfig(msg) => write!(f, "Invalid audio config: {msg}"),
412        }
413    }
414}
415
416impl std::error::Error for AudioEmulatorError {}
417
418#[cfg(test)]
419#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
420mod tests {
421    use super::*;
422
423    // ========================================================================
424    // H4: Audio injection is reliable - Falsification tests
425    // ========================================================================
426
427    #[test]
428    fn f016_zero_length_audio_no_crash() {
429        // Falsification: Zero-length audio should not crash
430        let mut emulator = AudioEmulator::new(AudioSource::Silence {
431            noise_floor_db: -60.0,
432        });
433        let samples = emulator.generate_samples(0.0);
434        assert!(samples.is_empty());
435    }
436
437    #[test]
438    fn f017_context_suspended_handling() {
439        // Falsification: Audio context suspension should be handled gracefully
440        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
441            frequency: 440.0,
442            amplitude: 0.5,
443        });
444        // Simulate context by generating samples
445        let samples = emulator.generate_samples(0.1);
446        assert!(!samples.is_empty());
447        // Reset simulates context resume
448        emulator.reset();
449        let samples_after = emulator.generate_samples(0.1);
450        assert!(!samples_after.is_empty());
451    }
452
453    #[test]
454    fn f018_sample_rate_mismatch() {
455        // Falsification: Different sample rates should be handled
456        let mut emulator_16k = AudioEmulator::with_config(
457            AudioSource::SineWave {
458                frequency: 440.0,
459                amplitude: 1.0,
460            },
461            AudioEmulatorConfig {
462                sample_rate: 16000,
463                ..Default::default()
464            },
465        );
466        let mut emulator_48k = AudioEmulator::with_config(
467            AudioSource::SineWave {
468                frequency: 440.0,
469                amplitude: 1.0,
470            },
471            AudioEmulatorConfig {
472                sample_rate: 48000,
473                ..Default::default()
474            },
475        );
476        let samples_16k = emulator_16k.generate_samples(0.1);
477        let samples_48k = emulator_48k.generate_samples(0.1);
478        // 48kHz should produce 3x more samples for same duration
479        assert!(samples_48k.len() >= samples_16k.len() * 2);
480    }
481
482    #[test]
483    fn f019_permission_denied_mock() {
484        // Falsification: Even with mocked audio, edge cases should work
485        // This simulates the scenario where getUserMedia would fail
486        let emulator = AudioEmulator::new(AudioSource::Silence {
487            noise_floor_db: -100.0,
488        });
489        // The mock JS should handle permission scenarios gracefully
490        let mock_js = emulator.generate_mock_js(&[]);
491        assert!(mock_js.contains("getUserMedia"));
492        assert!(mock_js.contains("audio"));
493    }
494
495    #[test]
496    fn f020_ultrasonic_filtered() {
497        // Falsification: Ultrasonic frequencies (>22kHz) should be capped at Nyquist
498        let mut emulator = AudioEmulator::with_config(
499            AudioSource::SineWave {
500                frequency: 50000.0, // Way above Nyquist for 16kHz
501                amplitude: 1.0,
502            },
503            AudioEmulatorConfig {
504                sample_rate: 16000,
505                ..Default::default()
506            },
507        );
508        let samples = emulator.generate_samples(0.1);
509        // Should not crash and should produce valid samples
510        assert!(!samples.is_empty());
511        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
512    }
513
514    // ========================================================================
515    // H5: Pattern generation is accurate - Falsification tests
516    // ========================================================================
517
518    #[test]
519    fn f021_zero_hz_sine() {
520        // Falsification: 0 Hz sine should be clamped to minimum frequency
521        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
522            frequency: 0.0,
523            amplitude: 1.0,
524        });
525        let samples = emulator.generate_samples(0.1);
526        assert!(!samples.is_empty());
527        // Should produce DC-like output (very slow oscillation)
528    }
529
530    #[test]
531    fn f022_negative_amplitude_handled() {
532        // Falsification: Negative amplitude should be clamped to 0
533        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
534            frequency: 440.0,
535            amplitude: -1.0,
536        });
537        let samples = emulator.generate_samples(0.1);
538        // All samples should be 0 or very close
539        assert!(samples.iter().all(|&s| s.abs() < 0.001));
540    }
541
542    #[test]
543    fn f023_amplitude_clamped() {
544        // Falsification: Amplitude > 1.0 should be clamped
545        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
546            frequency: 440.0,
547            amplitude: 5.0,
548        });
549        let samples = emulator.generate_samples(0.1);
550        // All samples should be in [-1, 1]
551        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
552    }
553
554    #[test]
555    fn f024_speech_pattern_no_harmonics() {
556        // Falsification: Speech pattern with empty harmonics should still work
557        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
558            fundamental_hz: 150.0,
559            harmonics: vec![],
560            variation_hz: 20.0,
561        });
562        let samples = emulator.generate_samples(0.1);
563        assert!(!samples.is_empty());
564        // Should produce valid samples (pure fundamental)
565        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
566    }
567
568    #[test]
569    fn f025_samples_callback_empty() {
570        // Falsification: Empty sample buffer should return zeros
571        let mut emulator = AudioEmulator::new(AudioSource::Samples {
572            data: vec![],
573            sample_rate: 16000,
574            loop_playback: false,
575        });
576        let samples = emulator.generate_samples(0.1);
577        assert!(samples.iter().all(|&s| s.abs() < f32::EPSILON));
578    }
579
580    // ========================================================================
581    // H6: VAD Detection Works - Falsification tests
582    // ========================================================================
583
584    #[test]
585    fn f026_pure_silence() {
586        // Falsification: Pure silence should have near-zero amplitude
587        let mut emulator = AudioEmulator::new(AudioSource::Silence {
588            noise_floor_db: -100.0,
589        });
590        let samples = emulator.generate_samples(0.1);
591        let rms = calculate_rms(&samples);
592        assert!(rms < 0.0001, "Silence RMS too high: {rms}");
593    }
594
595    #[test]
596    fn f027_white_noise_not_silent() {
597        // Falsification: White noise should have measurable energy
598        let mut emulator = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: 0.5 });
599        let samples = emulator.generate_samples(0.1);
600        let rms = calculate_rms(&samples);
601        assert!(rms > 0.1, "White noise RMS too low: {rms}");
602    }
603
604    #[test]
605    fn f028_speech_threshold_boundary() {
606        // Falsification: Speech pattern at various amplitudes
607        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
608            fundamental_hz: 150.0,
609            harmonics: vec![0.5, 0.3, 0.2],
610            variation_hz: 10.0,
611        });
612        let samples = emulator.generate_samples(0.5);
613        let rms = calculate_rms(&samples);
614        // Speech-like audio should have consistent energy
615        assert!(rms > 0.1 && rms < 1.0, "Speech RMS out of range: {rms}");
616    }
617
618    // ========================================================================
619    // Unit tests for core functionality
620    // ========================================================================
621
622    #[test]
623    fn test_sine_wave_generation() {
624        let mut emulator = AudioEmulator::with_config(
625            AudioSource::SineWave {
626                frequency: 440.0,
627                amplitude: 1.0,
628            },
629            AudioEmulatorConfig {
630                sample_rate: 44100,
631                ..Default::default()
632            },
633        );
634
635        let samples = emulator.generate_samples(0.01); // 10ms
636        assert_eq!(samples.len(), 441); // 44100 * 0.01
637
638        // Verify samples are in valid range
639        for &sample in &samples {
640            assert!((-1.0..=1.0).contains(&sample));
641        }
642
643        // Verify zero crossings (should be ~4.4 per 10ms at 440Hz)
644        let zero_crossings: usize = samples.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
645        assert!((7..=11).contains(&zero_crossings));
646    }
647
648    #[test]
649    fn test_speech_pattern_generation() {
650        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
651            fundamental_hz: 150.0,
652            harmonics: vec![0.5, 0.3, 0.2, 0.1],
653            variation_hz: 20.0,
654        });
655
656        let samples = emulator.generate_samples(1.0);
657        assert_eq!(samples.len(), 16000); // 16kHz * 1s
658
659        // Speech should have more complex waveform (more zero crossings than pure sine)
660        let zero_crossings: usize = samples.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
661        assert!(zero_crossings > 200, "Too few zero crossings for speech");
662    }
663
664    #[test]
665    fn test_deterministic_noise() {
666        // Noise with same seed should produce identical output
667        let mut emulator1 = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: 1.0 });
668        let mut emulator2 = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: 1.0 });
669
670        let samples1 = emulator1.generate_samples(0.1);
671        let samples2 = emulator2.generate_samples(0.1);
672
673        assert_eq!(samples1, samples2);
674    }
675
676    #[test]
677    fn test_reset() {
678        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
679            frequency: 440.0,
680            amplitude: 1.0,
681        });
682
683        let samples1 = emulator.generate_samples(0.1);
684        emulator.reset();
685        let samples2 = emulator.generate_samples(0.1);
686
687        // After reset, should produce identical output
688        assert_eq!(samples1, samples2);
689    }
690
691    #[test]
692    fn test_sample_counter() {
693        let mut emulator = AudioEmulator::new(AudioSource::Silence {
694            noise_floor_db: -60.0,
695        });
696        assert_eq!(emulator.samples_generated(), 0);
697
698        let _ = emulator.generate_n_samples(1000);
699        assert_eq!(emulator.samples_generated(), 1000);
700
701        let _ = emulator.generate_n_samples(500);
702        assert_eq!(emulator.samples_generated(), 1500);
703    }
704
705    #[test]
706    fn test_samples_source_with_loop() {
707        let data = vec![0.1, 0.2, 0.3, 0.4];
708        let mut emulator = AudioEmulator::new(AudioSource::Samples {
709            data,
710            sample_rate: 16000,
711            loop_playback: true,
712        });
713
714        let samples = emulator.generate_n_samples(10);
715        assert_eq!(samples[0], 0.1);
716        assert_eq!(samples[3], 0.4);
717        assert_eq!(samples[4], 0.1); // Looped
718        assert_eq!(samples[7], 0.4);
719    }
720
721    #[test]
722    fn test_samples_source_without_loop() {
723        let data = vec![0.1, 0.2, 0.3];
724        let mut emulator = AudioEmulator::new(AudioSource::Samples {
725            data,
726            sample_rate: 16000,
727            loop_playback: false,
728        });
729
730        let samples = emulator.generate_n_samples(6);
731        assert_eq!(samples[0], 0.1);
732        assert_eq!(samples[2], 0.3);
733        assert!(samples[3].abs() < f32::EPSILON); // Silence after exhausted
734    }
735
736    #[test]
737    fn test_mock_js_generation() {
738        let emulator = AudioEmulator::new(AudioSource::Silence {
739            noise_floor_db: -60.0,
740        });
741        let samples = vec![0.1, 0.2, 0.3];
742        let js = emulator.generate_mock_js(&samples);
743
744        assert!(js.contains("mockSamples"));
745        assert!(js.contains("sampleRate"));
746        assert!(js.contains("getUserMedia"));
747        assert!(js.contains("__PROBAR_AUDIO_EMULATOR__"));
748    }
749
750    #[test]
751    fn test_default_config() {
752        let config = AudioEmulatorConfig::default();
753        assert_eq!(config.sample_rate, 16000);
754        assert_eq!(config.channels, 1);
755        assert_eq!(config.buffer_size, 1024);
756    }
757
758    /// Helper to calculate RMS amplitude
759    fn calculate_rms(samples: &[f32]) -> f32 {
760        if samples.is_empty() {
761            return 0.0;
762        }
763        let sum_squares: f32 = samples.iter().map(|&s| s * s).sum();
764        (sum_squares / samples.len() as f32).sqrt()
765    }
766
767    // ========================================================================
768    // Additional coverage tests for 95%+ target
769    // ========================================================================
770
771    #[test]
772    fn test_audio_source_default() {
773        // Coverage: AudioSource::default() implementation
774        let source = AudioSource::default();
775        match source {
776            AudioSource::Silence { noise_floor_db } => {
777                assert!((noise_floor_db - (-60.0)).abs() < f32::EPSILON);
778            }
779            _ => panic!("Default should be Silence variant"),
780        }
781    }
782
783    #[test]
784    fn test_audio_emulator_error_display_injection_failed() {
785        // Coverage: AudioEmulatorError::InjectionFailed Display
786        let error = AudioEmulatorError::InjectionFailed("test error".to_string());
787        let display = format!("{error}");
788        assert_eq!(display, "Audio injection failed: test error");
789    }
790
791    #[test]
792    fn test_audio_emulator_error_display_context_not_available() {
793        // Coverage: AudioEmulatorError::ContextNotAvailable Display
794        let error = AudioEmulatorError::ContextNotAvailable;
795        let display = format!("{error}");
796        assert_eq!(display, "Audio context not available");
797    }
798
799    #[test]
800    fn test_audio_emulator_error_display_invalid_config() {
801        // Coverage: AudioEmulatorError::InvalidConfig Display
802        let error = AudioEmulatorError::InvalidConfig("bad config".to_string());
803        let display = format!("{error}");
804        assert_eq!(display, "Invalid audio config: bad config");
805    }
806
807    #[test]
808    fn test_audio_emulator_error_is_error_trait() {
809        // Coverage: std::error::Error impl for AudioEmulatorError
810        let error: Box<dyn std::error::Error> = Box::new(AudioEmulatorError::ContextNotAvailable);
811        // Just verify it implements Error trait
812        assert!(error.to_string().contains("context"));
813    }
814
815    #[test]
816    fn test_sample_rate_accessor() {
817        // Coverage: AudioEmulator::sample_rate() method
818        let emulator = AudioEmulator::with_config(
819            AudioSource::Silence {
820                noise_floor_db: -60.0,
821            },
822            AudioEmulatorConfig {
823                sample_rate: 44100,
824                channels: 2,
825                buffer_size: 512,
826            },
827        );
828        assert_eq!(emulator.sample_rate(), 44100);
829    }
830
831    #[test]
832    fn test_sine_wave_phase_wrap() {
833        // Coverage: Phase wrap-around (phase >= 1.0 branch)
834        // Generate enough samples to guarantee multiple phase wraps
835        let mut emulator = AudioEmulator::with_config(
836            AudioSource::SineWave {
837                frequency: 1000.0, // High frequency for faster phase advancement
838                amplitude: 1.0,
839            },
840            AudioEmulatorConfig {
841                sample_rate: 8000, // Low sample rate = faster phase wrap
842                ..Default::default()
843            },
844        );
845        // 1000 Hz at 8000 Hz sample rate = phase advances 0.125 per sample
846        // After 8 samples, phase wraps (8 * 0.125 = 1.0)
847        let samples = emulator.generate_n_samples(100);
848        // Verify continuous output (no discontinuities from wrap)
849        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
850    }
851
852    #[test]
853    fn test_speech_pattern_harmonic_exceeds_nyquist() {
854        // Coverage: Harmonic frequency exceeding Nyquist (line 202-205)
855        let mut emulator = AudioEmulator::with_config(
856            AudioSource::SpeechPattern {
857                fundamental_hz: 3000.0,                        // High fundamental
858                harmonics: vec![0.5, 0.3, 0.2, 0.1, 0.1, 0.1], // Harmonics will exceed Nyquist
859                variation_hz: 0.0,
860            },
861            AudioEmulatorConfig {
862                sample_rate: 16000, // Nyquist = 8000 Hz
863                ..Default::default()
864            },
865        );
866        // 3000 Hz fundamental, harmonics at 6000, 9000, 12000... (last 3 exceed Nyquist)
867        let samples = emulator.generate_n_samples(1000);
868        // Should still produce valid samples
869        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
870    }
871
872    #[test]
873    fn test_speech_pattern_phase_wrap() {
874        // Coverage: Speech pattern phase wrap-around
875        let mut emulator = AudioEmulator::with_config(
876            AudioSource::SpeechPattern {
877                fundamental_hz: 2000.0,
878                harmonics: vec![0.3],
879                variation_hz: 10.0,
880            },
881            AudioEmulatorConfig {
882                sample_rate: 8000,
883                ..Default::default()
884            },
885        );
886        // Generate enough samples for multiple phase wraps
887        let samples = emulator.generate_n_samples(500);
888        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
889    }
890
891    #[test]
892    fn test_speech_pattern_variation_clamping() {
893        // Coverage: variation_hz clamping (line 189)
894        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
895            fundamental_hz: 100.0,
896            harmonics: vec![0.5],
897            variation_hz: 1000.0, // Much larger than freq/2, should be clamped to 50
898        });
899        let samples = emulator.generate_n_samples(1000);
900        // Should produce valid samples despite extreme variation
901        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
902    }
903
904    #[test]
905    fn test_speech_pattern_low_fundamental_clamped() {
906        // Coverage: fundamental_hz clamping to minimum 20.0
907        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
908            fundamental_hz: 5.0, // Below minimum, should be clamped to 20.0
909            harmonics: vec![0.5, 0.3],
910            variation_hz: 2.0,
911        });
912        let samples = emulator.generate_n_samples(1000);
913        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
914    }
915
916    #[test]
917    fn test_speech_pattern_high_fundamental_clamped() {
918        // Coverage: fundamental_hz clamping to Nyquist
919        let mut emulator = AudioEmulator::with_config(
920            AudioSource::SpeechPattern {
921                fundamental_hz: 20000.0, // Above Nyquist for 16kHz
922                harmonics: vec![0.5],
923                variation_hz: 10.0,
924            },
925            AudioEmulatorConfig {
926                sample_rate: 16000,
927                ..Default::default()
928            },
929        );
930        let samples = emulator.generate_n_samples(1000);
931        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
932    }
933
934    #[test]
935    fn test_speech_pattern_harmonic_amplitude_clamping() {
936        // Coverage: Harmonic amplitude clamping (line 204)
937        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
938            fundamental_hz: 150.0,
939            harmonics: vec![2.0, -0.5, 1.5], // Amplitudes outside [0, 1] range
940            variation_hz: 10.0,
941        });
942        let samples = emulator.generate_n_samples(1000);
943        // Harmonics should be clamped, output in valid range
944        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
945    }
946
947    #[test]
948    fn test_silence_noise_floor_clamping_high() {
949        // Coverage: noise_floor_db clamping to 0 (max)
950        let mut emulator = AudioEmulator::new(AudioSource::Silence {
951            noise_floor_db: 10.0, // Above 0, should be clamped
952        });
953        let samples = emulator.generate_n_samples(1000);
954        // At 0 dB, amplitude = 1.0, so noise could be in full [-1, 1] range
955        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
956    }
957
958    #[test]
959    fn test_silence_noise_floor_clamping_low() {
960        // Coverage: noise_floor_db clamping to -100 (min)
961        let mut emulator = AudioEmulator::new(AudioSource::Silence {
962            noise_floor_db: -200.0, // Below -100, should be clamped
963        });
964        let samples = emulator.generate_n_samples(1000);
965        let rms = calculate_rms(&samples);
966        // Should be extremely quiet
967        assert!(rms < 0.0001);
968    }
969
970    #[test]
971    fn test_samples_source_clamping_positive() {
972        // Coverage: Sample clamping for values > 1.0 (lines 245, 247)
973        let data = vec![1.5, 2.0, 0.5, -0.5];
974        let mut emulator = AudioEmulator::new(AudioSource::Samples {
975            data,
976            sample_rate: 16000,
977            loop_playback: false,
978        });
979        let samples = emulator.generate_n_samples(4);
980        assert_eq!(samples[0], 1.0); // Clamped from 1.5
981        assert_eq!(samples[1], 1.0); // Clamped from 2.0
982        assert_eq!(samples[2], 0.5); // Unchanged
983        assert_eq!(samples[3], -0.5); // Unchanged
984    }
985
986    #[test]
987    fn test_samples_source_clamping_negative() {
988        // Coverage: Sample clamping for values < -1.0
989        let data = vec![-1.5, -2.0, 0.5];
990        let mut emulator = AudioEmulator::new(AudioSource::Samples {
991            data,
992            sample_rate: 16000,
993            loop_playback: false,
994        });
995        let samples = emulator.generate_n_samples(3);
996        assert_eq!(samples[0], -1.0); // Clamped from -1.5
997        assert_eq!(samples[1], -1.0); // Clamped from -2.0
998        assert_eq!(samples[2], 0.5); // Unchanged
999    }
1000
1001    #[test]
1002    fn test_samples_source_loop_with_clamping() {
1003        // Coverage: Looped samples with clamping (line 247)
1004        let data = vec![1.5, -1.5]; // Both need clamping
1005        let mut emulator = AudioEmulator::new(AudioSource::Samples {
1006            data,
1007            sample_rate: 16000,
1008            loop_playback: true,
1009        });
1010        let samples = emulator.generate_n_samples(6);
1011        assert_eq!(samples[0], 1.0); // Clamped
1012        assert_eq!(samples[1], -1.0); // Clamped
1013        assert_eq!(samples[2], 1.0); // Looped and clamped
1014        assert_eq!(samples[3], -1.0); // Looped and clamped
1015    }
1016
1017    #[test]
1018    fn test_white_noise_zero_amplitude() {
1019        // Coverage: WhiteNoise with zero amplitude
1020        let mut emulator = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: 0.0 });
1021        let samples = emulator.generate_n_samples(1000);
1022        // All samples should be 0
1023        assert!(samples.iter().all(|&s| s.abs() < f32::EPSILON));
1024    }
1025
1026    #[test]
1027    fn test_white_noise_negative_amplitude_clamped() {
1028        // Coverage: WhiteNoise with negative amplitude (clamped to 0)
1029        let mut emulator = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: -0.5 });
1030        let samples = emulator.generate_n_samples(1000);
1031        // All samples should be 0 (amplitude clamped to 0)
1032        assert!(samples.iter().all(|&s| s.abs() < f32::EPSILON));
1033    }
1034
1035    #[test]
1036    fn test_white_noise_high_amplitude_clamped() {
1037        // Coverage: WhiteNoise with amplitude > 1.0 (clamped to 1.0)
1038        let mut emulator = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: 5.0 });
1039        let samples = emulator.generate_n_samples(1000);
1040        // All samples should be in valid range
1041        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
1042    }
1043
1044    #[test]
1045    fn test_rng_determinism_after_reset() {
1046        // Coverage: RNG state reset
1047        let mut emulator = AudioEmulator::new(AudioSource::WhiteNoise { amplitude: 1.0 });
1048        let samples1 = emulator.generate_n_samples(100);
1049        emulator.reset();
1050        let samples2 = emulator.generate_n_samples(100);
1051        // After reset, noise should be identical
1052        assert_eq!(samples1, samples2);
1053    }
1054
1055    #[test]
1056    fn test_generate_mock_js_with_many_samples() {
1057        // Coverage: generate_mock_js with larger sample set
1058        let emulator = AudioEmulator::new(AudioSource::Silence {
1059            noise_floor_db: -60.0,
1060        });
1061        let samples: Vec<f32> = (0..100).map(|i| (i as f32) * 0.01).collect();
1062        let js = emulator.generate_mock_js(&samples);
1063
1064        // Verify sample count in output
1065        assert!(js.contains("0.990000")); // Last sample value
1066        assert!(js.contains("Float32Array"));
1067    }
1068
1069    #[test]
1070    fn test_config_custom_channels_and_buffer() {
1071        // Coverage: Custom AudioEmulatorConfig values
1072        let config = AudioEmulatorConfig {
1073            sample_rate: 48000,
1074            channels: 2,
1075            buffer_size: 2048,
1076        };
1077        assert_eq!(config.sample_rate, 48000);
1078        assert_eq!(config.channels, 2);
1079        assert_eq!(config.buffer_size, 2048);
1080    }
1081
1082    #[test]
1083    fn test_audio_source_clone() {
1084        // Coverage: AudioSource Clone implementation
1085        let source = AudioSource::SpeechPattern {
1086            fundamental_hz: 150.0,
1087            harmonics: vec![0.5, 0.3],
1088            variation_hz: 20.0,
1089        };
1090        let cloned = source;
1091        match cloned {
1092            AudioSource::SpeechPattern {
1093                fundamental_hz,
1094                harmonics,
1095                variation_hz,
1096            } => {
1097                assert!((fundamental_hz - 150.0).abs() < f32::EPSILON);
1098                assert_eq!(harmonics, vec![0.5, 0.3]);
1099                assert!((variation_hz - 20.0).abs() < f32::EPSILON);
1100            }
1101            _ => panic!("Clone should preserve variant"),
1102        }
1103    }
1104
1105    #[test]
1106    fn test_audio_emulator_config_clone() {
1107        // Coverage: AudioEmulatorConfig Clone implementation
1108        let config = AudioEmulatorConfig {
1109            sample_rate: 22050,
1110            channels: 2,
1111            buffer_size: 512,
1112        };
1113        let cloned = config;
1114        assert_eq!(cloned.sample_rate, 22050);
1115        assert_eq!(cloned.channels, 2);
1116        assert_eq!(cloned.buffer_size, 512);
1117    }
1118
1119    #[test]
1120    fn test_audio_emulator_clone() {
1121        // Coverage: AudioEmulator Clone implementation
1122        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
1123            frequency: 440.0,
1124            amplitude: 0.8,
1125        });
1126        let _ = emulator.generate_n_samples(100); // Advance state
1127
1128        let cloned = emulator.clone();
1129        assert_eq!(cloned.samples_generated(), 100);
1130        assert_eq!(cloned.sample_rate(), 16000);
1131    }
1132
1133    #[test]
1134    fn test_audio_emulator_error_clone() {
1135        // Coverage: AudioEmulatorError Clone implementation
1136        let error = AudioEmulatorError::InjectionFailed("test".to_string());
1137        let cloned = error;
1138        match cloned {
1139            AudioEmulatorError::InjectionFailed(msg) => assert_eq!(msg, "test"),
1140            _ => panic!("Clone should preserve variant"),
1141        }
1142    }
1143
1144    #[test]
1145    fn test_audio_source_debug() {
1146        // Coverage: AudioSource Debug implementation
1147        let source = AudioSource::SineWave {
1148            frequency: 440.0,
1149            amplitude: 1.0,
1150        };
1151        let debug_str = format!("{source:?}");
1152        assert!(debug_str.contains("SineWave"));
1153        assert!(debug_str.contains("440"));
1154    }
1155
1156    #[test]
1157    fn test_audio_emulator_config_debug() {
1158        // Coverage: AudioEmulatorConfig Debug implementation
1159        let config = AudioEmulatorConfig::default();
1160        let debug_str = format!("{config:?}");
1161        assert!(debug_str.contains("16000"));
1162    }
1163
1164    #[test]
1165    fn test_audio_emulator_debug() {
1166        // Coverage: AudioEmulator Debug implementation
1167        let emulator = AudioEmulator::new(AudioSource::default());
1168        let debug_str = format!("{emulator:?}");
1169        assert!(debug_str.contains("AudioEmulator"));
1170    }
1171
1172    #[test]
1173    fn test_audio_emulator_error_debug() {
1174        // Coverage: AudioEmulatorError Debug implementation
1175        let error = AudioEmulatorError::ContextNotAvailable;
1176        let debug_str = format!("{error:?}");
1177        assert!(debug_str.contains("ContextNotAvailable"));
1178    }
1179
1180    #[test]
1181    fn test_speech_pattern_normalization() {
1182        // Coverage: Speech pattern normalization (line 209-210)
1183        // Use harmonics that sum to > 1.0 to test normalization
1184        let mut emulator = AudioEmulator::new(AudioSource::SpeechPattern {
1185            fundamental_hz: 150.0,
1186            harmonics: vec![0.8, 0.8, 0.8, 0.8], // Sum = 3.2, total_amp = 4.2
1187            variation_hz: 0.0,
1188        });
1189        let samples = emulator.generate_n_samples(1000);
1190        // Normalization should keep all samples in range
1191        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
1192    }
1193
1194    #[test]
1195    fn test_sine_wave_very_low_frequency() {
1196        // Coverage: Very low frequency sine wave (near minimum clamp)
1197        let mut emulator = AudioEmulator::new(AudioSource::SineWave {
1198            frequency: 0.0001, // Very close to minimum
1199            amplitude: 1.0,
1200        });
1201        let samples = emulator.generate_n_samples(100);
1202        // At such low frequency, output should be near-constant
1203        assert!(samples.iter().all(|&s| (-1.0..=1.0).contains(&s)));
1204    }
1205
1206    #[test]
1207    fn test_generate_samples_fractional_duration() {
1208        // Coverage: generate_samples with fractional sample count
1209        let mut emulator = AudioEmulator::with_config(
1210            AudioSource::Silence {
1211                noise_floor_db: -60.0,
1212            },
1213            AudioEmulatorConfig {
1214                sample_rate: 1000, // 1kHz for easy math
1215                ..Default::default()
1216            },
1217        );
1218        // 0.0015 seconds * 1000 Hz = 1.5 samples, truncated to 1
1219        let samples = emulator.generate_samples(0.0015);
1220        assert_eq!(samples.len(), 1);
1221    }
1222}