aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-audio/src/playback.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/asist-audio/src/playback.rs')
-rw-r--r--crates/asist-audio/src/playback.rs81
1 files changed, 81 insertions, 0 deletions
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);
+ }
+}