use crate::{DtmfKey as RustDtmfKey, DtmfTable as RustDtmfTable, DtmfTone as RustDtmfTone};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
extern crate std;
use std::collections::hash_map::DefaultHasher;
use std::format;
use std::hash::{Hash, Hasher};
#[pyclass(name = "DtmfKey", module = "dtmf_table", from_py_object)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyDtmfKey {
inner: RustDtmfKey,
}
#[pymethods]
impl PyDtmfKey {
#[staticmethod]
#[pyo3(signature = (c: "str"), text_signature = "(c: str) -> DtmfKey")]
fn from_char(c: char) -> PyResult<Self> {
match RustDtmfKey::from_char(c) {
Some(key) => Ok(PyDtmfKey { inner: key }),
None => Err(PyValueError::new_err(format!(
"Invalid DTMF character: '{}'",
c
))),
}
}
#[pyo3(signature = (), text_signature = "($self) -> str")]
const fn to_char(&self) -> char {
self.inner.to_char()
}
#[pyo3(signature = (), text_signature = "($self) -> tuple[int, int]")]
const fn freqs(&self) -> (u16, u16) {
self.inner.freqs()
}
fn __str__(&self) -> String {
self.inner.to_char().to_string()
}
fn __repr__(&self) -> String {
format!("DtmfKey('{}')", self.inner.to_char())
}
fn __eq__(&self, other: &PyDtmfKey) -> bool {
self.inner == other.inner
}
fn __hash__(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.inner.hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "DtmfTone", module = "dtmf_table", from_py_object)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyDtmfTone {
inner: RustDtmfTone,
}
#[pymethods]
impl PyDtmfTone {
#[new]
#[pyo3(signature = (key: "DtmfKey", low_hz: "int", high_hz: "int"), text_signature = "(key: DtmfKey, low_hz: int, high_hz: int)")]
const fn new(key: PyDtmfKey, low_hz: u16, high_hz: u16) -> Self {
PyDtmfTone {
inner: RustDtmfTone {
key: key.inner,
low_hz,
high_hz,
},
}
}
#[getter]
const fn key(&self) -> PyDtmfKey {
PyDtmfKey {
inner: self.inner.key,
}
}
#[getter]
const fn low_hz(&self) -> u16 {
self.inner.low_hz
}
#[getter]
const fn high_hz(&self) -> u16 {
self.inner.high_hz
}
fn __str__(&self) -> String {
format!(
"{}: ({} Hz, {} Hz)",
self.inner.key.to_char(),
self.inner.low_hz,
self.inner.high_hz
)
}
fn __repr__(&self) -> String {
format!(
"DtmfTone(key=DtmfKey('{}'), low_hz={}, high_hz={})",
self.inner.key.to_char(),
self.inner.low_hz,
self.inner.high_hz
)
}
fn __eq__(&self, other: &PyDtmfTone) -> bool {
self.inner == other.inner
}
}
#[pyclass(name = "DtmfTable", module = "dtmf_table")]
#[derive(Debug)]
pub struct PyDtmfTable {
inner: RustDtmfTable,
}
#[pymethods]
impl PyDtmfTable {
#[new]
#[pyo3(signature = (), text_signature = "()")]
const fn new() -> Self {
PyDtmfTable {
inner: RustDtmfTable::new(),
}
}
#[staticmethod]
#[pyo3(signature = (), text_signature = "() -> list[DtmfKey]")]
fn all_keys() -> Vec<PyDtmfKey> {
RustDtmfTable::ALL_KEYS
.iter()
.map(|&key| PyDtmfKey { inner: key })
.collect()
}
#[staticmethod]
#[pyo3(signature = (), text_signature = "() -> list[DtmfTone]")]
fn all_tones() -> Vec<PyDtmfTone> {
RustDtmfTable::ALL_TONES
.iter()
.map(|&tone| PyDtmfTone { inner: tone })
.collect()
}
#[staticmethod]
#[pyo3(signature = (), text_signature = "() -> list[list[DtmfKey]]")]
fn all_keys_matrix() -> Vec<Vec<PyDtmfKey>> {
let keys = &RustDtmfTable::ALL_KEYS;
vec![
keys[0..4].iter().map(|&k| PyDtmfKey { inner: k }).collect(),
keys[4..8].iter().map(|&k| PyDtmfKey { inner: k }).collect(),
keys[8..12]
.iter()
.map(|&k| PyDtmfKey { inner: k })
.collect(),
keys[12..16]
.iter()
.map(|&k| PyDtmfKey { inner: k })
.collect(),
]
}
#[staticmethod]
#[pyo3(signature = (), text_signature = "() -> list[list[DtmfTone]]")]
fn all_tones_matrix() -> Vec<Vec<PyDtmfTone>> {
let tones = &RustDtmfTable::ALL_TONES;
vec![
tones[0..4]
.iter()
.map(|&t| PyDtmfTone { inner: t })
.collect(),
tones[4..8]
.iter()
.map(|&t| PyDtmfTone { inner: t })
.collect(),
tones[8..12]
.iter()
.map(|&t| PyDtmfTone { inner: t })
.collect(),
tones[12..16]
.iter()
.map(|&t| PyDtmfTone { inner: t })
.collect(),
]
}
#[staticmethod]
#[pyo3(signature = (key: "DtmfKey"), text_signature = "(key: DtmfKey) -> tuple[int, int]")]
const fn lookup_key(key: PyDtmfKey) -> (u16, u16) {
RustDtmfTable::lookup_key(key.inner)
}
#[staticmethod]
#[pyo3(signature = (low: "int", high: "int"), text_signature = "(low: int, high: int) -> DtmfKey | None")]
fn from_pair_exact(low: u16, high: u16) -> Option<PyDtmfKey> {
RustDtmfTable::from_pair_exact(low, high).map(|key| PyDtmfKey { inner: key })
}
#[staticmethod]
#[pyo3(signature = (a: "int", b: "int"), text_signature = "(a: int, b: int) -> DtmfKey | None")]
fn from_pair_normalised(a: u16, b: u16) -> Option<PyDtmfKey> {
RustDtmfTable::from_pair_normalised(a, b).map(|key| PyDtmfKey { inner: key })
}
#[allow(clippy::wrong_self_convention)]
#[pyo3(signature = (low: "int", high: "int", tol_hz: "int"), text_signature = "(low: int, high: int, tol_hz: int) -> DtmfKey | None")]
fn from_pair_tol_u32(&self, low: u32, high: u32, tol_hz: u32) -> Option<PyDtmfKey> {
self.inner
.from_pair_tol_u32(low, high, tol_hz)
.map(|key| PyDtmfKey { inner: key })
}
#[allow(clippy::wrong_self_convention)]
#[pyo3(signature = (low: "float", high: "float", tol_hz: "float"), text_signature = "($self, low: float, high: float, tol_hz: float) -> DtmfKey | None")]
fn from_pair_tol_f64(&self, low: f64, high: f64, tol_hz: f64) -> Option<PyDtmfKey> {
self.inner
.from_pair_tol_f64(low, high, tol_hz)
.map(|key| PyDtmfKey { inner: key })
}
#[pyo3(signature = (low: "int", high: "int"), text_signature = "($self, low: int, high: int) -> tuple[DtmfKey, int, int]")]
fn nearest_u32(&self, low: u32, high: u32) -> (PyDtmfKey, u16, u16) {
let (key, snapped_low, snapped_high) = self.inner.nearest_u32(low, high);
(PyDtmfKey { inner: key }, snapped_low, snapped_high)
}
#[pyo3(signature = (low: "float", high: "float"), text_signature = "($self, low: float, high: float) -> tuple[DtmfKey, int, int]")]
fn nearest_f64(&self, low: f64, high: f64) -> (PyDtmfKey, u16, u16) {
let (key, snapped_low, snapped_high) = self.inner.nearest_f64(low, high);
(PyDtmfKey { inner: key }, snapped_low, snapped_high)
}
fn __str__(&self) -> String {
format!("{:#}", self.inner)
}
fn __repr__(&self) -> String {
"DtmfTable()".to_string()
}
}
#[pymodule]
fn dtmf_table(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyDtmfKey>()?;
m.add_class::<PyDtmfTone>()?;
m.add_class::<PyDtmfTable>()?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add(
"LOWS",
(
RustDtmfTable::LOWS[0],
RustDtmfTable::LOWS[1],
RustDtmfTable::LOWS[2],
RustDtmfTable::LOWS[3],
),
)?;
m.add(
"HIGHS",
(
RustDtmfTable::HIGHS[0],
RustDtmfTable::HIGHS[1],
RustDtmfTable::HIGHS[2],
RustDtmfTable::HIGHS[3],
),
)?;
m.add(
"__doc__",
r#"DTMF (Dual-Tone Multi-Frequency) frequency table for telephony applications.
This library provides efficient, const-first mappings between DTMF keys and their
canonical frequency pairs. Built with Rust for performance, it offers both exact
lookups and tolerance-based matching for real-world audio analysis.
Key Features:
- Zero-allocation const-evaluated mappings
- Bidirectional key ⟷ frequency conversion
- Tolerance-based reverse lookup for FFT analysis
- Frequency snapping for noisy estimates
- Support for all 16 standard DTMF tones
Constants:
LOWS: Low-band DTMF frequencies in Hz (697, 770, 852, 941)
HIGHS: High-band DTMF frequencies in Hz (1209, 1336, 1477, 1633)
Classes:
DtmfKey: Represents a single DTMF key (0-9, *, #, A-D)
DtmfTone: Combines a key with its frequency pair
DtmfTable: Main lookup table for conversions and analysis
Example:
>>> from dtmf_table import DtmfTable, DtmfKey, LOWS, HIGHS
>>> table = DtmfTable()
>>> key = DtmfKey.from_char('5')
>>> low, high = key.freqs()
>>> print(f"Key {key} = {low}Hz + {high}Hz")
Key 5 = 770Hz + 1336Hz
>>> print(f"Low frequencies: {LOWS}")
Low frequencies: (697, 770, 852, 941)
"#,
)?;
Ok(())
}