536 lines
19 KiB
C
536 lines
19 KiB
C
/*
|
|
* SpanDSP - a series of DSP components for telephony
|
|
*
|
|
* dtmf.c - DTMF generation and detection.
|
|
*
|
|
* Written by Steve Underwood <steveu@coppice.org>
|
|
*
|
|
* Copyright (C) 2001-2003, 2005, 2006 Steve Underwood
|
|
*
|
|
* All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License version 2.1,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*
|
|
* $Id: dtmf.c,v 1.50 2009/02/03 16:28:39 steveu Exp $
|
|
*/
|
|
|
|
/*! \file dtmf.h */
|
|
|
|
#if defined(HAVE_CONFIG_H)
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <inttypes.h>
|
|
#include <stdlib.h>
|
|
#if defined(HAVE_TGMATH_H)
|
|
#include <tgmath.h>
|
|
#endif
|
|
#if defined(HAVE_MATH_H)
|
|
#include <math.h>
|
|
#endif
|
|
#include "floating_fudge.h"
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
#include <fcntl.h>
|
|
|
|
#include "spandsp/telephony.h"
|
|
#include "spandsp/fast_convert.h"
|
|
#include "spandsp/queue.h"
|
|
#include "spandsp/complex.h"
|
|
#include "spandsp/dds.h"
|
|
#include "spandsp/tone_detect.h"
|
|
#include "spandsp/tone_generate.h"
|
|
#include "spandsp/super_tone_rx.h"
|
|
#include "spandsp/dtmf.h"
|
|
|
|
#include "spandsp/private/queue.h"
|
|
#include "spandsp/private/tone_generate.h"
|
|
#include "spandsp/private/dtmf.h"
|
|
|
|
#define DEFAULT_DTMF_TX_LEVEL -10
|
|
#define DEFAULT_DTMF_TX_ON_TIME 50
|
|
#define DEFAULT_DTMF_TX_OFF_TIME 55
|
|
|
|
#if defined(SPANDSP_USE_FIXED_POINT)
|
|
#define DTMF_THRESHOLD 10438 /* -42dBm0 */
|
|
#define DTMF_NORMAL_TWIST 6.309f /* 8dB */
|
|
#define DTMF_REVERSE_TWIST 2.512f /* 4dB */
|
|
#define DTMF_RELATIVE_PEAK_ROW 6.309f /* 8dB */
|
|
#define DTMF_RELATIVE_PEAK_COL 6.309f /* 8dB */
|
|
#define DTMF_TO_TOTAL_ENERGY 83.868f /* -0.85dB */
|
|
#define DTMF_POWER_OFFSET 68.251f /* 10*log(256.0*256.0*DTMF_SAMPLES_PER_BLOCK) */
|
|
#define DTMF_SAMPLES_PER_BLOCK 102
|
|
#else
|
|
#define DTMF_THRESHOLD 171032462.0f /* -42dBm0 [((DTMF_SAMPLES_PER_BLOCK*32768.0/1.4142)*10^((-42 - DBM0_MAX_SINE_POWER)/20.0))^2 => 171032462.0] */
|
|
#define DTMF_NORMAL_TWIST 6.309f /* 8dB [10^(8/10) => 6.309] */
|
|
#define DTMF_REVERSE_TWIST 2.512f /* 4dB */
|
|
#define DTMF_RELATIVE_PEAK_ROW 6.309f /* 8dB */
|
|
#define DTMF_RELATIVE_PEAK_COL 6.309f /* 8dB */
|
|
#define DTMF_TO_TOTAL_ENERGY 83.868f /* -0.85dB [DTMF_SAMPLES_PER_BLOCK*10^(-0.85/10.0)] */
|
|
#define DTMF_POWER_OFFSET 110.395f /* 10*log(32768.0*32768.0*DTMF_SAMPLES_PER_BLOCK) */
|
|
#define DTMF_SAMPLES_PER_BLOCK 102
|
|
#endif
|
|
|
|
static const float dtmf_row[] =
|
|
{
|
|
697.0f, 770.0f, 852.0f, 941.0f
|
|
};
|
|
static const float dtmf_col[] =
|
|
{
|
|
1209.0f, 1336.0f, 1477.0f, 1633.0f
|
|
};
|
|
|
|
static const char dtmf_positions[] = "123A" "456B" "789C" "*0#D";
|
|
|
|
static goertzel_descriptor_t dtmf_detect_row[4];
|
|
static goertzel_descriptor_t dtmf_detect_col[4];
|
|
|
|
static int dtmf_tx_inited = FALSE;
|
|
static tone_gen_descriptor_t dtmf_digit_tones[16];
|
|
|
|
SPAN_DECLARE(int) dtmf_rx(dtmf_rx_state_t *s, const int16_t amp[], int samples)
|
|
{
|
|
#if defined(SPANDSP_USE_FIXED_POINT)
|
|
int32_t row_energy[4];
|
|
int32_t col_energy[4];
|
|
int16_t xamp;
|
|
float famp;
|
|
#else
|
|
float row_energy[4];
|
|
float col_energy[4];
|
|
float xamp;
|
|
float famp;
|
|
#endif
|
|
float v1;
|
|
int i;
|
|
int j;
|
|
int sample;
|
|
int best_row;
|
|
int best_col;
|
|
int limit;
|
|
uint8_t hit;
|
|
|
|
hit = 0;
|
|
for (sample = 0; sample < samples; sample = limit)
|
|
{
|
|
/* The block length is optimised to meet the DTMF specs. */
|
|
if ((samples - sample) >= (DTMF_SAMPLES_PER_BLOCK - s->current_sample))
|
|
limit = sample + (DTMF_SAMPLES_PER_BLOCK - s->current_sample);
|
|
else
|
|
limit = samples;
|
|
/* The following unrolled loop takes only 35% (rough estimate) of the
|
|
time of a rolled loop on the machine on which it was developed */
|
|
for (j = sample; j < limit; j++)
|
|
{
|
|
xamp = amp[j];
|
|
if (s->filter_dialtone)
|
|
{
|
|
famp = xamp;
|
|
/* Sharp notches applied at 350Hz and 440Hz - the two common dialtone frequencies.
|
|
These are rather high Q, to achieve the required narrowness, without using lots of
|
|
sections. */
|
|
v1 = 0.98356f*famp + 1.8954426f*s->z350[0] - 0.9691396f*s->z350[1];
|
|
famp = v1 - 1.9251480f*s->z350[0] + s->z350[1];
|
|
s->z350[1] = s->z350[0];
|
|
s->z350[0] = v1;
|
|
|
|
v1 = 0.98456f*famp + 1.8529543f*s->z440[0] - 0.9691396f*s->z440[1];
|
|
famp = v1 - 1.8819938f*s->z440[0] + s->z440[1];
|
|
s->z440[1] = s->z440[0];
|
|
s->z440[0] = v1;
|
|
xamp = famp;
|
|
}
|
|
xamp = goertzel_preadjust_amp(xamp);
|
|
#if defined(SPANDSP_USE_FIXED_POINT)
|
|
s->energy += ((int32_t) xamp*xamp);
|
|
#else
|
|
s->energy += xamp*xamp;
|
|
#endif
|
|
goertzel_samplex(&s->row_out[0], xamp);
|
|
goertzel_samplex(&s->col_out[0], xamp);
|
|
goertzel_samplex(&s->row_out[1], xamp);
|
|
goertzel_samplex(&s->col_out[1], xamp);
|
|
goertzel_samplex(&s->row_out[2], xamp);
|
|
goertzel_samplex(&s->col_out[2], xamp);
|
|
goertzel_samplex(&s->row_out[3], xamp);
|
|
goertzel_samplex(&s->col_out[3], xamp);
|
|
}
|
|
s->current_sample += (limit - sample);
|
|
if (s->current_sample < DTMF_SAMPLES_PER_BLOCK)
|
|
continue;
|
|
|
|
/* We are at the end of a DTMF detection block */
|
|
/* Find the peak row and the peak column */
|
|
row_energy[0] = goertzel_result(&s->row_out[0]);
|
|
best_row = 0;
|
|
col_energy[0] = goertzel_result(&s->col_out[0]);
|
|
best_col = 0;
|
|
for (i = 1; i < 4; i++)
|
|
{
|
|
row_energy[i] = goertzel_result(&s->row_out[i]);
|
|
if (row_energy[i] > row_energy[best_row])
|
|
best_row = i;
|
|
col_energy[i] = goertzel_result(&s->col_out[i]);
|
|
if (col_energy[i] > col_energy[best_col])
|
|
best_col = i;
|
|
}
|
|
hit = 0;
|
|
/* Basic signal level test and the twist test */
|
|
if (row_energy[best_row] >= s->threshold
|
|
&&
|
|
col_energy[best_col] >= s->threshold
|
|
&&
|
|
col_energy[best_col] < row_energy[best_row]*s->reverse_twist
|
|
&&
|
|
col_energy[best_col]*s->normal_twist > row_energy[best_row])
|
|
{
|
|
/* Relative peak test ... */
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
if ((i != best_col && col_energy[i]*DTMF_RELATIVE_PEAK_COL > col_energy[best_col])
|
|
||
|
|
(i != best_row && row_energy[i]*DTMF_RELATIVE_PEAK_ROW > row_energy[best_row]))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
/* ... and fraction of total energy test */
|
|
if (i >= 4
|
|
&&
|
|
(row_energy[best_row] + col_energy[best_col]) > DTMF_TO_TOTAL_ENERGY*s->energy)
|
|
{
|
|
/* Got a hit */
|
|
hit = dtmf_positions[(best_row << 2) + best_col];
|
|
}
|
|
}
|
|
/* The logic in the next test should ensure the following for different successive hit patterns:
|
|
-----ABB = start of digit B.
|
|
----B-BB = start of digit B
|
|
----A-BB = start of digit B
|
|
BBBBBABB = still in digit B.
|
|
BBBBBB-- = end of digit B
|
|
BBBBBBC- = end of digit B
|
|
BBBBACBB = B ends, then B starts again.
|
|
BBBBBBCC = B ends, then C starts.
|
|
BBBBBCDD = B ends, then D starts.
|
|
This can work with:
|
|
- Back to back differing digits. Back-to-back digits should
|
|
not happen. The spec. says there should be a gap between digits.
|
|
However, many real phones do not impose a gap, and rolling across
|
|
the keypad can produce little or no gap.
|
|
- It tolerates nasty phones that give a very wobbly start to a digit.
|
|
- VoIP can give sample slips. The phase jumps that produces will cause
|
|
the block it is in to give no detection. This logic will ride over a
|
|
single missed block, and not falsely declare a second digit. If the
|
|
hiccup happens in the wrong place on a minimum length digit, however
|
|
we would still fail to detect that digit. Could anything be done to
|
|
deal with that? Packet loss is clearly a no-go zone.
|
|
Note this is only relevant to VoIP using A-law, u-law or similar.
|
|
Low bit rate codecs scramble DTMF too much for it to be recognised,
|
|
and often slip in units larger than a sample. */
|
|
if (hit != s->in_digit)
|
|
{
|
|
if (s->last_hit != s->in_digit)
|
|
{
|
|
/* We have two successive indications that something has changed. */
|
|
/* To declare digit on, the hits must agree. Otherwise we declare tone off. */
|
|
hit = (hit && hit == s->last_hit) ? hit : 0;
|
|
if (s->realtime_callback)
|
|
{
|
|
/* Avoid reporting multiple no digit conditions on flaky hits */
|
|
if (s->in_digit || hit)
|
|
{
|
|
i = (s->in_digit && !hit) ? -99 : lfastrintf(log10f(s->energy)*10.0f - DTMF_POWER_OFFSET + DBM0_MAX_POWER);
|
|
s->realtime_callback(s->realtime_callback_data, hit, i, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (hit)
|
|
{
|
|
if (s->current_digits < MAX_DTMF_DIGITS)
|
|
{
|
|
s->digits[s->current_digits++] = (char) hit;
|
|
s->digits[s->current_digits] = '\0';
|
|
if (s->digits_callback)
|
|
{
|
|
s->digits_callback(s->digits_callback_data, s->digits, s->current_digits);
|
|
s->current_digits = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
s->lost_digits++;
|
|
}
|
|
}
|
|
}
|
|
s->in_digit = hit;
|
|
}
|
|
}
|
|
s->last_hit = hit;
|
|
#if defined(SPANDSP_USE_FIXED_POINT)
|
|
s->energy = 0;
|
|
#else
|
|
s->energy = 0.0f;
|
|
#endif
|
|
s->current_sample = 0;
|
|
}
|
|
if (s->current_digits && s->digits_callback)
|
|
{
|
|
s->digits_callback(s->digits_callback_data, s->digits, s->current_digits);
|
|
s->digits[0] = '\0';
|
|
s->current_digits = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(int) dtmf_rx_status(dtmf_rx_state_t *s)
|
|
{
|
|
if (s->in_digit)
|
|
return s->in_digit;
|
|
if (s->last_hit)
|
|
return 'x';
|
|
return 0;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(size_t) dtmf_rx_get(dtmf_rx_state_t *s, char *buf, int max)
|
|
{
|
|
if (max > s->current_digits)
|
|
max = s->current_digits;
|
|
if (max > 0)
|
|
{
|
|
memcpy(buf, s->digits, max);
|
|
memmove(s->digits, s->digits + max, s->current_digits - max);
|
|
s->current_digits -= max;
|
|
}
|
|
buf[max] = '\0';
|
|
return max;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(void) dtmf_rx_set_realtime_callback(dtmf_rx_state_t *s,
|
|
tone_report_func_t callback,
|
|
void *user_data)
|
|
{
|
|
s->realtime_callback = callback;
|
|
s->realtime_callback_data = user_data;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(void) dtmf_rx_parms(dtmf_rx_state_t *s,
|
|
int filter_dialtone,
|
|
int twist,
|
|
int reverse_twist,
|
|
int threshold)
|
|
{
|
|
float x;
|
|
|
|
if (filter_dialtone >= 0)
|
|
{
|
|
s->z350[0] = 0.0f;
|
|
s->z350[1] = 0.0f;
|
|
s->z440[0] = 0.0f;
|
|
s->z440[1] = 0.0f;
|
|
s->filter_dialtone = filter_dialtone;
|
|
}
|
|
if (twist >= 0)
|
|
s->normal_twist = powf(10.0f, twist/10.0f);
|
|
if (reverse_twist >= 0)
|
|
s->reverse_twist = powf(10.0f, reverse_twist/10.0f);
|
|
if (threshold > -99)
|
|
{
|
|
x = (DTMF_SAMPLES_PER_BLOCK*32768.0f/1.4142f)*powf(10.0f, (threshold - DBM0_MAX_SINE_POWER)/20.0f);
|
|
s->threshold = x*x;
|
|
}
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(dtmf_rx_state_t *) dtmf_rx_init(dtmf_rx_state_t *s,
|
|
digits_rx_callback_t callback,
|
|
void *user_data)
|
|
{
|
|
int i;
|
|
static int initialised = FALSE;
|
|
|
|
if (s == NULL)
|
|
{
|
|
if ((s = (dtmf_rx_state_t *) malloc(sizeof (*s))) == NULL)
|
|
return NULL;
|
|
}
|
|
s->digits_callback = callback;
|
|
s->digits_callback_data = user_data;
|
|
s->realtime_callback = NULL;
|
|
s->realtime_callback_data = NULL;
|
|
s->filter_dialtone = FALSE;
|
|
s->normal_twist = DTMF_NORMAL_TWIST;
|
|
s->reverse_twist = DTMF_REVERSE_TWIST;
|
|
s->threshold = DTMF_THRESHOLD;
|
|
|
|
s->in_digit = 0;
|
|
s->last_hit = 0;
|
|
|
|
if (!initialised)
|
|
{
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
make_goertzel_descriptor(&dtmf_detect_row[i], dtmf_row[i], DTMF_SAMPLES_PER_BLOCK);
|
|
make_goertzel_descriptor(&dtmf_detect_col[i], dtmf_col[i], DTMF_SAMPLES_PER_BLOCK);
|
|
}
|
|
initialised = TRUE;
|
|
}
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
goertzel_init(&s->row_out[i], &dtmf_detect_row[i]);
|
|
goertzel_init(&s->col_out[i], &dtmf_detect_col[i]);
|
|
}
|
|
#if defined(SPANDSP_USE_FIXED_POINT)
|
|
s->energy = 0;
|
|
#else
|
|
s->energy = 0.0f;
|
|
#endif
|
|
s->current_sample = 0;
|
|
s->lost_digits = 0;
|
|
s->current_digits = 0;
|
|
s->digits[0] = '\0';
|
|
return s;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(int) dtmf_rx_free(dtmf_rx_state_t *s)
|
|
{
|
|
free(s);
|
|
return 0;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
static void dtmf_tx_initialise(void)
|
|
{
|
|
int row;
|
|
int col;
|
|
|
|
if (dtmf_tx_inited)
|
|
return;
|
|
for (row = 0; row < 4; row++)
|
|
{
|
|
for (col = 0; col < 4; col++)
|
|
{
|
|
make_tone_gen_descriptor(&dtmf_digit_tones[row*4 + col],
|
|
(int) dtmf_row[row],
|
|
DEFAULT_DTMF_TX_LEVEL,
|
|
(int) dtmf_col[col],
|
|
DEFAULT_DTMF_TX_LEVEL,
|
|
DEFAULT_DTMF_TX_ON_TIME,
|
|
DEFAULT_DTMF_TX_OFF_TIME,
|
|
0,
|
|
0,
|
|
FALSE);
|
|
}
|
|
}
|
|
dtmf_tx_inited = TRUE;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(int) dtmf_tx(dtmf_tx_state_t *s, int16_t amp[], int max_samples)
|
|
{
|
|
int len;
|
|
const char *cp;
|
|
int digit;
|
|
|
|
len = 0;
|
|
if (s->tones.current_section >= 0)
|
|
{
|
|
/* Deal with the fragment left over from last time */
|
|
len = tone_gen(&(s->tones), amp, max_samples);
|
|
}
|
|
while (len < max_samples && (digit = queue_read_byte(&s->queue.queue)) >= 0)
|
|
{
|
|
/* Step to the next digit */
|
|
if (digit == 0)
|
|
continue;
|
|
if ((cp = strchr(dtmf_positions, digit)) == NULL)
|
|
continue;
|
|
tone_gen_init(&(s->tones), &dtmf_digit_tones[cp - dtmf_positions]);
|
|
s->tones.tone[0].gain = s->low_level;
|
|
s->tones.tone[1].gain = s->high_level;
|
|
s->tones.duration[0] = s->on_time;
|
|
s->tones.duration[1] = s->off_time;
|
|
len += tone_gen(&(s->tones), amp + len, max_samples - len);
|
|
}
|
|
return len;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(int) dtmf_tx_put(dtmf_tx_state_t *s, const char *digits, int len)
|
|
{
|
|
size_t space;
|
|
|
|
/* This returns the number of characters that would not fit in the buffer.
|
|
The buffer will only be loaded if the whole string of digits will fit,
|
|
in which case zero is returned. */
|
|
if (len < 0)
|
|
{
|
|
if ((len = strlen(digits)) == 0)
|
|
return 0;
|
|
}
|
|
if ((space = queue_free_space(&s->queue.queue)) < (size_t) len)
|
|
return len - (int) space;
|
|
if (queue_write(&s->queue.queue, (const uint8_t *) digits, len) >= 0)
|
|
return 0;
|
|
return -1;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(void) dtmf_tx_set_level(dtmf_tx_state_t *s, int level, int twist)
|
|
{
|
|
s->low_level = dds_scaling_dbm0f((float) level);
|
|
s->high_level = dds_scaling_dbm0f((float) (level + twist));
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(void) dtmf_tx_set_timing(dtmf_tx_state_t *s, int on_time, int off_time)
|
|
{
|
|
s->on_time = ((on_time >= 0) ? on_time : DEFAULT_DTMF_TX_ON_TIME)*SAMPLE_RATE/1000;
|
|
s->off_time = ((off_time >= 0) ? off_time : DEFAULT_DTMF_TX_OFF_TIME)*SAMPLE_RATE/1000;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(dtmf_tx_state_t *) dtmf_tx_init(dtmf_tx_state_t *s)
|
|
{
|
|
if (s == NULL)
|
|
{
|
|
if ((s = (dtmf_tx_state_t *) malloc(sizeof (*s))) == NULL)
|
|
return NULL;
|
|
}
|
|
if (!dtmf_tx_inited)
|
|
dtmf_tx_initialise();
|
|
tone_gen_init(&(s->tones), &dtmf_digit_tones[0]);
|
|
dtmf_tx_set_level(s, DEFAULT_DTMF_TX_LEVEL, 0);
|
|
dtmf_tx_set_timing(s, -1, -1);
|
|
queue_init(&s->queue.queue, MAX_DTMF_DIGITS, QUEUE_READ_ATOMIC | QUEUE_WRITE_ATOMIC);
|
|
s->tones.current_section = -1;
|
|
return s;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
|
|
SPAN_DECLARE(int) dtmf_tx_free(dtmf_tx_state_t *s)
|
|
{
|
|
free(s);
|
|
return 0;
|
|
}
|
|
/*- End of function --------------------------------------------------------*/
|
|
/*- End of file ------------------------------------------------------------*/
|