From 3c43650cba84e53f93dd951bec51ae9c7df2759d Mon Sep 17 00:00:00 2001 From: Eric des Courtis Date: Fri, 5 Dec 2008 17:45:37 +0000 Subject: [PATCH] Voicemain detection mod initial upload. By Eric des Courtis git-svn-id: http://svn.freeswitch.org/svn/freeswitch/trunk@10623 d0543943-73ff-0310-b7d9-9358b9ac24b2 --- src/mod/applications/mod_vmd/Makefile | 2 + src/mod/applications/mod_vmd/mod_vmd.cpp | 726 +++++++++++++++++++++++ 2 files changed, 728 insertions(+) create mode 100644 src/mod/applications/mod_vmd/Makefile create mode 100644 src/mod/applications/mod_vmd/mod_vmd.cpp diff --git a/src/mod/applications/mod_vmd/Makefile b/src/mod/applications/mod_vmd/Makefile new file mode 100644 index 0000000000..2c35e6e98f --- /dev/null +++ b/src/mod/applications/mod_vmd/Makefile @@ -0,0 +1,2 @@ +BASE=../../../.. +include $(BASE)/build/modmake.rules diff --git a/src/mod/applications/mod_vmd/mod_vmd.cpp b/src/mod/applications/mod_vmd/mod_vmd.cpp new file mode 100644 index 0000000000..cc9b3420d3 --- /dev/null +++ b/src/mod/applications/mod_vmd/mod_vmd.cpp @@ -0,0 +1,726 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2008, Eric des Courtis + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * Eric des Courtis + * Copyright (C) Benbria. All Rights Reserved. + * + * Contributor(s): + * + * Eric des Courtis + * + * + * mod_vmd.c -- Voicemail Detection Module + * + * This module detects voicemail beeps at any frequency in O(1) time. + * + */ + +#include +#include +#include +#include +#include +#include + +/* Number of points for beep detection */ +#define POINTS 32 + +/* Number of valid points required for beep detection */ +#define VALID 22 + +/* Maximum number of invalid points + * to declare beep has stopped */ +#define MAX_CHIRP 22 + +/* Minimum time for a beep */ +#define MIN_TIME 8000 + +/* Minimum amplitude of the signal */ +#define MIN_AMPL 0.10 + +/* Minimum beep frequency */ +#define MIN_FREQ (600) + +/* Maximum beep frequency */ +#define MAX_FREQ (1100) + +/* PSI function for amplitude calculation*/ +#define PSI(x) (x[1]*x[1]-x[2]*x[0]) + +/* Sample rate */ +#define F (8000) + +/* Conversion of frequency to Hz */ +#define TO_HZ(f) ((F * f) / (2.0 * M_PI)) + +/* Number of points in discreet energy separation */ +#define P (5) + +/* Maximum signed value of int16_t + * DEPRECATED */ +#define ADJUST (32768) +/* Same as above times two + * DEPRECATED */ +#define ADJUST_MAX (65536) + +/* Signed L16 to relative floating point conversion */ +#define CONVERT_PT(d, i, m) do{ d = ((((double)i + (double)m) / \ + (double)(2 * m)) - 0.5) * 2.0; } while(0) + +/* Discreet energy separation tolerance to error */ +#define TOLERANCE (0.20) + +/* Maximum value within tolerance */ +#define TOLERANCE_T(m) (m + (m * TOLERANCE)) + +/* Minimum value within tolerance */ +#define TOLERANCE_B(m) (m - (m * TOLERANCE)) + +/* Syntax of the API call */ +#define VMD_SYNTAX " " + +/* Number of expected parameters in api call */ +#define VMD_PARAMS 2 + +/* FreeSWITCH CUSTOM event type */ +#define VMD_EVENT_BEEP "vmd::beep" + +/* Prototypes */ +SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_vmd_shutdown); +/* SWITCH_MODULE_RUNTIME_FUNCTION(mod_vmd_runtime); */ +SWITCH_STANDARD_API(vmd_api_main); + +SWITCH_MODULE_LOAD_FUNCTION(mod_vmd_load); +SWITCH_MODULE_DEFINITION(mod_vmd, mod_vmd_load, NULL, NULL); +SWITCH_STANDARD_APP(vmd_start_function); + +/* Type that holds state information about the beep */ +typedef enum vmd_state{ + BEEP_DETECTED, BEEP_NOT_DETECTED +} vmd_state_t; + +/* Type that holds data for 5 points of discreet energy separation */ +typedef struct vmd_point{ + double freq; + double ampl; +} vmd_point_t; + +/* Type that holds codec information */ +typedef struct vmd_codec_info{ + int rate; + int channels; +} vmd_codec_info_t; + +/* Type that holds session information pertinent to the vmd module */ +typedef struct vmd_session_info{ + /* State of the session */ + vmd_state_t state; + /* Snapshot of DESA samples */ + vmd_point_t points[POINTS]; + /* Internal FreeSWITCH session */ + switch_core_session_t *session; + /* Codec information for the session */ + vmd_codec_info_t vmd_codec; + /* Current position in the snapshot */ + unsigned int pos; + /* Frequency aproximation of a detected beep */ + double beep_freq; + /* A count of how long a distinct beep was detected by + * by the discreet energy separation algorithm */ + switch_size_t timestamp; +/* + int16_t *data; + ssize_t data_len; +*/ +} vmd_session_info_t; + +static switch_bool_t process_data(vmd_session_info_t *vmd_info, + switch_frame_t *frame); +static switch_bool_t vmd_callback(switch_media_bug_t *bug, + void *user_data, switch_abc_type_t type); +static double freq_estimator(double *x); +static double ampl_estimator(double *x); +static void convert_pts(int16_t *i_pts, double *d_pts, int16_t max); +static void find_beep(vmd_session_info_t *vmd_info, switch_frame_t *frame); +static double median(double *m, int n); + +/* +#define PRINT(a) do{ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, a); }while(0) +#define PRINT2(a, b) do{ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, a, b); }while(0) +*/ + +static switch_bool_t vmd_callback(switch_media_bug_t *bug, + void *user_data, switch_abc_type_t type) +{ + vmd_session_info_t *vmd_info; + switch_codec_t *read_codec; + switch_frame_t *frame; + + vmd_info = (vmd_session_info_t *)user_data; + if(vmd_info == NULL) return SWITCH_FALSE; + + switch(type){ + + case SWITCH_ABC_TYPE_INIT: + read_codec = + switch_core_session_get_read_codec(vmd_info->session); + vmd_info->vmd_codec.rate = + read_codec->implementation->samples_per_second; + vmd_info->vmd_codec.channels = + read_codec->implementation->number_of_channels; + break; + + case SWITCH_ABC_TYPE_CLOSE: + case SWITCH_ABC_TYPE_READ: + case SWITCH_ABC_TYPE_WRITE: + break; + + case SWITCH_ABC_TYPE_READ_REPLACE: + frame = switch_core_media_bug_get_read_replace_frame(bug); + return process_data(vmd_info, frame); + + case SWITCH_ABC_TYPE_WRITE_REPLACE: + break; + } + + return SWITCH_TRUE; +} + +static switch_bool_t process_data(vmd_session_info_t *vmd_info, + switch_frame_t *frame) +{ + int i; + unsigned int j; + double pts[P]; + int16_t *data; + int16_t max; + ssize_t len; + + len = frame->samples * sizeof(int16_t); + data = (int16_t *)frame->data; + + for(max = abs(data[0]), i = 1; i < frame->samples; i++){ + if(abs(data[i]) > max) max = abs(data[i]); + } + +/* + if(vmd_info->data_len != len){ + vmd_info->data_len = len; + if(vmd_info->data != NULL) free(vmd_info->data); + vmd_info->data = (int16_t *)malloc(len); + if(vmd_info->data == NULL) return SWITCH_FALSE; + } + + (void)memcpy(vmd_info->data, data, len); + for(i = 2; i < frame->samples; i++){ + vmd_info->data[i] = + 0.0947997 * data[i] + - + 0.0947997 * data[i - 2] + - + 1.4083405 * vmd_info->data[i - 1] + + + 0.8104005 * vmd_info->data[i - 2]; + } +*/ + + for(i = 0, j = vmd_info->pos; i < frame->samples; j++, j %= POINTS, i += 5){ +/* convert_pts(vmd_info->data + i, pts); */ + convert_pts(data + i, pts, max); + vmd_info->points[j].freq = TO_HZ(freq_estimator(pts)); + vmd_info->points[j].ampl = ampl_estimator(pts); + vmd_info->pos = j % POINTS; + find_beep(vmd_info, frame); + } + + return SWITCH_TRUE; +} + +static void find_beep(vmd_session_info_t *vmd_info, switch_frame_t *frame) +{ + int i; + int c; + double m[POINTS]; + double med; + unsigned int j = (vmd_info->pos + 1) % POINTS; + unsigned int k = j; + switch_status_t status; + switch_event_t *event; + switch_event_t *event_copy; + + switch(vmd_info->state){ + case BEEP_DETECTED: + for(c = 0, i = 0; i < POINTS; j++, j %= POINTS, i++){ + vmd_info->timestamp++; + if(vmd_info->points[j].freq < TOLERANCE_T(vmd_info->beep_freq) && + vmd_info->points[j].freq > TOLERANCE_B(vmd_info->beep_freq)){ + c++; + vmd_info->beep_freq = (vmd_info->beep_freq * 0.95) + (vmd_info->points[j].freq * 0.05); + } + } + + if(c < (POINTS - MAX_CHIRP)){ + vmd_info->state = BEEP_NOT_DETECTED; + if(vmd_info->timestamp < MIN_TIME) break; + + status = switch_event_create_subclass(&event, + SWITCH_EVENT_CUSTOM, VMD_EVENT_BEEP); + if(status != SWITCH_STATUS_SUCCESS) return; + + switch_event_add_header_string( + event, + SWITCH_STACK_BOTTOM, + "Beep-Status", + "stop" + ); + + switch_event_add_header( + event, + SWITCH_STACK_BOTTOM, + "Beep-Time", + "%d", vmd_info->timestamp / POINTS + ); + + switch_event_add_header( + event, + SWITCH_STACK_BOTTOM, + "Unique-ID", + "%s", switch_core_session_get_uuid(vmd_info->session) + ); + + switch_event_add_header( + event, + SWITCH_STACK_BOTTOM, + "Frequency", + "%6.4lf", vmd_info->beep_freq + ); + + switch_event_add_header_string( + event, + SWITCH_STACK_BOTTOM, + "call-command", + "vmd" + ); + + status = switch_event_dup(&event_copy, event); + if(status != SWITCH_STATUS_SUCCESS) return; + + switch_core_session_queue_event(vmd_info->session, &event); + switch_event_fire(&event_copy); + + vmd_info->timestamp = 0; + } + + break; + + case BEEP_NOT_DETECTED: + + for(i = 0; i < POINTS; k++, k %= POINTS, i++){ + m[i] = vmd_info->points[k].freq; + if(m[i] == NAN) m[i] = 0.0; + } + + med = median(m, POINTS); + if(med == NAN){ + for(i = 0; i < POINTS; i++){ + if(m[i] != NAN){ + med = m[i]; + break; + } + } + } + + for(c = 0, i = 0; i < POINTS; j++, j %= POINTS, i++){ + if(vmd_info->points[j].freq < TOLERANCE_T(med) && + vmd_info->points[j].freq > TOLERANCE_B(med)){ + if(vmd_info->points[j].ampl > MIN_AMPL && + vmd_info->points[j].freq > MIN_FREQ && + vmd_info->points[j].freq < MAX_FREQ){ + c++; + } + } + } + + + if(c >= VALID){ + vmd_info->state = BEEP_DETECTED; + vmd_info->beep_freq = med; + vmd_info->timestamp = 0; + } + + break; + } +} + +/* Find the median of an array of doubles */ +static double median(double *m, int n) +{ + int i; + int less; + int greater; + int equal; + double min; + double max; + double guess; + double maxltguess; + double mingtguess; + + min = max = m[0] ; + + for (i = 1; i < n; i++) { + if(m[i] < min) min = m[i]; + if(m[i] > max) max = m[i]; + } + + while(1){ + guess = ( min + max ) / 2; + less = 0; + greater = 0; + equal = 0; + maxltguess = min; + mingtguess = max; + + for(i = 0; i < n; i++) { + if(m[i] < guess){ + less++; + if(m[i] > maxltguess) maxltguess = m[i]; + }else if (m[i] > guess) { + greater++; + if(m[i] < mingtguess) mingtguess = m[i]; + }else equal++; + } + + if (less <= ( n + 1 ) / 2 && greater <= ( n + 1 ) / 2) break; + else if (less > greater) max = maxltguess; + else min = mingtguess; + } + + if(less >= ( n + 1 ) / 2) return maxltguess; + else if(less + equal >= ( n + 1 ) / 2) return guess; + + return mingtguess; +} + +/* Convert many points for Signed L16 to relative floating point */ +static void convert_pts(int16_t *i_pts, double *d_pts, int16_t max) +{ + int i; + for(i = 0; i < P; i++) CONVERT_PT(d_pts[i], i_pts[i], max); +} + +/* Amplitude estimator for DESA-2 */ +double ampl_estimator(double *x) +{ + double freq_sq; + + freq_sq = freq_estimator(x); + freq_sq *= freq_sq; + + return sqrt( PSI(x) / sin(freq_sq) ); +} + +/* The DESA-2 algorithm */ +double freq_estimator(double *x) +{ + return 0.5 * acos( + (((x[2] * x[2]) - (x[0] * x[4])) + - + ( (x[1] * x[1]) - (x[0] * x[2])) + - + ( (x[3] * x[3]) - (x[2] * x[4]))) + / + (2.0 * ((x[2] * x[2]) - (x[1] * x[3]))) + + ); +} + + +SWITCH_MODULE_LOAD_FUNCTION(mod_vmd_load) +{ + switch_application_interface_t *app_interface; + switch_api_interface_t *api_interface; + /* connect my internal structure to the blank pointer passed to me */ + *module_interface = + switch_loadable_module_create_module_interface(pool, modname); + + switch_log_printf( + SWITCH_CHANNEL_LOG, + SWITCH_LOG_NOTICE, + "Voicemail detection enabled\n" + ); + + SWITCH_ADD_APP(app_interface, "vmd", "Detect beeps", "Detect voicemail beeps", + vmd_start_function, "[start] [stop]", SAF_NONE); + + SWITCH_ADD_API(api_interface, "vmd", "Detected voicemail beeps", + vmd_api_main, VMD_SYNTAX); + + /* indicate that the module should continue to be loaded */ + return SWITCH_STATUS_SUCCESS; +} + +/* Same as api function see it for comments */ +SWITCH_STANDARD_APP(vmd_start_function) +{ + switch_media_bug_t *bug; + switch_status_t status; + switch_channel_t *channel; + vmd_session_info_t *vmd_info; + int i; + + if(session == NULL) return; + + channel = switch_core_session_get_channel(session); + + /* Is this channel already set? */ + bug = (switch_media_bug_t *) switch_channel_get_private(channel, "_vmd_"); + /* If yes */ + if(bug != NULL){ + /* If we have a stop remove audio bug */ + if(strcasecmp(data, "stop") == 0){ + switch_channel_set_private(channel, "_vmd_", NULL); + switch_core_media_bug_remove(session, &bug); + return; + } + + /* We have already started */ + switch_log_printf( + SWITCH_CHANNEL_LOG, + SWITCH_LOG_WARNING, + "Cannot run 2 at once on the same channel!\n" + ); + + return; + } + + vmd_info = (vmd_session_info_t *)switch_core_session_alloc( + session, + sizeof(vmd_session_info_t) + ); + + vmd_info->state = BEEP_NOT_DETECTED; + vmd_info->session = session; + vmd_info->pos = 0; +/* + vmd_info->data = NULL; + vmd_info->data_len = 0; +*/ + for(i = 0; i < POINTS; i++){ + vmd_info->points[i].freq = 0.0; + vmd_info->points[i].ampl = 0.0; + } + + status = switch_core_media_bug_add( + session, + vmd_callback, + vmd_info, + 0, + SMBF_READ_REPLACE, + &bug + ); + + if(status != SWITCH_STATUS_SUCCESS){ + switch_log_printf( + SWITCH_CHANNEL_LOG, + SWITCH_LOG_ERROR, + "Failure hooking to stream\n" + ); + + return; + } + + + switch_channel_set_private(channel, "_vmd_", bug); + +} + +/* Called when the system shuts down */ +SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_vmd_shutdown) +{ + switch_log_printf( + SWITCH_CHANNEL_LOG, + SWITCH_LOG_NOTICE, + "Voicemail detection disabled\n" + ); + + return SWITCH_STATUS_SUCCESS; +} + +SWITCH_STANDARD_API(vmd_api_main) +{ + switch_core_session_t *vmd_session; + switch_media_bug_t *bug; + vmd_session_info_t *vmd_info; + switch_channel_t *channel; + switch_event_t *event; + switch_status_t status; + int argc; + char *argv[VMD_PARAMS]; + char *ccmd; + char *uuid; + char *command; + int i; + + /* No command? Display usage */ + if(cmd == NULL){ + stream->write_function(stream, "-USAGE: %s\n", VMD_SYNTAX); + return SWITCH_STATUS_SUCCESS; + } + + /* Duplicated contents of original string */ + ccmd = strdup(cmd); + /* Separate the arguments */ + argc = switch_separate_string(ccmd, ' ', argv, VMD_PARAMS); + + /* If we don't have the expected number of parameters + * display usage */ + if(argc != VMD_PARAMS){ + stream->write_function(stream, "-USAGE: %s\n", VMD_SYNTAX); + switch_safe_free(ccmd); + return SWITCH_STATUS_SUCCESS; + } + + uuid = argv[0]; + command = argv[1]; + + /* using uuid locate a reference to the FreeSWITCH session */ + vmd_session = switch_core_session_locate(uuid); + + /* If the session was not found exit */ + if(vmd_session == NULL){ + switch_safe_free(ccmd); + stream->write_function(stream, "-USAGE: %s\n", VMD_SYNTAX); + return SWITCH_STATUS_FALSE; + } + + /* Get current channel of the session to tag the session + * This indicates that our module is present */ + channel = switch_core_session_get_channel(vmd_session); + + /* Is this channel already set? */ + bug = (switch_media_bug_t *) switch_channel_get_private(channel, "_vmd_"); + /* If yes */ + if(bug != NULL){ + /* If we have a stop remove audio bug */ + if(strcasecmp(command, "stop") == 0){ + switch_channel_set_private(channel, "_vmd_", NULL); + switch_core_media_bug_remove(vmd_session, &bug); + switch_safe_free(ccmd); + stream->write_function(stream, "+OK\n"); + return SWITCH_STATUS_SUCCESS; + } + + /* We have already started */ + switch_log_printf( + SWITCH_CHANNEL_LOG, + SWITCH_LOG_WARNING, + "Cannot run 2 at once on the same channel!\n" + ); + + switch_safe_free(ccmd); + return SWITCH_STATUS_FALSE; + } + + /* If we don't see the expected start exit */ + if(strcasecmp(command, "start") != 0){ + switch_safe_free(ccmd); + stream->write_function(stream, "-USAGE: %s\n", VMD_SYNTAX); + return SWITCH_STATUS_FALSE; + } + + /* Allocate memory attached to this FreeSWITCH session for + * use in the callback routine and to store state information */ + vmd_info = (vmd_session_info_t *)switch_core_session_alloc( + vmd_session, + sizeof(vmd_session_info_t) + ); + + /* Set initial values and states */ + vmd_info->state = BEEP_NOT_DETECTED; + vmd_info->session = vmd_session; + vmd_info->pos = 0; +/* + vmd_info->data = NULL; + vmd_info->data_len = 0; +*/ + + for(i = 0; i < POINTS; i++){ + vmd_info->points[i].freq = 0.0; + vmd_info->points[i].ampl = 0.0; + } + + /* Add a media bug that allows me to intercept the + * reading leg of the audio stream */ + status = switch_core_media_bug_add( + vmd_session, + vmd_callback, + vmd_info, + 0, + SMBF_READ_REPLACE, + &bug + ); + + /* If adding a media bug fails exit */ + if(status != SWITCH_STATUS_SUCCESS){ + switch_log_printf( + SWITCH_CHANNEL_LOG, + SWITCH_LOG_ERROR, + "Failure hooking to stream\n" + ); + + switch_safe_free(ccmd); + return SWITCH_STATUS_FALSE; + } + + /* Set the vmd tag to detect an existing vmd media bug */ + switch_channel_set_private(channel, "_vmd_", bug); + + /* Everything went according to plan! Notify the user */ + stream->write_function(stream, "+OK\n"); + + switch_safe_free(ccmd); + return SWITCH_STATUS_SUCCESS; +} + +/* + If it exists, this is called in it's own thread + when the module-load completes. If it returns anything + but SWITCH_STATUS_TERM it will be called again automatically +*/ + +/* +SWITCH_MODULE_RUNTIME_FUNCTION(mod_vmd_runtime) +{ + while(looping){ + + switch_yield(1000); + } + + return SWITCH_STATUS_TERM; +} +*/ + +/* For Emacs: + * Local Variables: + * mode:c + * indent-tabs-mode:t + * tab-width:4 + * c-basic-offset:4 + * End: + * For VIM: + * vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab: + */ +