aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-audio/src/lib.rs
blob: de3f002eee9d0f1247b0c206eb9f0c34b5d1670a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Entrada y salida de audio, y el detector de voz que las separa en turnos.
//!
//! La regla que gobierna este crate: **la retrollamada de audio no bloquea
//! nunca**. cpal la ejecuta en un hilo de tiempo real y cualquier espera ahí
//! se oye como un chasquido, así que se limita a copiar muestras a un canal
//! (entrada) o a vaciar un anillo ya rellenado (salida). Todo el trabajo real
//! —remuestreo, VAD, HTTP— ocurre en hilos normales al otro lado.

pub mod capture;
pub mod playback;
pub mod vad;

/// Se reexporta para que el binario pueda listar dispositivos sin volver a
/// declarar cpal y arriesgarse a resolver otra versión.
pub use cpal;

pub use capture::{Capture, CaptureBlock, InputFormat};
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í
/// cuando el dispositivo lo permite, lo que quita el remuestreo del camino.
pub const ASR_SAMPLE_RATE: u32 = 16_000;

/// Frecuencia a la que sintetiza qwentts.
pub const TTS_SAMPLE_RATE: u32 = 24_000;

/// Nombre legible de un dispositivo.
///
/// `DeviceTrait::name` está obsoleto en cpal 0.17 a favor de `description`,
/// que devuelve una ficha entera; aquí sólo interesa el nombre, y tener un
/// único sitio donde extraerlo evita repetir el desempaquetado.
pub fn describe(device: &impl cpal::traits::DeviceTrait) -> String {
    device
        .description()
        .map(|d| d.name().to_string())
        .unwrap_or_else(|_| "desconocido".into())
}

/// Nivel RMS de un bloque, la medida con la que el VAD decide.
pub fn rms(samples: &[f32]) -> f32 {
    if samples.is_empty() {
        return 0.0;
    }
    let sum: f32 = samples.iter().map(|v| v * v).sum();
    (sum / samples.len() as f32).sqrt()
}

/// Mezcla a mono y remuestrea linealmente a `target`.
///
/// La interpolación lineal basta: el dispositivo ya entrega la señal limitada
/// en banda, y un remuestreador decente costaría más que la decodificación a
/// la que alimenta.
pub fn to_mono_at(samples: &[f32], format: InputFormat, target: u32) -> Vec<f32> {
    let mono: Vec<f32> = if format.channels > 1 {
        samples
            .chunks(format.channels)
            .map(|frame| frame.iter().sum::<f32>() / format.channels as f32)
            .collect()
    } else {
        samples.to_vec()
    };

    if format.sample_rate == target as usize {
        return mono;
    }
    let ratio = target as f64 / format.sample_rate as f64;
    let out_len = (mono.len() as f64 * ratio) as usize;
    (0..out_len)
        .map(|i| {
            let pos = i as f64 / ratio;
            let idx = pos as usize;
            let frac = (pos - idx as f64) as f32;
            let a = mono.get(idx).copied().unwrap_or(0.0);
            let b = mono.get(idx + 1).copied().unwrap_or(a);
            a + (b - a) * frac
        })
        .collect()
}

/// Convierte s16le a f32 en [-1, 1]. Es el formato en el que el TTS entrega.
pub fn s16le_to_f32(bytes: &[u8], out: &mut Vec<f32>) {
    for pair in bytes.chunks_exact(2) {
        let sample = i16::from_le_bytes([pair[0], pair[1]]);
        out.push(sample as f32 / 32768.0);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn el_rms_de_una_senal_constante_es_su_amplitud() {
        assert!((rms(&[0.5; 100]) - 0.5).abs() < 1e-6);
        assert_eq!(rms(&[]), 0.0);
    }

    #[test]
    fn el_estereo_se_mezcla_a_mono_promediando() {
        let format = InputFormat {
            sample_rate: 16_000,
            channels: 2,
        };
        let out = to_mono_at(&[1.0, 0.0, 0.5, 0.5], format, 16_000);
        assert_eq!(out, vec![0.5, 0.5]);
    }

    #[test]
    fn el_remuestreo_ajusta_la_duracion() {
        let format = InputFormat {
            sample_rate: 48_000,
            channels: 1,
        };
        let out = to_mono_at(&vec![0.0; 4800], format, 16_000);
        assert_eq!(out.len(), 1600, "48 kHz -> 16 kHz debe dividir por tres");
    }

    #[test]
    fn a_la_misma_frecuencia_el_remuestreo_no_toca_nada() {
        let format = InputFormat {
            sample_rate: 16_000,
            channels: 1,
        };
        let input = vec![0.1, -0.2, 0.3];
        assert_eq!(to_mono_at(&input, format, 16_000), input);
    }

    #[test]
    fn s16le_recorre_el_rango_completo() {
        let mut out = Vec::new();
        s16le_to_f32(&[0x00, 0x00, 0xff, 0x7f, 0x00, 0x80], &mut out);
        assert_eq!(out[0], 0.0);
        assert!((out[1] - 1.0).abs() < 1e-4);
        assert!((out[2] + 1.0).abs() < 1e-6);
    }

    #[test]
    fn un_byte_suelto_no_produce_una_muestra_a_medias() {
        let mut out = Vec::new();
        s16le_to_f32(&[0x00, 0x00, 0x11], &mut out);
        assert_eq!(
            out.len(),
            1,
            "el byte impar se ignora en vez de corromper la muestra"
        );
    }
}