aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-audio/src/lib.rs
blob: a339d167efea3c0b1d1c2903c3252a83dd9d164d (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
//! Audio input and output, and the voice detector that splits them into turns.
//!
//! The rule that governs this crate: **the audio callback never blocks**. cpal
//! runs it on a real-time thread and any wait there is heard as a click, so it
//! only copies samples to a channel (input) or drains an already filled ring
//! (output). All the real work (resampling, VAD, HTTP) happens on regular
//! threads on the other side.

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

/// Re-exported so the binary can list devices without declaring cpal
/// again and risking resolving another version.
pub use cpal;

pub use capture::{Capture, CaptureBlock, InputFormat};
pub use playback::{detached_handle, Playback, PlaybackHandle};
pub use vad::{Gate, Segmenter, Utterance, VoiceEvent};

/// Rate Canary works at. Capture is opened directly at this rate when the
/// device allows it, which takes resampling off the path.
pub const ASR_SAMPLE_RATE: u32 = 16_000;

/// Rate qwentts synthesizes at.
pub const TTS_SAMPLE_RATE: u32 = 24_000;

/// Human-readable device name.
///
/// `DeviceTrait::name` is deprecated in cpal 0.17 in favour of `description`,
/// which returns a whole record; only the name matters here, and having a
/// single place to extract it avoids repeating the unwrapping.
pub fn describe(device: &impl cpal::traits::DeviceTrait) -> String {
    device
        .description()
        .map(|d| d.name().to_string())
        .unwrap_or_else(|_| "desconocido".into())
}

/// RMS level of a block, the measure the VAD decides on.
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()
}

/// Mixes down to mono and resamples linearly to `target`.
///
/// Linear interpolation is enough: the device already delivers a
/// band-limited signal, and a decent resampler would cost more than the
/// decoding it feeds.
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()
}

/// Converts s16le to f32 in [-1, 1]. It is the format the TTS delivers.
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 rms_of_a_constant_signal_is_its_amplitude() {
        assert!((rms(&[0.5; 100]) - 0.5).abs() < 1e-6);
        assert_eq!(rms(&[]), 0.0);
    }

    #[test]
    fn stereo_is_mixed_to_mono_by_averaging() {
        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 resampling_adjusts_the_duration() {
        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 must divide by three");
    }

    #[test]
    fn resampling_at_the_same_rate_changes_nothing() {
        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_covers_the_full_range() {
        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 stray_byte_does_not_produce_a_partial_sample() {
        let mut out = Vec::new();
        s16le_to_f32(&[0x00, 0x00, 0x11], &mut out);
        assert_eq!(
            out.len(),
            1,
            "the odd byte is ignored instead of corrupting the sample"
        );
    }
}