双音频是电话系统中电话机与交换机之间的一种用户信令,通俗的讲, 就是两种不同的频率混音在一起的音频信号, 并代表某个数值.
双音频信号是贝尔实验室发明的,其目的是为了自动完成长途呼叫。
双音频的拨号键盘是4×4的矩阵,每一行代表一个低频,每一列代表一个高频。每按一个键就发送一个高频和低频的正弦信号组合,比如'1'相当于697和1209赫兹(Hz)。交换机可以解码这些频率组合並确定所对应的按键。
双音多频键盘
1209 Hz 1336 Hz 1477 Hz 1633 Hz
697 Hz 1 2 3 A
770 Hz 4 5 6 B
852 Hz 7 8 9 C
941 Hz * 0 # D
特別頻率
忙音 480 Hz 620 Hz
拔号提示音 350 Hz 440 Hz
国内忙音是450hz的音频信号, 350 毫秒的音频和350毫秒的静音不停的切换.
国内的拔号提示音也是450Hz的音频信号, 只不过是连续的不间断的音频信号.
像这种固定的频率信号的检测, 可以使用FFT将音频从时域转到频域, 然后分析频点即可确定信号内容. 但是, FFT是针对整体信号的整体频域的计算, FFT算法复杂, 执行效率低, 因而并不推荐.
Goertzel算法刚好能够高效的检测频率点, 并且算法简单, 执行效率高, 下面是Goertzel算法的描述:
s_prev = 0
s_prev2 = 0
normalized_frequency = target_frequency / sample_rate;
coeff = 2*cos(2*PI*normalized_frequency);
for each sample, x[n],
s = x[n] + coeff*s_prev - s_prev2;
s_prev2 = s_prev;
s_prev = s;
end
power = s_prev2*s_prev2 + s_prev*s_prev - coeff*s_prev*s_prev2;
有了上面的算法描述, 不难写出代码的. 下面给出一段开源的关于Goertzel算法的c代码实现:
#define SAMPLING_RATE 8000
#define MAX_BINS 8
#define GOERTZEL_N 92
int sample_count;
double q1[ MAX_BINS ];
double q2[ MAX_BINS ];
double r[ MAX_BINS ];
/*
* coef = 2.0 * cos( (2.0 * PI * k) / (float)GOERTZEL_N)) ;
* Where k = (int) (0.5 + ((float)GOERTZEL_N * target_freq) / SAMPLING_RATE));
*
* More simply: coef = 2.0 * cos( (2.0 * PI * target_freq) / SAMPLING_RATE );
*/
double freqs[ MAX_BINS] =
{
697,
770,
852,
941,
1209,
1336,
1477,
1633
};
double coefs[ MAX_BINS ] ;
/*----------------------------------------------------------------------------
* calc_coeffs
*----------------------------------------------------------------------------
* This is where we calculate the correct co-efficients.
*/
void calc_coeffs()
{
int n;
for(n = 0; n < MAX_BINS; n++)
{
coefs[n] = 2.0 * cos(2.0 * 3.141592654 * freqs[n] / SAMPLING_RATE);
}
}
/*----------------------------------------------------------------------------
* post_testing
*----------------------------------------------------------------------------
* This is where we look at the bins and decide if we have a valid signal.
*/
void post_testing()
{
int row, col, see_digit;
int peak_count, max_index;
double maxval, t;
int i;
char * row_col_ascii_codes[4][4] = {
{"1", "2", "3", "A"},
{"4", "5", "6", "B"},
{"7", "8", "9", "C"},
{"*", "0", "#", "D"}};
/* Find the largest in the row group. */
row = 0;
maxval = 0.0;
for ( i=0; i<4; i++ )
{
if ( r[i] > maxval )
{
maxval = r[i];
row = i;
}
}
/* Find the largest in the column group. */
col = 4;
maxval = 0.0;
for ( i=4; i<8; i++ )
{
if ( r[i] > maxval )
{
maxval = r[i];
col = i;
}
}
/* Check for minimum energy */
if ( r[row] < 4.0e5 ) /* 2.0e5 ... 1.0e8 no change */
{
/* energy not high enough */
}
else if ( r[col] < 4.0e5 )
{
/* energy not high enough */
}
else
{
see_digit = TRUE;
/* Twist check
* CEPT => twist < 6dB
* AT&T => forward twist < 4dB and reverse twist < 8dB
* -ndB < 10 log10( v1 / v2 ), where v1 < v2
* -4dB < 10 log10( v1 / v2 )
* -0.4 < log10( v1 / v2 )
* 0.398 < v1 / v2
* 0.398 * v2 < v1
*/
if ( r[col] > r[row] )
{
/* Normal twist */
max_index = col;
if ( r[row] < (r[col] * 0.398) ) /* twist > 4dB, error */
see_digit = FALSE;
}
else /* if ( r[row] > r[col] ) */
{
/* Reverse twist */
max_index = row;
if ( r[col] < (r[row] * 0.158) ) /* twist > 8db, error */
see_digit = FALSE;
}
/* Signal to noise test
* AT&T states that the noise must be 16dB down from the signal.
* Here we count the number of signals above the threshold and
* there ought to be only two.
*/
if ( r[max_index] > 1.0e9 )
t = r[max_index] * 0.158;
else
t = r[max_index] * 0.010;
peak_count = 0;
for ( i=0; i<8; i++ )
{
if ( r[i] > t )
peak_count++;
}
if ( peak_count > 2 )
see_digit = FALSE;
if ( see_digit )
{
printf( "%s", row_col_ascii_codes[row][col-4] );
fflush(stdout);
}
}
}
/*----------------------------------------------------------------------------
* goertzel
*----------------------------------------------------------------------------
*/
void goertzel( int sample )
{
double q0;
ui32 i;
sample_count++;
for ( i=0; i<MAX_BINS; i++ )
{
q0 = coefs[i] * q1[i] - q2[i] + sample;
q2[i] = q1[i];
q1[i] = q0;
}
if (sample_count == GOERTZEL_N)
{
for ( i=0; i<MAX_BINS; i++ )
{
r[i] = (q1[i] * q1[i]) + (q2[i] * q2[i]) - (coefs[i] * q1[i] * q2[i]);
q1[i] = 0.0;
q2[i] = 0.0;
}
post_testing();
sample_count = 0;
}
}
到这为止, 你只需要用麦克风把拔电话号码发出的声音录下来, 然后用这段代码一跑, 按了什么号码便立即可以得到了. Goertzel算法同样也可以检测忙音和拔号音.
Goertzel算法还可以用于解码FSK信号.
FSK也称为频移键控, 通俗的讲, 就是两种AB不同的频率组成的信号, 其中令A频率为1令B频率为0, 从而实现传输2进制信息. FSK在传统电话机中也是使用非常广泛.
像FSK这种已知固定的两种频率, 显然比检测DTMF还要简单, FSK只需要检测2个频点, 即可检测出信号内容, 而DTMF需要检测8个频点.
Goertzel算法关于N的取值
N的值需要一定的经验和根据实际情况需要来确定.
影响N取值一般有2个: 1. 采样率, 2. 信号长度.
总之N取值太小可能导致出现重码, 太大会导致检测结果不正确, 我通常是取信号时长的5/4来做N, 但这不一定, 需要多做试验来确定N值.
固定频率检测的另类算法
我曾经还使用过神经网络来检测双音频和FSK, 效果也不错, 运算量略大于 Goertzel, 准确率和 Goertzel 相当, 难点在于训练网络, 所以只是作学习而尝试.