dtmf_table/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2//! # DTMF Table
3//!
4//! A zero-heap, `no_std`, const-first implementation of the standard DTMF keypad
5//! frequencies with ergonomic runtime helpers for real-world audio decoding.
6//!
7//! ## Features
8//! - Type-safe closed enum for DTMF keys — invalid keys are unrepresentable.
9//! - Fully `const` forward and reverse mappings (key ↔ frequencies).
10//! - Runtime helpers for tolerance-based reverse lookup and nearest snapping.
11//! - No heap, no allocations, no dependencies.
12//!
13//! ## Example
14//!
15//! ```rust
16//! use dtmf_tones::{DtmfTable, DtmfKey};
17//!
18//! // Construct a zero-sized table instance
19//! let table = DtmfTable::new();
20//!
21//! // Forward lookup from key to canonical frequencies
22//! let (low, high) = DtmfTable::lookup_key(DtmfKey::K8);
23//! assert_eq!((low, high), (852, 1336));
24//!
25//! // Reverse lookup with tolerance (e.g. from FFT bin centres)
26//! let key = table.from_pair_tol_f64(770.2, 1335.6, 6.0).unwrap();
27//! assert_eq!(key.to_char(), '5');
28//!
29//! // Nearest snapping for noisy estimates
30//! let (k, snapped_low, snapped_high) = table.nearest_u32(768, 1342);
31//! assert_eq!(k.to_char(), '5');
32//! assert_eq!((snapped_low, snapped_high), (770, 1336));
33//! ```
34//!
35//! This makes it easy to integrate DTMF tone detection directly into audio
36//! processing pipelines (e.g., FFT bin peak picking) with robust tolerance handling
37//! and compile-time validation of key mappings.
38
39
40use core::cmp::Ordering;
41
42/// Type-safe, closed set of DTMF keys.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum DtmfKey {
45    K1, K2, K3, A,
46    K4, K5, K6, B,
47    K7, K8, K9, C,
48    Star, K0, Hash, D,
49}
50
51impl DtmfKey {
52    /// Strict constructor from `char` (const).
53    pub const fn from_char(c: char) -> Option<Self> {
54        match c {
55            '1' => Some(Self::K1),
56            '2' => Some(Self::K2),
57            '3' => Some(Self::K3),
58            'A' => Some(Self::A),
59            '4' => Some(Self::K4),
60            '5' => Some(Self::K5),
61            '6' => Some(Self::K6),
62            'B' => Some(Self::B),
63            '7' => Some(Self::K7),
64            '8' => Some(Self::K8),
65            '9' => Some(Self::K9),
66            'C' => Some(Self::C),
67            '*' => Some(Self::Star),
68            '0' => Some(Self::K0),
69            '#' => Some(Self::Hash),
70            'D' => Some(Self::D),
71            _ => None,
72        }
73    }
74
75    /// Panic-on-invalid (const), useful with char literals at compile time.
76    pub const fn from_char_or_panic(c: char) -> Self {
77        match Self::from_char(c) {
78            Some(k) => k,
79            None => panic!("invalid DTMF char"),
80        }
81    }
82
83    /// Back to char (const).
84    pub const fn to_char(self) -> char {
85        match self {
86            Self::K1 => '1', Self::K2 => '2', Self::K3 => '3', Self::A => 'A',
87            Self::K4 => '4', Self::K5 => '5', Self::K6 => '6', Self::B => 'B',
88            Self::K7 => '7', Self::K8 => '8', Self::K9 => '9', Self::C => 'C',
89            Self::Star => '*', Self::K0 => '0', Self::Hash => '#', Self::D => 'D',
90        }
91    }
92
93    /// Canonical (low, high) frequencies in Hz (const).
94    pub const fn freqs(self) -> (u16, u16) {
95        match self {
96            Self::K1 => (697, 1209),
97            Self::K2 => (697, 1336),
98            Self::K3 => (697, 1477),
99            Self::A  => (697, 1633),
100
101            Self::K4 => (770, 1209),
102            Self::K5 => (770, 1336),
103            Self::K6 => (770, 1477),
104            Self::B  => (770, 1633),
105
106            Self::K7 => (852, 1209),
107            Self::K8 => (852, 1336),
108            Self::K9 => (852, 1477),
109            Self::C  => (852, 1633),
110
111            Self::Star => (941, 1209),
112            Self::K0   => (941, 1336),
113            Self::Hash => (941, 1477),
114            Self::D    => (941, 1633),
115        }
116    }
117}
118
119/// Tone record (ties the key to its canonical freqs).
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct DtmfTone {
122    pub key: DtmfKey,
123    pub low_hz: u16,
124    pub high_hz: u16,
125}
126
127/// Zero-sized table wrapper for const and runtime utilities.
128pub struct DtmfTable;
129
130impl DtmfTable {
131    /// Canonical low-/high-band frequencies (Hz).
132    pub const LOWS: [u16; 4]  = [697, 770, 852, 941];
133    pub const HIGHS: [u16; 4] = [1209, 1336, 1477, 1633];
134
135    /// All keys in keypad order (row-major).
136    pub const ALL_KEYS: [DtmfKey; 16] = [
137        DtmfKey::K1, DtmfKey::K2, DtmfKey::K3, DtmfKey::A,
138        DtmfKey::K4, DtmfKey::K5, DtmfKey::K6, DtmfKey::B,
139        DtmfKey::K7, DtmfKey::K8, DtmfKey::K9, DtmfKey::C,
140        DtmfKey::Star, DtmfKey::K0, DtmfKey::Hash, DtmfKey::D,
141    ];
142
143    /// All tones as (key, low, high). Kept explicit to stay `const`.
144    pub const ALL_TONES: [DtmfTone; 16] = [
145        DtmfTone { key: DtmfKey::K1,   low_hz: 697, high_hz: 1209 },
146        DtmfTone { key: DtmfKey::K2,   low_hz: 697, high_hz: 1336 },
147        DtmfTone { key: DtmfKey::K3,   low_hz: 697, high_hz: 1477 },
148        DtmfTone { key: DtmfKey::A,    low_hz: 697, high_hz: 1633 },
149
150        DtmfTone { key: DtmfKey::K4,   low_hz: 770, high_hz: 1209 },
151        DtmfTone { key: DtmfKey::K5,   low_hz: 770, high_hz: 1336 },
152        DtmfTone { key: DtmfKey::K6,   low_hz: 770, high_hz: 1477 },
153        DtmfTone { key: DtmfKey::B,    low_hz: 770, high_hz: 1633 },
154
155        DtmfTone { key: DtmfKey::K7,   low_hz: 852, high_hz: 1209 },
156        DtmfTone { key: DtmfKey::K8,   low_hz: 852, high_hz: 1336 },
157        DtmfTone { key: DtmfKey::K9,   low_hz: 852, high_hz: 1477 },
158        DtmfTone { key: DtmfKey::C,    low_hz: 852, high_hz: 1633 },
159
160        DtmfTone { key: DtmfKey::Star, low_hz: 941, high_hz: 1209 },
161        DtmfTone { key: DtmfKey::K0,   low_hz: 941, high_hz: 1336 },
162        DtmfTone { key: DtmfKey::Hash, low_hz: 941, high_hz: 1477 },
163        DtmfTone { key: DtmfKey::D,    low_hz: 941, high_hz: 1633 },
164    ];
165
166    /// Constructor (zero-sized instance).
167    pub const fn new() -> Self { DtmfTable }
168
169    /* ---------------------- Const utilities ---------------------- */
170
171    /// Forward: key → (low, high) (const).
172    pub const fn lookup_key(key: DtmfKey) -> (u16, u16) { key.freqs() }
173
174    /// Reverse: exact (low, high) → key (const). Order-sensitive.
175    pub const fn from_pair_exact(low: u16, high: u16) -> Option<DtmfKey> {
176        match (low, high) {
177            (697, 1209) => Some(DtmfKey::K1),
178            (697, 1336) => Some(DtmfKey::K2),
179            (697, 1477) => Some(DtmfKey::K3),
180            (697, 1633) => Some(DtmfKey::A),
181            (770, 1209) => Some(DtmfKey::K4),
182            (770, 1336) => Some(DtmfKey::K5),
183            (770, 1477) => Some(DtmfKey::K6),
184            (770, 1633) => Some(DtmfKey::B),
185            (852, 1209) => Some(DtmfKey::K7),
186            (852, 1336) => Some(DtmfKey::K8),
187            (852, 1477) => Some(DtmfKey::K9),
188            (852, 1633) => Some(DtmfKey::C),
189            (941, 1209) => Some(DtmfKey::Star),
190            (941, 1336) => Some(DtmfKey::K0),
191            (941, 1477) => Some(DtmfKey::Hash),
192            (941, 1633) => Some(DtmfKey::D),
193            _ => None,
194        }
195    }
196
197    /// Reverse with normalisation (const): accepts (high, low) as well.
198    pub const fn from_pair_normalised(a: u16, b: u16) -> Option<DtmfKey> {
199        let (low, high) = if a <= b { (a, b) } else { (b, a) };
200        Self::from_pair_exact(low, high)
201    }
202
203    /* ---------------------- Runtime helpers ---------------------- */
204
205    /// Iterate keys in keypad order (no allocation).
206    pub fn iter_keys(&self) -> core::slice::Iter<'static, DtmfKey> {
207        Self::ALL_KEYS.iter()
208    }
209
210    /// Iterate tones (key + freqs) in keypad order (no allocation).
211    pub fn iter_tones(&self) -> core::slice::Iter<'static, DtmfTone> {
212        Self::ALL_TONES.iter()
213    }
214
215    /// Reverse lookup with tolerance in Hz (integer inputs).
216    /// Matches only when *both* low and high fall within `±tol_hz` of a canonical pair.
217    pub fn from_pair_tol_u32(&self, low: u32, high: u32, tol_hz: u32) -> Option<DtmfKey> {
218        let (lo, hi) = normalise_u32_pair(low, high);
219        for t in Self::ALL_TONES {
220            if abs_diff_u32(lo, t.low_hz as u32) <= tol_hz &&
221               abs_diff_u32(hi, t.high_hz as u32) <= tol_hz {
222                return Some(t.key);
223            }
224        }
225        None
226    }
227
228    /// Reverse lookup with tolerance for floating-point estimates (e.g., FFT bin centres).
229    pub fn from_pair_tol_f64(&self, low: f64, high: f64, tol_hz: f64) -> Option<DtmfKey> {
230        let (lo, hi) = normalise_f64_pair(low, high);
231        for t in Self::ALL_TONES {
232            if (lo - t.low_hz as f64).abs() <= tol_hz &&
233               (hi - t.high_hz as f64).abs() <= tol_hz {
234                return Some(t.key);
235            }
236        }
237        None
238    }
239
240    /// Snap an arbitrary (low, high) estimate to the nearest canonical pair and return (key, snapped_low, snapped_high).
241    /// Uses absolute distance independently on low and high bands.
242    pub fn nearest_u32(&self, low: u32, high: u32) -> (DtmfKey, u16, u16) {
243        let (lo, hi) = normalise_u32_pair(low, high);
244        let nearest_low  = nearest_in_set_u32(lo,  &Self::LOWS);
245        let nearest_high = nearest_in_set_u32(hi, &Self::HIGHS);
246        let key = Self::from_pair_exact(nearest_low, nearest_high)
247            .expect("canonical pair must map to a key");
248        (key, nearest_low, nearest_high)
249    }
250
251    /// Floating-point variant of nearest snap.
252    pub fn nearest_f64(&self, low: f64, high: f64) -> (DtmfKey, u16, u16) {
253        let (lo, hi) = normalise_f64_pair(low, high);
254        let nearest_low  = nearest_in_set_f64(lo,  &Self::LOWS);
255        let nearest_high = nearest_in_set_f64(hi, &Self::HIGHS);
256        let key = Self::from_pair_exact(nearest_low, nearest_high)
257            .expect("canonical pair must map to a key");
258        (key, nearest_low, nearest_high)
259    }
260}
261
262/* --------------------------- Small helpers --------------------------- */
263
264const fn abs_diff_u32(a: u32, b: u32) -> u32 {
265    if a >= b { a - b } else { b - a }
266}
267
268fn nearest_in_set_u32(x: u32, set: &[u16]) -> u16 {
269    let mut best = set[0];
270    let mut best_d = abs_diff_u32(x, best as u32);
271    let mut i = 1;
272    while i < set.len() {
273        let d = abs_diff_u32(x, set[i] as u32);
274        if d < best_d { best = set[i]; best_d = d; }
275        i += 1;
276    }
277    best
278}
279
280fn nearest_in_set_f64(x: f64, set: &[u16]) -> u16 {
281    let mut best = set[0];
282    let mut best_d = (x - best as f64).abs();
283    let mut i = 1;
284    while i < set.len() {
285        let d = (x - set[i] as f64).abs();
286        if d < best_d { best = set[i]; best_d = d; }
287        i += 1;
288    }
289    best
290}
291
292const fn normalise_u32_pair(a: u32, b: u32) -> (u32, u32) {
293    if a <= b { (a, b) } else { (b, a) }
294}
295
296fn normalise_f64_pair(a: f64, b: f64) -> (f64, f64) {
297    match a.partial_cmp(&b) {
298        Some(Ordering::Greater) => (b, a),
299        _ => (a, b),
300    }
301}