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