一个使用 Python 实现的 DTMF(双音多频)编码与解码项目。
本项目当前已经包含两条主线能力:
-
DTMF Encoder- 将按键字符串(如
123#A)编码为标准 DTMF 双音音频 - 支持输出为内存中的 PCM 浮点样本
- 支持导出为
.wav文件
- 将按键字符串(如
-
DTMF Decoder- 对
.wav文件或内存样本进行 DTMF 解码 - 核心检测算法使用
Goertzel - 支持常见采样率输入
- 第一版会自动重采样到内部工作采样率
8000 Hz - 支持滑动窗口扫描与状态机去重,避免长按重复输出
- 对
- 核心解码算法使用
Goertzel,不依赖全频域FFT - 支持
0-9、*、#、A-D全部 16 个标准 DTMF 按键 - 内部采用模块化设计,编码器与解码器职责分离
- 针对噪声和误报加入了峰值集中度、Twist、绝对阈值等判定逻辑
- 正式实现中使用了中文注释,方便后续继续维护
DTMF-Encod-Decode/
├── PRD.md
├── README.md
├── assets/
├── docs/
├── src/
│ ├── dtmf_decoder/
│ │ ├── __init__.py
│ │ ├── audio.py
│ │ ├── cli.py
│ │ ├── config.py
│ │ ├── detector.py
│ │ ├── goertzel.py
│ │ ├── scanner.py
│ │ └── state_machine.py
│ └── dtmf_encoder/
│ ├── __init__.py
│ └── encoder.py
└── tests/
├── __init__.py
├── test_audio.py
├── test_detector.py
├── test_encoder.py
├── test_goertzel.py
├── test_scanner.py
└── test_state_machine.py
config.py- 管理解码参数、频率表、Twist 门限、窗口长度、步长等配置
audio.py- 读取
.wav - 执行单声道归一化
- 在必要时自动重采样到内部工作采样率
- 读取
goertzel.py- 对单帧样本计算 8 个目标频率的能量
detector.py- 根据频率能量完成合法 DTMF 判定
scanner.py- 对长音频做滑动窗口扫描
- 汇总逐帧结果
- 输出最终去重后的按键串
state_machine.py- 对连续帧结果做去重
cli.py- 提供命令行编解码入口
encoder.py- 根据按键字符串生成 DTMF 双音
- 支持输出浮点样本或写出
.wav
DTMF 只关心固定的 8 个标准频率:
- 低频组:
697、770、852、941 - 高频组:
1209、1336、1477、1633
因此没有必要对整段频谱做 FFT。
本项目使用 Goertzel 算法逐帧计算这 8 个目标频点的能量,降低无关计算。
- 窗口长度:
205 - 步长:
102 - 内部工作采样率:
8000 Hz
这样既能区分各个 DTMF 频率,又能减少漏检。
检测并不是简单地取两个最大频率,而是同时做以下检查:
- 绝对能量阈值检查
- 高频与低频的
Twist比例检查 - 同组内最大峰值与次大峰值的集中度检查
一个按键通常持续几十到上百毫秒,会跨越多个窗口。
如果不做处理,解码结果会变成类似 5555。
本项目使用状态机去重:
- 同一个按键连续出现时只记录一次
- 只有当信号消失后再次出现,才算新的同键输入
- 如果按键从一个值切换到另一个值,则立即记录新按键
本项目不要求输入音频必须是 8000 Hz。
当前第一版策略是:
- 支持常见输入采样率,例如
8000、16000、22050、44100、48000 Hz - 解码前自动重采样到内部工作采样率
8000 Hz - 后续所有 Goertzel 检测参数都基于内部工作采样率执行
这样做的原因:
- 不限制用户输入来源
- 保持窗口参数和阈值体系稳定
- 先把第一版工程复杂度控制住
编码器会为每个按键找到对应的:
- 一个低频
- 一个高频
然后将两个正弦波叠加,生成双音信号。
默认参数:
- 输出采样率:
8000 Hz - 单键持续时间:
100 ms - 按键间静音:
50 ms - 输出幅度:
0.8
编码器也支持自定义这些参数。
建议环境:
Python 3.10+
当前实现优先使用标准库,不强依赖第三方科学计算包。
PYTHONPATH=src python3 -m dtmf_decoder.cli encode "123#A" output.wav可选参数:
PYTHONPATH=src python3 -m dtmf_decoder.cli encode "123#A" output.wav \
--sample-rate 16000 \
--tone-ms 120 \
--silence-ms 60 \
--amplitude 0.8PYTHONPATH=src python3 -m dtmf_decoder.cli decode input.wav输出内容为最终去重后的按键字符串。
from dtmf_encoder import DTMFEncoder
encoder = DTMFEncoder()
samples = encoder.encode("13800138000")
encoder.write_wav("dial.wav", "13800138000")from dtmf_decoder import DTMFScanner
scanner = DTMFScanner()
result = scanner.scan_samples(samples, sample_rate=8000)
print(result.decoded_text)from dtmf_decoder import decode_wav_file
decoded = decode_wav_file("dial.wav")
print(decoded)项目里已经提供了一个完整示例:
这个脚本会执行完整回环:
- 取圆周率前
100位数字 - 生成 DTMF 音频
- 输出示例文件
- 再对这个音频执行解码
- 检查解码结果是否与原始输入完全一致
运行方式:
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 examples/pi_roundtrip.py当前保留的示例音频文件:
这个示例文件已经实际跑通过,当前结果是:
- 原始长度:
100 - 解码长度:
100 - 是否完全一致:
True
当前测试覆盖以下能力:
- Goertzel 对目标频率的响应
- 单帧 DTMF 检测
- 噪声拒识别
- 自动重采样路径
- 长音频扫描解码
- 去重状态机
- 编码结果回环解码
- 非法字符输入异常
按你当前项目规则,测试命令需要确认后再执行。
建议执行命令:
PYTHONPATH=src python3 -m unittest discover -s tests -v- 重采样目前使用纯 Python 线性插值实现,优先保证可读性和工程可控性
- 当前主要支持
.wav输入,实时流接口还没有单独封装 - 抗噪参数已经有基础实现,但后续仍建议结合真实样本继续调参
可以继续推进的方向:
- 增加实时流式解码接口
- 增加更强的重采样实现
- 为 CLI 增加批量处理能力
- 引入真实录音样本做参数标定
- 增加可视化调试工具,输出逐帧能量和判定结果