gemini_adk_fluent_rs/telephony/
dtmf.rs1const ROWS: [f64; 4] = [697.0, 770.0, 852.0, 941.0];
17const COLS: [f64; 4] = [1209.0, 1336.0, 1477.0, 1633.0];
18const KEYS: [[char; 4]; 4] = [
19 ['1', '2', '3', 'A'],
20 ['4', '5', '6', 'B'],
21 ['7', '8', '9', 'C'],
22 ['*', '0', '#', 'D'],
23];
24const BLOCK_SECS: f64 = 0.025_625;
26const EIGHT_DB: f64 = 6.31;
29const PURITY: f64 = 0.6;
31const MIN_MEAN_SQUARE: f64 = 500.0 * 500.0;
33
34#[derive(Debug, Clone)]
36pub struct DtmfDetector {
37 block: usize,
38 coeffs: [f64; 8],
39 buffer: Vec<i16>,
40 last: Option<char>,
42}
43
44impl DtmfDetector {
45 pub fn new(sample_rate: u32) -> Self {
48 let rate = f64::from(sample_rate.max(4_000));
49 let block = (rate * BLOCK_SECS).round() as usize;
50 let mut coeffs = [0.0; 8];
51 for (i, f) in ROWS.iter().chain(COLS.iter()).enumerate() {
52 let k = (block as f64 * f / rate).round();
53 coeffs[i] = 2.0 * (2.0 * std::f64::consts::PI * k / block as f64).cos();
54 }
55 Self {
56 block,
57 coeffs,
58 buffer: Vec::with_capacity(block),
59 last: None,
60 }
61 }
62
63 pub fn feed(&mut self, samples: &[i16]) -> Vec<char> {
66 let mut digits = Vec::new();
67 for &sample in samples {
68 self.buffer.push(sample);
69 if self.buffer.len() == self.block {
70 let digit = self.classify();
71 if digit.is_some() && digit != self.last {
72 digits.extend(digit);
73 }
74 self.last = digit;
75 self.buffer.clear();
76 }
77 }
78 digits
79 }
80
81 pub fn tone_present(&self) -> bool {
83 self.last.is_some()
84 }
85
86 fn classify(&self) -> Option<char> {
87 let n = self.buffer.len() as f64;
88 let energy: f64 = self.buffer.iter().map(|&s| f64::from(s).powi(2)).sum();
89 if energy / n < MIN_MEAN_SQUARE {
90 return None;
91 }
92 let mut power = [0f64; 8];
93 for (i, &coeff) in self.coeffs.iter().enumerate() {
94 let (mut s1, mut s2) = (0f64, 0f64);
95 for &x in &self.buffer {
96 let s0 = f64::from(x) + coeff * s1 - s2;
97 s2 = s1;
98 s1 = s0;
99 }
100 power[i] = s1 * s1 + s2 * s2 - coeff * s1 * s2;
101 }
102 let strongest = |group: &[f64]| {
103 let (best, &p) = group
104 .iter()
105 .enumerate()
106 .max_by(|a, b| a.1.total_cmp(b.1))
107 .expect("four tones");
108 let runner_up = group
109 .iter()
110 .enumerate()
111 .filter(|(i, _)| *i != best)
112 .map(|(_, &q)| q)
113 .fold(0.0, f64::max);
114 (best, p, runner_up)
115 };
116 let (row, row_power, row_next) = strongest(&power[..4]);
117 let (col, col_power, col_next) = strongest(&power[4..]);
118 let dominant = row_power > row_next * EIGHT_DB && col_power > col_next * EIGHT_DB;
119 let twist_ok = row_power < col_power * EIGHT_DB && col_power < row_power * EIGHT_DB;
120 let tone_energy = (row_power + col_power) * 2.0 / n;
123 let pure = tone_energy > PURITY * energy;
124 (dominant && twist_ok && pure).then_some(KEYS[row][col])
125 }
126}
127
128#[cfg(test)]
129pub(crate) mod tests {
130 use super::*;
131
132 pub(crate) fn keypresses(digits: &str, on_ms: usize, off_ms: usize, level: f64) -> Vec<i16> {
134 let rate = 8_000.0;
135 let mut out = Vec::new();
136 for digit in digits.chars() {
137 let (r, c) = KEYS
138 .iter()
139 .enumerate()
140 .find_map(|(r, row)| row.iter().position(|&k| k == digit).map(|c| (r, c)))
141 .expect("a keypad digit");
142 for n in 0..on_ms * 8 {
143 let t = n as f64 / rate;
144 let v = level
145 * ((2.0 * std::f64::consts::PI * ROWS[r] * t).sin()
146 + (2.0 * std::f64::consts::PI * COLS[c] * t).sin());
147 out.push(v as i16);
148 }
149 out.extend(std::iter::repeat_n(0, off_ms * 8));
150 }
151 out
152 }
153
154 #[test]
155 fn keypresses_are_detected_once_each() {
156 let audio = keypresses("4129#*0D", 70, 50, 6_000.0);
157 let mut detector = DtmfDetector::new(8_000);
158 let mut found = String::new();
159 for frame in audio.chunks(160) {
161 found.extend(detector.feed(frame));
162 }
163 assert_eq!(found, "4129#*0D");
164 }
165
166 #[test]
167 fn a_held_key_is_one_press_and_repeats_need_a_gap() {
168 let mut detector = DtmfDetector::new(8_000);
169 let held = keypresses("5", 400, 60, 6_000.0);
170 assert_eq!(detector.feed(&held), ['5']);
171 let again = keypresses("55", 60, 60, 6_000.0);
172 assert_eq!(detector.feed(&again), ['5', '5']);
173 }
174
175 #[test]
176 fn speech_like_audio_is_not_a_digit() {
177 let mut seed = 7u32;
180 let audio: Vec<i16> = (0..8_000 * 2)
181 .map(|n| {
182 let t = n as f64 / 8_000.0;
183 let voiced: f64 = (1..=25)
184 .map(|h| {
185 (4_000.0 / h as f64)
186 * (2.0 * std::f64::consts::PI * 140.0 * h as f64 * t).sin()
187 })
188 .sum();
189 seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
190 let noise = f64::from((seed >> 16) as i16 >> 4);
191 (voiced + noise) as i16
192 })
193 .collect();
194 let mut detector = DtmfDetector::new(8_000);
195 assert!(detector.feed(&audio).is_empty());
196 }
197
198 #[test]
199 fn quiet_or_single_tones_are_ignored() {
200 let mut detector = DtmfDetector::new(8_000);
201 assert!(detector.feed(&keypresses("7", 100, 0, 200.0)).is_empty());
202 let beep: Vec<i16> = (0..1_600)
204 .map(|n| {
205 (8_000.0 * (2.0 * std::f64::consts::PI * 1_000.0 * n as f64 / 8_000.0).sin()) as i16
206 })
207 .collect();
208 assert!(detector.feed(&beep).is_empty());
209 }
210}