diff options
Diffstat (limited to 'crates/asist-audio')
| -rw-r--r-- | crates/asist-audio/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/asist-audio/src/playback.rs | 81 |
2 files changed, 82 insertions, 1 deletions
diff --git a/crates/asist-audio/src/lib.rs b/crates/asist-audio/src/lib.rs index f6979ca..de3f002 100644 --- a/crates/asist-audio/src/lib.rs +++ b/crates/asist-audio/src/lib.rs @@ -15,7 +15,7 @@ pub mod vad; pub use cpal; pub use capture::{Capture, CaptureBlock, InputFormat}; -pub use playback::{Playback, PlaybackHandle}; +pub use playback::{detached_handle, Playback, PlaybackHandle}; pub use vad::{Gate, Segmenter, Utterance, VoiceEvent}; /// Frecuencia a la que trabaja Canary. La captura se abre directamente aquí diff --git a/crates/asist-audio/src/playback.rs b/crates/asist-audio/src/playback.rs index fcdfe7d..903519f 100644 --- a/crates/asist-audio/src/playback.rs +++ b/crates/asist-audio/src/playback.rs @@ -78,6 +78,20 @@ impl PlaybackHandle { self.push(&to_mono_at(samples, format, self.sample_rate)); } + /// Marca que se ha terminado de alimentar audio y ya ha sonado todo. + /// + /// Hace falta un método aparte de `stop`: éste no tira nada, sólo apaga la + /// bandera de «hay audio en marcha». Sin él la bandera se quedaba puesta + /// tras la primera respuesta, el segmentador mantenía el micrófono cerrado + /// creyendo que el asistente seguía hablando, y el asistente no volvía a + /// oír nada en toda la sesión. + pub fn mark_idle(&self) { + let queue = self.ring.lock(); + if queue.is_empty() { + self.ring.active.store(false, Ordering::SeqCst); + } + } + /// Calla ahora mismo y tira lo que quedaba por sonar. pub fn stop(&self) { let mut queue = self.ring.lock(); @@ -135,6 +149,15 @@ impl PlaybackHandle { } } +/// Un mando sin dispositivo detrás, para probar la lógica del anillo sin +/// abrir una tarjeta de sonido. +pub fn detached_handle(sample_rate: u32) -> PlaybackHandle { + PlaybackHandle { + ring: Arc::new(Ring::new(1.0)), + sample_rate, + } +} + pub struct Playback { stream: cpal::Stream, handle: PlaybackHandle, @@ -285,3 +308,61 @@ fn preferred_config(device: &cpal::Device) -> Result<cpal::SupportedStreamConfig .map_err(|e| Error::Audio(format!("sin configuración de salida: {e}"))), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn el_anillo_deja_de_estar_activo_cuando_se_vacia() { + // La regresión que dejaba mudo al asistente tras la primera respuesta: + // sin marcar el fin, el segmentador creía que seguía hablando y no + // volvía a abrir el micrófono nunca. + let handle = detached_handle(24_000); + assert!(!handle.is_active(), "recién creado no hay nada sonando"); + + handle.push(&[0.1; 480]); + assert!(handle.is_active()); + + handle.mark_idle(); + assert!( + handle.is_active(), + "con audio pendiente todavía está sonando, no debe apagarse" + ); + + handle.stop(); + handle.push(&[0.1; 240]); + handle.stop(); + handle.mark_idle(); + assert!( + !handle.is_active(), + "con la cola vacía debe quedar inactivo" + ); + } + + #[test] + fn la_cola_se_mide_en_segundos_de_audio() { + let handle = detached_handle(24_000); + handle.push(&[0.0; 12_000]); + assert!((handle.queued_secs() - 0.5).abs() < 1e-6); + handle.stop(); + assert_eq!(handle.queued_secs(), 0.0); + } + + #[test] + fn cortar_vacia_la_cola_al_instante() { + let handle = detached_handle(24_000); + handle.push(&[0.5; 48_000]); + handle.stop(); + assert_eq!(handle.queued_secs(), 0.0, "cortar debe tirar lo pendiente"); + assert!(!handle.is_active()); + } + + #[test] + fn el_audio_del_tts_se_remuestrea_al_dispositivo() { + // Medio segundo a 24 kHz debe seguir durando medio segundo a 48 kHz. + let handle = detached_handle(48_000); + handle.push_tts(&[0.0; 12_000]); + assert!((handle.queued_secs() - 0.5).abs() < 0.01); + } +} |