diff --git a/doc/CHANGES-staging/tonescan.txt b/doc/CHANGES-staging/tonescan.txt new file mode 100644 index 0000000000000000000000000000000000000000..cbed34fa099c57dd4f243dacd614ad54c422e2d2 --- /dev/null +++ b/doc/CHANGES-staging/tonescan.txt @@ -0,0 +1,6 @@ +Subject: ToneScan application + +A new application, ToneScan, allows for +synchronous detection of call progress +signals such as dial tone, busy tone, +Special Information Tones, and modems. diff --git a/res/res_tonedetect.c b/res/res_tonedetect.c index 1d5db8334470a27596ab5ae287aef79f2013b978..b65a69b9774c03a812cd5e55e7028989efe2ac1e 100644 --- a/res/res_tonedetect.c +++ b/res/res_tonedetect.c @@ -93,13 +93,90 @@ <ref type="application">PlayTones</ref> </see-also> </application> + <application name="ToneScan" language="en_US"> + <synopsis> + Wait for period of time while scanning for call progress tones + </synopsis> + <syntax> + <parameter name="zone" required="false"> + <para>Call progress zone. Default is the system default.</para> + </parameter> + <parameter name="timeout" required="false"> + <para>Maximum amount of time, in seconds, to wait for call progress + or signal tones. Default is forever.</para> + </parameter> + <parameter name="threshold" required="false"> + <para>DSP threshold required for a match. A higher number will + require a longer match and may reduce false positives, at the + expense of false negatives. Default is 1.</para> + </parameter> + <parameter name="options" required="false"> + <optionlist> + <option name="f"> + <para>Enable fax machine detection. By default, this is disabled.</para> + </option> + <option name="v"> + <para>Enable voice detection. By default, this is disabled.</para> + </option> + </optionlist> + </parameter> + </syntax> + <description> + <para>Waits for a a distinguishable call progress tone and then exits. + Unlike a conventional scanner, this is not currently capable of + scanning for modem carriers.</para> + <variablelist> + <variable name="TONESCANSTATUS"> + This indicates the result of the scan. + <value name="RINGING"> + Audible ringback tone + </value> + <value name="BUSY"> + Busy tone + </value> + <value name="SIT"> + Special Information Tones + </value> + <value name="VOICE"> + Human voice detected + </value> + <value name="DTMF"> + DTMF digit + </value> + <value name="FAX"> + Fax (answering) + </value> + <value name="MODEM"> + Modem (answering) + </value> + <value name="DIALTONE"> + Dial tone + </value> + <value name="NUT"> + UK Number Unobtainable tone + </value> + <value name="TIMEOUT"> + Timeout reached before any positive detection + </value> + <value name="HANGUP"> + Caller hung up before any positive detection + </value> + </variable> + </variablelist> + </description> + <see-also> + <ref type="application">WaitForTone</ref> + </see-also> + </application> <function name="TONE_DETECT" language="en_US"> <synopsis> Asynchronously detects a tone </synopsis> <syntax> <parameter name="freq" required="true"> - <para>Frequency of the tone to detect.</para> + <para>Frequency of the tone to detect. To disable frequency + detection completely (e.g. for signal detection only), + specify 0 for the frequency.</para> </parameter> <parameter name="duration_ms" required="false"> <para>Minimum duration of tone, in ms. Default is 500ms. @@ -108,6 +185,18 @@ </parameter> <parameter name="options"> <optionlist> + <option name="a"> + <para>Match immediately on Special Information Tones, instead of or in addition + to a particular frequency.</para> + </option> + <option name="b"> + <para>Match immediately on a busy signal, instead of or in addition to + a particular frequency.</para> + </option> + <option name="c"> + <para>Match immediately on a dial tone, instead of or in addition to + a particular frequency.</para> + </option> <option name="d"> <para>Custom decibel threshold to use. Default is 16.</para> </option> @@ -147,10 +236,18 @@ <literal>rx</literal> to get the number of times a tone has been detected in the RX direction.</para> <example title="intercept2600"> - same => n,Set(TONE_DETECT(2600,1000,g(got-2600,s,1))=) + same => n,Set(TONE_DETECT(2600,1000,g(got-2600,s,1))=) ; detect 2600 Hz same => n,Wait(15) same => n,NoOp(${TONE_DETECT(rx)}) </example> + <example title="dropondialtone"> + same => n,Set(TONE_DETECT(0,,bg(my-hangup,s,1))=) ; disconnect a call if we hear a busy signal + same => n,Goto(somewhere-else) + same => n(myhangup),Hangup() + </example> + <example title="removedetector"> + same => n,Set(TONE_DETECT(0,,x)=) ; remove the detector from the channel + </example> </description> </function> ***/ @@ -170,6 +267,7 @@ struct detect_information { int txcount; int rxcount; int hitsrequired; + int signalfeatures; }; enum td_opts { @@ -181,6 +279,9 @@ enum td_opts { OPT_DECIBEL = (1 << 6), OPT_SQUELCH = (1 << 7), OPT_HITS_REQ = (1 << 8), + OPT_SIT = (1 << 9), + OPT_BUSY = (1 << 10), + OPT_DIALTONE = (1 << 11), }; enum { @@ -193,6 +294,9 @@ enum { }; AST_APP_OPTIONS(td_opts, { + AST_APP_OPTION('a', OPT_SIT), + AST_APP_OPTION('b', OPT_BUSY), + AST_APP_OPTION('c', OPT_DIALTONE), AST_APP_OPTION_ARG('d', OPT_DECIBEL, OPT_ARG_DECIBEL), AST_APP_OPTION_ARG('g', OPT_GOTO_RX, OPT_ARG_GOTO_RX), AST_APP_OPTION_ARG('h', OPT_GOTO_TX, OPT_ARG_GOTO_TX), @@ -230,6 +334,7 @@ static int detect_callback(struct ast_audiohook *audiohook, struct ast_channel * { struct ast_datastore *datastore = NULL; struct detect_information *di = NULL; + int match = 0; /* If the audiohook is stopping it means the channel is shutting down.... but we let the datastore destroy take care of it */ if (audiohook->status == AST_AUDIOHOOK_STATUS_DONE) { @@ -258,6 +363,7 @@ static int detect_callback(struct ast_audiohook *audiohook, struct ast_channel * char result = frame->subclass.integer; if (result == 'q') { int now; + match = 1; if (direction == AST_AUDIOHOOK_DIRECTION_READ) { di->rxcount = di->rxcount + 1; now = di->rxcount; @@ -275,6 +381,42 @@ static int detect_callback(struct ast_audiohook *audiohook, struct ast_channel * } } } + if (di->signalfeatures && !match) { /* skip unless there are call progress/signal options */ + int tstate, tcount; + tcount = ast_dsp_get_tcount(di->dsp); + tstate = ast_dsp_get_tstate(di->dsp); + if (tstate > 0) { + ast_debug(3, "tcount: %d, tstate: %d\n", tcount, tstate); + switch (tstate) { + case DSP_TONE_STATE_DIALTONE: + if (di->signalfeatures & DSP_FEATURE_WAITDIALTONE) { + match = 1; + } + break; + case DSP_TONE_STATE_BUSY: + if (di->signalfeatures & DSP_PROGRESS_BUSY) { + match = 1; + } + break; + case DSP_TONE_STATE_SPECIAL3: + if (di->signalfeatures & DSP_PROGRESS_CONGESTION) { + match = 1; + } + break; + default: /* ignore */ + break; + } + if (match) { + if (direction == AST_AUDIOHOOK_DIRECTION_READ && di->gotorx) { + ast_async_parseable_goto(chan, di->gotorx); + } else if (di->gototx) { + ast_async_parseable_goto(chan, di->gototx); + } else { + ast_debug(3, "Detected call progress signal, but don't know where to go\n"); + } + } + } + } /* this could be the duplicated frame or a new one, doesn't matter */ ast_frfree(frame); return 0; @@ -326,8 +468,8 @@ static int freq_parser(char *freqs, int *freq1, int *freq2) { ast_log(LOG_WARNING, "Frequency must be an integer: %s\n", f1); return -1; } - if (*freq1 < 1) { - ast_log(LOG_WARNING, "Sorry, positive frequencies only: %d\n", *freq1); + if (*freq1 < 0) { + ast_log(LOG_WARNING, "Sorry, no negative frequencies: %d\n", *freq1); return -1; } if (!ast_strlen_zero(f2)) { @@ -413,6 +555,23 @@ static int detect_read(struct ast_channel *chan, const char *cmd, char *data, ch return 0; } +static int parse_signal_features(struct ast_flags *flags) +{ + int features = 0; + + if (ast_test_flag(flags, OPT_SIT)) { + features |= DSP_PROGRESS_CONGESTION; + } + if (ast_test_flag(flags, OPT_BUSY)) { + features |= DSP_PROGRESS_BUSY; + } + if (ast_test_flag(flags, OPT_DIALTONE)) { + features |= DSP_FEATURE_WAITDIALTONE; + } + + return features; +} + static int detect_write(struct ast_channel *chan, const char *cmd, char *data, const char *value) { char *parse; @@ -422,6 +581,7 @@ static int detect_write(struct ast_channel *chan, const char *cmd, char *data, c char *opt_args[OPT_ARG_ARRAY_SIZE]; struct ast_dsp *dsp; int freq1 = 0, freq2 = 0, duration = 500, db = 16, squelch = 0, hitsrequired = 1; + int signalfeatures = 0; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(freqs); @@ -461,6 +621,7 @@ static int detect_write(struct ast_channel *chan, const char *cmd, char *data, c return -1; } } + signalfeatures = parse_signal_features(&flags); ast_channel_lock(chan); if (!(datastore = ast_channel_datastore_find(chan, &detect_datastore, NULL))) { @@ -481,8 +642,12 @@ static int detect_write(struct ast_channel *chan, const char *cmd, char *data, c ast_log(LOG_WARNING, "Unable to allocate DSP!\n"); return -1; } - ast_dsp_set_features(dsp, DSP_FEATURE_FREQ_DETECT); - ast_dsp_set_freqmode(dsp, freq1, duration, db, squelch); + di->signalfeatures = signalfeatures; /* we're not including freq detect */ + if (freq1 > 0) { + signalfeatures |= DSP_FEATURE_FREQ_DETECT; + ast_dsp_set_freqmode(dsp, freq1, duration, db, squelch); + } + ast_dsp_set_features(dsp, signalfeatures); di->dsp = dsp; di->txcount = 0; di->rxcount = 0; @@ -493,7 +658,12 @@ static int detect_write(struct ast_channel *chan, const char *cmd, char *data, c } else { di = datastore->data; dsp = di->dsp; - ast_dsp_set_freqmode(dsp, freq1, duration, db, squelch); + di->signalfeatures = signalfeatures; /* we're not including freq detect */ + if (freq1 > 0) { + signalfeatures |= DSP_FEATURE_FREQ_DETECT; + ast_dsp_set_freqmode(dsp, freq1, duration, db, squelch); + } + ast_dsp_set_features(dsp, signalfeatures); } di->duration = duration; di->gotorx = NULL; @@ -641,6 +811,185 @@ static int wait_exec(struct ast_channel *chan, const char *data) } static char *waitapp = "WaitForTone"; +static char *scanapp = "ToneScan"; + +static int scan_exec(struct ast_channel *chan, const char *data) +{ + char *appdata; + double timeoutf = 0; + int timeout = 0; + struct ast_frame *frame = NULL, *frame2 = NULL; + struct ast_dsp *dsp = NULL, *dsp2 = NULL; + struct timeval start; + int remaining_time = 0; + int features, match = 0, fax = 0, voice = 0, threshold = 1; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(zone); + AST_APP_ARG(timeout); + AST_APP_ARG(threshold); + AST_APP_ARG(options); + ); + + appdata = ast_strdupa(data); + AST_STANDARD_APP_ARGS(args, appdata); + + if (!ast_strlen_zero(args.timeout) && (sscanf(args.timeout, "%30lf", &timeoutf) != 1 || timeout < 0)) { + ast_log(LOG_WARNING, "Invalid timeout: %s\n", args.timeout); + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "ERROR"); + return -1; + } + if (!ast_strlen_zero(args.threshold) && (ast_str_to_int(args.threshold, &threshold) || threshold < 1)) { + ast_log(LOG_WARNING, "Invalid threshold: %s\n", args.threshold); + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "ERROR"); + return -1; + } + timeout = 1000 * timeoutf; + + if (!ast_strlen_zero(args.options) && strchr(args.options, 'f')) { + fax = 1; + } + if (!ast_strlen_zero(args.options) && strchr(args.options, 'v')) { + voice = 1; + } + + if (!(dsp = ast_dsp_new())) { + ast_log(LOG_WARNING, "Unable to allocate DSP!\n"); + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "ERROR"); + return -1; + } + + if (!ast_strlen_zero(args.zone)) { + if (ast_dsp_set_call_progress_zone(dsp, args.zone)) { + ast_log(LOG_WARNING, "Invalid call progress zone: %s\n", args.zone); + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "ERROR"); + ast_dsp_free(dsp); + return -1; + } + } + + if (fax) { + if (!(dsp2 = ast_dsp_new())) { + ast_dsp_free(dsp); + ast_log(LOG_WARNING, "Unable to allocate DSP!\n"); + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "ERROR"); + return -1; + } + } + + features = DSP_PROGRESS_RINGING; /* audible ringback tone */ + features |= DSP_PROGRESS_BUSY; /* busy signal */ + features |= DSP_PROGRESS_CONGESTION; /* SIT tones (not reorder!) */ + features |= DSP_PROGRESS_TALK; /* voice. */ + features |= DSP_FEATURE_WAITDIALTONE; /* dial tone */ + features |= DSP_FEATURE_FREQ_DETECT; /* modem answer */ + if (voice) { + features |= DSP_TONE_STATE_TALKING; /* voice */ + } + ast_dsp_set_features(dsp, features); + /* all modems begin negotiating with Bell 103. An answering modem just sends mark tone, or 2225 Hz */ + ast_dsp_set_freqmode(dsp, 2225, 400, 16, 0); /* this needs to be pretty short, or the progress tones code will thing this is voice */ + + if (fax) { /* fax detect uses same tone detect internals as modem and causes things to not work as intended, so use a separate DSP if needed. */ + ast_dsp_set_features(dsp2, DSP_FEATURE_FAX_DETECT); /* fax tone */ + ast_dsp_set_faxmode(dsp2, DSP_FAXMODE_DETECT_CED); /* we only care about the answering side (CED), not originating (CNG) */ + } + + ast_debug(1, "Starting tone scan, timeout: %d ms, threshold: %d\n", timeout, threshold); + start = ast_tvnow(); + do { + if (timeout > 0) { + remaining_time = ast_remaining_ms(start, timeout); + if (remaining_time <= 0) { + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "TIMEOUT"); + break; + } + } + if (ast_waitfor(chan, 1000) > 0) { + if (!(frame = ast_read(chan))) { + ast_debug(1, "Channel '%s' did not return a frame; probably hung up.\n", ast_channel_name(chan)); + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "HANGUP"); + break; + } else if (frame->frametype == AST_FRAME_VOICE) { + if (fax) { + frame2 = ast_frdup(frame); + } + frame = ast_dsp_process(chan, dsp, frame); + if (frame->frametype == AST_FRAME_DTMF) { + char result = frame->subclass.integer; + match = 1; + if (result == 'q') { + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "MODEM"); + } else { + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "DTMF"); + } + } else if (fax) { + char result; + frame2 = ast_dsp_process(chan, dsp2, frame2); + result = frame->subclass.integer; + if (result == AST_FRAME_DTMF) { + if (result == 'e') { + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "FAX"); + match = 1; + } else { + ast_debug(1, "Ignoring inactionable event\n"); /* shouldn't happen */ + } + } + ast_frfree(frame2); + } + if (!match) { + int tstate, tcount; + tcount = ast_dsp_get_tcount(dsp); + tstate = ast_dsp_get_tstate(dsp); + if (tstate > 0) { + ast_debug(3, "tcount: %d, tstate: %d\n", tcount, tstate); + if (tcount >= threshold) { + match = 1; + switch (tstate) { + case DSP_TONE_STATE_RINGING: + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "RINGING"); + break; + case DSP_TONE_STATE_DIALTONE: + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "DIALTONE"); + break; + case DSP_TONE_STATE_TALKING: + /* even if we don't specify this feature, it's still checked, so we always need to handle it. + Even if we are looking for it, we need to wait a while or tones will be interpreted + as voice, because this will match first (and this should match last). */ + if (voice && tcount > 15 && tcount >= threshold) { + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "VOICE"); + } else { + match = 0; + } + break; + case DSP_TONE_STATE_BUSY: + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "BUSY"); + break; + case DSP_TONE_STATE_SPECIAL3: + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "SIT"); + break; + case DSP_TONE_STATE_HUNGUP: /* UK only */ + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "NUT"); + break; + default: + match = 0; + ast_debug(1, "Something else we weren't expecting? tstate: %d, #%d\n", tstate, tcount); + } + } + } + } + } + ast_frfree(frame); + } else { + pbx_builtin_setvar_helper(chan, "TONESCANSTATUS", "HANGUP"); + } + } while (!match && (timeout == 0 || remaining_time > 0)); + ast_dsp_free(dsp); + if (dsp2) { + ast_dsp_free(dsp2); + } + + return 0; +} static struct ast_custom_function detect_function = { .name = "TONE_DETECT", @@ -653,6 +1002,7 @@ static int unload_module(void) int res; res = ast_unregister_application(waitapp); + res |= ast_unregister_application(scanapp); res |= ast_custom_function_unregister(&detect_function); return res; @@ -663,6 +1013,7 @@ static int load_module(void) int res; res = ast_register_application_xml(waitapp, wait_exec); + res |= ast_register_application_xml(scanapp, scan_exec); res |= ast_custom_function_register(&detect_function); return res;