miniDSP
A small C library for audio DSP
Loading...
Searching...
No Matches
minidsp_dtmf.c
Go to the documentation of this file.
1
11#include "minidsp.h"
12
13/* -----------------------------------------------------------------------
14 * DTMF frequency tables
15 * ----------------------------------------------------------------------- */
16
17static const double dtmf_row_freqs[4] = {697.0, 770.0, 852.0, 941.0};
18static const double dtmf_col_freqs[4] = {1209.0, 1336.0, 1477.0, 1633.0};
19
20/* Keypad layout (row, col):
21 * 1209 1336 1477 1633
22 * 697: 1 2 3 A
23 * 770: 4 5 6 B
24 * 852: 7 8 9 C
25 * 941: * 0 # D
26 */
27static const char dtmf_keypad[4][4] = {
28 {'1', '2', '3', 'A'},
29 {'4', '5', '6', 'B'},
30 {'7', '8', '9', 'C'},
31 {'*', '0', '#', 'D'}
32};
33
34/* -----------------------------------------------------------------------
35 * Internal helpers
36 * ----------------------------------------------------------------------- */
37
40static void dtmf_char_to_freqs(char ch, double *row_freq, double *col_freq)
41{
42 int row = -1, col = -1;
43 switch (ch) {
44 case '1': row = 0; col = 0; break;
45 case '2': row = 0; col = 1; break;
46 case '3': row = 0; col = 2; break;
47 case 'A': case 'a': row = 0; col = 3; break;
48 case '4': row = 1; col = 0; break;
49 case '5': row = 1; col = 1; break;
50 case '6': row = 1; col = 2; break;
51 case 'B': case 'b': row = 1; col = 3; break;
52 case '7': row = 2; col = 0; break;
53 case '8': row = 2; col = 1; break;
54 case '9': row = 2; col = 2; break;
55 case 'C': case 'c': row = 2; col = 3; break;
56 case '*': row = 3; col = 0; break;
57 case '0': row = 3; col = 1; break;
58 case '#': row = 3; col = 2; break;
59 case 'D': case 'd': row = 3; col = 3; break;
60 default:
61 assert(0 && "invalid DTMF character");
62 }
63 *row_freq = dtmf_row_freqs[row];
64 *col_freq = dtmf_col_freqs[col];
65}
66
68static double peak_near_bin(const double *mag, unsigned num_bins, unsigned bin)
69{
70 double peak = mag[bin];
71 if (bin > 0 && mag[bin - 1] > peak)
72 peak = mag[bin - 1];
73 if (bin + 1 < num_bins && mag[bin + 1] > peak)
74 peak = mag[bin + 1];
75 return peak;
76}
77
80static char detect_frame(const double *mag, unsigned num_bins,
81 unsigned N, double sample_rate)
82{
83 /* Compute mean magnitude (exclude DC and Nyquist) for threshold. */
84 double sum = 0.0;
85 for (unsigned k = 1; k + 1 < num_bins; k++)
86 sum += mag[k];
87 double mean_mag = (num_bins > 2) ? sum / (double)(num_bins - 2) : 0.0;
88 double threshold = mean_mag * 8.0; /* ~18 dB above mean */
89
90 /* Measure magnitude at each DTMF row frequency. */
91 double row_mags[4];
92 for (int r = 0; r < 4; r++) {
93 unsigned bin = (unsigned)(dtmf_row_freqs[r] * N / sample_rate + 0.5);
94 if (bin >= num_bins) bin = num_bins - 1;
95 row_mags[r] = peak_near_bin(mag, num_bins, bin);
96 }
97
98 /* Measure magnitude at each DTMF column frequency. */
99 double col_mags[4];
100 for (int c = 0; c < 4; c++) {
101 unsigned bin = (unsigned)(dtmf_col_freqs[c] * N / sample_rate + 0.5);
102 if (bin >= num_bins) bin = num_bins - 1;
103 col_mags[c] = peak_near_bin(mag, num_bins, bin);
104 }
105
106 /* Find the strongest row and column that exceed the threshold. */
107 int best_row = -1;
108 double best_row_mag = 0.0;
109 for (int r = 0; r < 4; r++) {
110 if (row_mags[r] > threshold && row_mags[r] > best_row_mag) {
111 best_row = r;
112 best_row_mag = row_mags[r];
113 }
114 }
115
116 int best_col = -1;
117 double best_col_mag = 0.0;
118 for (int c = 0; c < 4; c++) {
119 if (col_mags[c] > threshold && col_mags[c] > best_col_mag) {
120 best_col = c;
121 best_col_mag = col_mags[c];
122 }
123 }
124
125 if (best_row < 0 || best_col < 0)
126 return '\0';
127
128 return dtmf_keypad[best_row][best_col];
129}
130
131/* -----------------------------------------------------------------------
132 * Public API
133 * ----------------------------------------------------------------------- */
134
135unsigned MD_dtmf_detect(const double *signal, unsigned signal_len,
136 double sample_rate,
137 MD_DTMFTone *tones_out, unsigned max_tones)
138{
139 assert(signal);
140 assert(tones_out);
141 assert(signal_len > 0);
142 assert(sample_rate >= 4000.0);
143 assert(max_tones > 0);
144
145 /* Pick FFT size: need enough resolution to separate DTMF row
146 * pairs (73 Hz minimum gap → need < 37 Hz resolution) but the
147 * window must stay shorter than the 40 ms Q.24 minimum pause so
148 * that the frame-based state machine can resolve inter-digit gaps.
149 * Target: largest power of two with window <= 35 ms. */
150 unsigned max_n = (unsigned)(0.035 * sample_rate);
151 unsigned N = 128;
152 while (N * 2 <= max_n) N <<= 1;
153 unsigned hop = N / 4;
154 unsigned num_bins = N / 2 + 1;
155
156 unsigned num_frames = (signal_len >= N)
157 ? (signal_len - N) / hop + 1
158 : 0;
159 if (num_frames == 0)
160 return 0;
161
162 /* ITU-T Q.24: 40 ms minimum tone-on and inter-digit pause.
163 * A tone of F consecutive detected frames implies a minimum duration
164 * of N + (F-1)*hop samples. Use ceiling division so that the
165 * minimum detectable tone is always >= 40 ms, with a floor of 2
166 * to debounce noise. */
167 unsigned q24_samples = (unsigned)(0.040 * sample_rate);
168 unsigned min_on_frames = (q24_samples > N)
169 ? (q24_samples - N + hop - 1) / hop + 1 : 2;
170 if (min_on_frames < 2) min_on_frames = 2;
171 unsigned min_off_frames = min_on_frames;
172
173 /* Working buffers. */
174 double *window = malloc(N * sizeof(double));
175 double *frame = malloc(N * sizeof(double));
176 double *mag = malloc(num_bins * sizeof(double));
177 assert(window && frame && mag);
178
179 MD_Gen_Hann_Win(window, N);
180
181 /* State machine. */
182 enum { IDLE, PENDING, ACTIVE } state = IDLE;
183 char current_digit = '\0';
184 unsigned on_count = 0;
185 unsigned off_count = 0;
186 unsigned tone_start_frame = 0;
187 unsigned tone_end_frame = 0;
188 unsigned num_tones = 0;
189
190 for (unsigned f = 0; f < num_frames && num_tones < max_tones; f++) {
191 unsigned start = f * hop;
192
193 /* Window the frame. */
194 for (unsigned i = 0; i < N; i++)
195 frame[i] = signal[start + i] * window[i];
196
197 /* Magnitude spectrum. */
198 MD_magnitude_spectrum(frame, N, mag);
199
200 /* Normalise to single-sided amplitude. */
201 for (unsigned k = 0; k < num_bins; k++) {
202 mag[k] /= (double)N;
203 if (k > 0 && k < N / 2)
204 mag[k] *= 2.0;
205 }
206
207 char digit = detect_frame(mag, num_bins, N, sample_rate);
208
209 switch (state) {
210 case IDLE:
211 if (digit != '\0') {
212 current_digit = digit;
213 on_count = 1;
214 tone_start_frame = f;
215 state = PENDING;
216 }
217 break;
218
219 case PENDING:
220 if (digit == current_digit) {
221 on_count++;
222 if (on_count >= min_on_frames) {
223 state = ACTIVE;
224 tone_end_frame = f;
225 off_count = 0;
226 }
227 } else if (digit != '\0') {
228 /* Different digit — restart. */
229 current_digit = digit;
230 on_count = 1;
231 tone_start_frame = f;
232 } else {
233 state = IDLE;
234 current_digit = '\0';
235 }
236 break;
237
238 case ACTIVE:
239 if (digit == current_digit && off_count == 0) {
240 /* Same digit, no gap — tone continues. */
241 tone_end_frame = f;
242 } else if (digit == current_digit
243 && off_count >= min_off_frames) {
244 /* Same digit reappeared after a gap >= Q.24 pause.
245 * This is a new instance of the same digit.
246 * Emit the current tone and start fresh. */
247 tones_out[num_tones].digit = current_digit;
248 tones_out[num_tones].start_s =
249 (double)(tone_start_frame * hop) / sample_rate;
250 tones_out[num_tones].end_s =
251 (double)((tone_end_frame + 1) * hop) / sample_rate;
252 num_tones++;
253
254 current_digit = digit;
255 on_count = 1;
256 tone_start_frame = f;
257 state = PENDING;
258 } else if (digit == current_digit) {
259 /* Brief interruption (< Q.24 pause), tolerate. */
260 off_count = 0;
261 tone_end_frame = f;
262 } else {
263 off_count++;
264 if (off_count >= min_off_frames) {
265 /* Emit the completed tone. */
266 tones_out[num_tones].digit = current_digit;
267 tones_out[num_tones].start_s =
268 (double)(tone_start_frame * hop) / sample_rate;
269 tones_out[num_tones].end_s =
270 (double)((tone_end_frame + 1) * hop) / sample_rate;
271 num_tones++;
272
273 if (digit != '\0') {
274 /* A different digit is present — start tracking
275 * it immediately so we don't lose this frame. */
276 current_digit = digit;
277 on_count = 1;
278 tone_start_frame = f;
279 state = PENDING;
280 } else {
281 state = IDLE;
282 current_digit = '\0';
283 }
284 }
285 }
286 break;
287 }
288 }
289
290 /* Emit a tone still active at end-of-signal.
291 * PENDING is only emitted if it already meets the Q.24 minimum-on
292 * requirement (on_count >= min_on_frames) to avoid false trailing
293 * detections. */
294 if (num_tones < max_tones) {
295 if (state == ACTIVE) {
296 tones_out[num_tones].digit = current_digit;
297 tones_out[num_tones].start_s =
298 (double)(tone_start_frame * hop) / sample_rate;
299 tones_out[num_tones].end_s =
300 (double)((tone_end_frame + 1) * hop) / sample_rate;
301 num_tones++;
302 } else if (state == PENDING && on_count >= min_on_frames) {
303 unsigned last = tone_start_frame + on_count - 1;
304 tones_out[num_tones].digit = current_digit;
305 tones_out[num_tones].start_s =
306 (double)(tone_start_frame * hop) / sample_rate;
307 tones_out[num_tones].end_s =
308 (double)((last + 1) * hop) / sample_rate;
309 num_tones++;
310 }
311 }
312
313 free(mag);
314 free(frame);
315 free(window);
316 return num_tones;
317}
318
319void MD_dtmf_generate(double *output, const char *digits,
320 double sample_rate,
321 unsigned tone_ms, unsigned pause_ms)
322{
323 assert(output);
324 assert(digits);
325 assert(sample_rate > 0);
326 assert(tone_ms >= 40);
327 assert(pause_ms >= 40);
328
329 unsigned num_digits = (unsigned)strlen(digits);
330 unsigned tone_samples = (unsigned)(tone_ms * sample_rate / 1000.0);
331 unsigned pause_samples = (unsigned)(pause_ms * sample_rate / 1000.0);
332 unsigned total = MD_dtmf_signal_length(num_digits, sample_rate,
333 tone_ms, pause_ms);
334
335 /* Zero-fill the entire output (silences between tones). */
336 memset(output, 0, total * sizeof(double));
337
338 /* Temporary buffer for the column sinusoid. */
339 double *col_tone = malloc(tone_samples * sizeof(double));
340 assert(col_tone);
341
342 unsigned offset = 0;
343 for (unsigned d = 0; d < num_digits; d++) {
344 double row_freq, col_freq;
345 dtmf_char_to_freqs(digits[d], &row_freq, &col_freq);
346
347 /* Row tone directly into output at amplitude 0.5. */
348 MD_sine_wave(output + offset, tone_samples, 0.5, row_freq, sample_rate);
349
350 /* Column tone into temp, then add. */
351 MD_sine_wave(col_tone, tone_samples, 0.5, col_freq, sample_rate);
352 for (unsigned i = 0; i < tone_samples; i++)
353 output[offset + i] += col_tone[i];
354
355 offset += tone_samples + pause_samples;
356 }
357
358 free(col_tone);
359}
360
361unsigned MD_dtmf_signal_length(unsigned num_digits, double sample_rate,
362 unsigned tone_ms, unsigned pause_ms)
363{
364 assert(sample_rate > 0);
365 if (num_digits == 0) return 0;
366
367 unsigned tone_samples = (unsigned)(tone_ms * sample_rate / 1000.0);
368 unsigned pause_samples = (unsigned)(pause_ms * sample_rate / 1000.0);
369 return num_digits * tone_samples
370 + (num_digits - 1) * pause_samples;
371}
A mini library of DSP (Digital Signal Processing) routines.
void MD_sine_wave(double *output, unsigned N, double amplitude, double freq, double sample_rate)
Generate a sine wave.
void MD_Gen_Hann_Win(double *out, unsigned n)
Generate a Hanning (Hann) window of length n.
void MD_magnitude_spectrum(const double *signal, unsigned N, double *mag_out)
Compute the magnitude spectrum of a real-valued signal.
unsigned MD_dtmf_detect(const double *signal, unsigned signal_len, double sample_rate, MD_DTMFTone *tones_out, unsigned max_tones)
Detect DTMF tones in an audio signal.
void MD_dtmf_generate(double *output, const char *digits, double sample_rate, unsigned tone_ms, unsigned pause_ms)
Generate a DTMF tone sequence.
static char detect_frame(const double *mag, unsigned num_bins, unsigned N, double sample_rate)
Detect the DTMF digit present in a single normalised magnitude frame.
static void dtmf_char_to_freqs(char ch, double *row_freq, double *col_freq)
Map a DTMF character to its row and column frequencies.
unsigned MD_dtmf_signal_length(unsigned num_digits, double sample_rate, unsigned tone_ms, unsigned pause_ms)
Calculate the number of samples needed for MD_dtmf_generate().
static double peak_near_bin(const double *mag, unsigned num_bins, unsigned bin)
Peak magnitude in bins [bin-1, bin, bin+1], clamped to [0, num_bins).
A single detected DTMF tone with timing information.
Definition minidsp.h:1112
char digit
Decoded digit: '0'–'9', 'A'–'D', '*', or '#'.
Definition minidsp.h:1113
double end_s
Tone offset time in seconds.
Definition minidsp.h:1115
double start_s
Tone onset time in seconds.
Definition minidsp.h:1114