--- /dev/null
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2010, Eric des Courtis <eric.des.courtis@benbria.com>
+ *
+ * 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 <eric.des.courtis@benbria.com>
+ * Copyright (C) Benbria. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Eric des Courtis <eric.des.courtis@benbria.com>
+ *
+ * mod_avmd.c -- Advanced Voicemail Detection Module
+ *
+ * This module detects voicemail beeps using a generalized approach.
+ *
+ */
+
+#include <switch.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#ifdef WIN32
+#include <float.h>
+#define ISNAN(x) (!!(_isnan(x)))
+#else
+#define ISNAN(x) (isnan(x))
+#endif
+
+/*! Calculate how many audio samples per ms based on the rate */
+#define SAMPLES_PER_MS(r, m) ((r) / (1000/(m)))
+/*! Minimum beep length */
+#define BEEP_TIME (100)
+/*! How often to evaluate the output of desa2 in ms */
+#define SINE_TIME (10)
+/*! How long in samples does desa2 results get evaluated */
+#define SINE_LEN(r) SAMPLES_PER_MS((r), SINE_TIME)
+/*! How long in samples is the minimum beep length */
+#define BEEP_LEN(r) SAMPLES_PER_MS((r), BEEP_TIME)
+/*! Number of points in desa2 sample */
+#define P (5)
+/*! Guesstimate frame length in ms */
+#define FRAME_TIME (20)
+/*! Length in samples of the frame (guesstimate) */
+#define FRAME_LEN(r) SAMPLES_PER_MS((r), FRAME_TIME)
+/*! Conversion to Hertz */
+#define TO_HZ(r, f) (((r) * (f)) / (2.0 * M_PI))
+/*! Minimum beep frequency in Hertz */
+#define MIN_FREQUENCY (300.0)
+/*! Maximum beep frequency in Hertz */
+#define MAX_FREQUENCY (1500.0)
+
+#include "amplitude.h"
+#include "buffer.h"
+#include "desa2.h"
+#include "goertzel.h"
+#include "psi.h"
+#include "sma_buf.h"
+#include "options.h"
+
+#ifdef FASTMATH
+#include "fast_acosf.h"
+#endif
+
+/*! Syntax of the API call. */
+#define AVMD_SYNTAX "<uuid> <command>"
+
+/*! Number of expected parameters in api call. */
+#define AVMD_PARAMS 2
+
+/*! FreeSWITCH CUSTOM event type. */
+#define AVMD_EVENT_BEEP "avmd::beep"
+
+
+/* Prototypes */
+SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_avmd_shutdown);
+SWITCH_STANDARD_API(avmd_api_main);
+
+SWITCH_MODULE_LOAD_FUNCTION(mod_avmd_load);
+SWITCH_MODULE_DEFINITION(mod_avmd, mod_avmd_load, NULL, NULL);
+SWITCH_STANDARD_APP(avmd_start_function);
+
+/*! Status of the beep detection */
+typedef enum {
+ BEEP_DETECTED,
+ BEEP_NOTDETECTED
+} avmd_beep_state_t;
+
+/*! Data related to the current status of the beep */
+typedef struct {
+ avmd_beep_state_t beep_state;
+ size_t last_beep;
+} avmd_state_t;
+
+/*! Type that holds session information pertinent to the avmd module. */
+typedef struct {
+ /*! Internal FreeSWITCH session. */
+ switch_core_session_t *session;
+ uint32_t rate;
+ circ_buffer_t b;
+ sma_buffer_t sma_b;
+ size_t pos;
+ /* freq_table_t ft; */
+ avmd_state_t state;
+} avmd_session_t;
+
+static void avmd_process(avmd_session_t *session, switch_frame_t *frame);
+static switch_bool_t avmd_callback(switch_media_bug_t * bug, void *user_data, switch_abc_type_t type);
+static void init_avmd_session_data(avmd_session_t *avmd_session, switch_core_session_t *fs_session);
+
+
+/*! \brief The avmd session data initialization function
+ * @author Eric des Courtis
+ * @param avmd_session A reference to a avmd session
+ * @param fs_session A reference to a FreeSWITCH session
+ */
+static void init_avmd_session_data(avmd_session_t *avmd_session, switch_core_session_t *fs_session)
+{
+ /*! This is a worst case sample rate estimate */
+ avmd_session->rate = 48000;
+ INIT_CIRC_BUFFER(&avmd_session->b, BEEP_LEN(avmd_session->rate), FRAME_LEN(avmd_session->rate));
+
+ avmd_session->session = fs_session;
+ avmd_session->pos = 0;
+ avmd_session->state.last_beep = 0;
+ avmd_session->state.beep_state = BEEP_NOTDETECTED;
+
+ INIT_SMA_BUFFER(
+ &avmd_session->sma_b,
+ BEEP_LEN(avmd_session->rate) / SINE_LEN(avmd_session->rate),
+ fs_session
+ );
+}
+
+
+/*! \brief The callback function that is called when new audio data becomes available
+ *
+ * @author Eric des Courtis
+ * @param bug A reference to the media bug.
+ * @param user_data The session information for this call.
+ * @param type The switch callback type.
+ * @return The success or failure of the function.
+ */
+static switch_bool_t avmd_callback(switch_media_bug_t * bug, void *user_data, switch_abc_type_t type)
+{
+ avmd_session_t *avmd_session;
+ switch_codec_t *read_codec;
+ switch_frame_t *frame;
+
+
+ avmd_session = (avmd_session_t *) user_data;
+ if (avmd_session == NULL) {
+ return SWITCH_FALSE;
+ }
+
+ switch (type) {
+
+ case SWITCH_ABC_TYPE_INIT:
+ read_codec = switch_core_session_get_read_codec(avmd_session->session);
+ avmd_session->rate = read_codec->implementation->samples_per_second;
+ /* avmd_session->vmd_codec.channels = read_codec->implementation->number_of_channels; */
+ break;
+
+ case SWITCH_ABC_TYPE_READ_PING:
+ break;
+ case SWITCH_ABC_TYPE_CLOSE:
+ break;
+ case SWITCH_ABC_TYPE_READ:
+ break;
+ case SWITCH_ABC_TYPE_WRITE:
+ break;
+
+ case SWITCH_ABC_TYPE_READ_REPLACE:
+ frame = switch_core_media_bug_get_read_replace_frame(bug);
+ avmd_process(avmd_session, frame);
+ return SWITCH_TRUE;
+
+ case SWITCH_ABC_TYPE_WRITE_REPLACE:
+ break;
+ }
+
+ return SWITCH_TRUE;
+}
+
+/*! \brief FreeSWITCH module loading function
+ *
+ * @author Eric des Courtis
+ * @return Load success or failure.
+ */
+SWITCH_MODULE_LOAD_FUNCTION(mod_avmd_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,
+ "Advanced Voicemail detection enabled\n"
+ );
+
+#ifdef FASTMATH
+ init_fast_acosf();
+ switch_log_printf(
+ SWITCH_CHANNEL_LOG,
+ SWITCH_LOG_NOTICE,
+ "Advanced Voicemail detection: fast math enabled\n"
+ );
+#endif
+
+ SWITCH_ADD_APP(
+ app_interface,
+ "avmd",
+ "Beep detection",
+ "Advanced detection of voicemail beeps",
+ avmd_start_function,
+ "[start] [stop]",
+ SAF_NONE
+ );
+
+ SWITCH_ADD_API(api_interface, "avmd", "Voicemail beep detection", avmd_api_main, AVMD_SYNTAX);
+
+ /* indicate that the module should continue to be loaded */
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/*! \brief FreeSWITCH application handler function.
+ * This handles calls made from applications such as LUA and the dialplan
+ *
+ * @author Eric des Courtis
+ * @return Success or failure of the function.
+ */
+SWITCH_STANDARD_APP(avmd_start_function)
+{
+ switch_media_bug_t *bug;
+ switch_status_t status;
+ switch_channel_t *channel;
+ avmd_session_t *avmd_session;
+
+ if (session == NULL)
+ return;
+
+ channel = switch_core_session_get_channel(session);
+
+ /* Is this channel already using avmd ? */
+ bug = (switch_media_bug_t *) switch_channel_get_private(channel, "_avmd_");
+ /* If it is using avmd */
+ if (bug != NULL) {
+ /* If we have a stop remove audio bug */
+ if (strcasecmp(data, "stop") == 0) {
+ switch_channel_set_private(channel, "_avmd_", NULL);
+ switch_core_media_bug_remove(session, &bug);
+ return;
+ }
+
+ /* We have already started */
+ switch_log_printf(
+ SWITCH_CHANNEL_SESSION_LOG(session),
+ SWITCH_LOG_WARNING,
+ "Cannot run 2 at once on the same channel!\n"
+ );
+
+ return;
+ }
+
+ avmd_session = (avmd_session_t *)switch_core_session_alloc(session, sizeof(avmd_session_t));
+
+ init_avmd_session_data(avmd_session, session);
+
+ status = switch_core_media_bug_add(
+ session,
+ "avmd",
+ NULL,
+ avmd_callback,
+ avmd_session,
+ 0,
+ SMBF_READ_REPLACE,
+ &bug
+ );
+
+ if (status != SWITCH_STATUS_SUCCESS) {
+ switch_log_printf(
+ SWITCH_CHANNEL_SESSION_LOG(session),
+ SWITCH_LOG_ERROR,
+ "Failure hooking to stream\n"
+ );
+
+ return;
+ }
+
+ switch_channel_set_private(channel, "_avmd_", bug);
+}
+
+/*! \brief Called when the module shuts down
+ *
+ * @author Eric des Courtis
+ * @return The success or failure of the function.
+ */
+SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_avmd_shutdown)
+{
+
+#ifdef FASTMATH
+ destroy_fast_acosf();
+#endif
+
+ switch_log_printf(
+ SWITCH_CHANNEL_LOG,
+ SWITCH_LOG_NOTICE,
+ "Advanced Voicemail detection disabled\n"
+ );
+
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/*! \brief FreeSWITCH API handler function.
+ * This function handles API calls such as the ones from mod_event_socket and in some cases
+ * scripts such as LUA scripts.
+ *
+ * @author Eric des Courtis
+ * @return The success or failure of the function.
+ */
+SWITCH_STANDARD_API(avmd_api_main)
+{
+ switch_core_session_t *fs_session = NULL;
+ switch_media_bug_t *bug;
+ avmd_session_t *avmd_session;
+ switch_channel_t *channel;
+ switch_status_t status;
+ int argc;
+ char *argv[AVMD_PARAMS];
+ char *ccmd = NULL;
+ char *uuid;
+ char *command;
+
+ /* No command? Display usage */
+ if (zstr(cmd)) {
+ stream->write_function(stream, "-USAGE: %s\n", AVMD_SYNTAX);
+ return SWITCH_STATUS_SUCCESS;
+ }
+
+ /* Duplicated contents of original string */
+ ccmd = strdup(cmd);
+ /* Separate the arguments */
+ argc = switch_separate_string(ccmd, ' ', argv, AVMD_PARAMS);
+
+ /* If we don't have the expected number of parameters
+ * display usage */
+ if (argc != AVMD_PARAMS) {
+ stream->write_function(stream, "-USAGE: %s\n", AVMD_SYNTAX);
+ goto end;
+ }
+
+ uuid = argv[0];
+ command = argv[1];
+
+ /* using uuid locate a reference to the FreeSWITCH session */
+ fs_session = switch_core_session_locate(uuid);
+
+ /* If the session was not found exit */
+ if (fs_session == NULL) {
+ stream->write_function(stream, "-USAGE: %s\n", AVMD_SYNTAX);
+ goto end;
+ }
+
+ /* Get current channel of the session to tag the session
+ * This indicates that our module is present */
+ channel = switch_core_session_get_channel(fs_session);
+
+ /* Is this channel already set? */
+ bug = (switch_media_bug_t *) switch_channel_get_private(channel, "_avmd_");
+ /* If yes */
+ if (bug != NULL) {
+ /* If we have a stop remove audio bug */
+ if (strcasecmp(command, "stop") == 0) {
+ switch_channel_set_private(channel, "_avmd_", NULL);
+ switch_core_media_bug_remove(fs_session, &bug);
+ switch_safe_free(ccmd);
+ stream->write_function(stream, "+OK\n");
+ goto end;
+ }
+
+ /* We have already started */
+ switch_log_printf(
+ SWITCH_CHANNEL_SESSION_LOG(session),
+ SWITCH_LOG_WARNING,
+ "Cannot run 2 at once on the same channel!\n"
+ );
+
+ goto end;
+ }
+
+ /* If we don't see the expected start exit */
+ if (strcasecmp(command, "start") != 0) {
+ stream->write_function(stream, "-USAGE: %s\n", AVMD_SYNTAX);
+ goto end;
+ }
+
+ /* Allocate memory attached to this FreeSWITCH session for
+ * use in the callback routine and to store state information */
+ avmd_session = (avmd_session_t *) switch_core_session_alloc(fs_session, sizeof(avmd_session_t));
+
+ init_avmd_session_data(avmd_session, fs_session);
+
+ /* Add a media bug that allows me to intercept the
+ * reading leg of the audio stream */
+ status = switch_core_media_bug_add(
+ fs_session,
+ "avmd",
+ NULL,
+ avmd_callback,
+ avmd_session,
+ 0,
+ SMBF_READ_REPLACE,
+ &bug
+ );
+
+ /* If adding a media bug fails exit */
+ if (status != SWITCH_STATUS_SUCCESS) {
+
+ switch_log_printf(
+ SWITCH_CHANNEL_SESSION_LOG(session),
+ SWITCH_LOG_ERROR,
+ "Failure hooking to stream\n"
+ );
+
+ goto end;
+ }
+
+ /* Set the vmd tag to detect an existing vmd media bug */
+ switch_channel_set_private(channel, "_avmd_", bug);
+
+ /* Everything went according to plan! Notify the user */
+ stream->write_function(stream, "+OK\n");
+
+
+end:
+
+ if (fs_session) {
+ switch_core_session_rwunlock(fs_session);
+ }
+
+ switch_safe_free(ccmd);
+
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/*! \brief Process one frame of data with avmd algorithm
+ * @author Eric des Courtis
+ * @param session An avmd session
+ * @param frame A audio frame
+ */
+static void avmd_process(avmd_session_t *session, switch_frame_t *frame)
+{
+ switch_event_t *event;
+ switch_status_t status;
+ switch_event_t *event_copy;
+ switch_channel_t *channel;
+
+ circ_buffer_t *b;
+ size_t pos;
+ double f;
+ double a;
+ double error = 0.0;
+ double success = 0.0;
+ double amp = 0.0;
+ double s_rate;
+ double e_rate;
+ double avg_a;
+ double sine_len;
+ uint32_t sine_len_i;
+ int valid;
+
+ b = &session->b;
+
+ /*! If beep has already been detected skip the CPU heavy stuff */
+ if(session->state.beep_state == BEEP_DETECTED){
+ return;
+ }
+
+ /*! Precompute values used heavily in the inner loop */
+ sine_len_i = SINE_LEN(session->rate);
+ sine_len = (double)sine_len_i;
+
+
+ channel = switch_core_session_get_channel(session->session);
+
+ /*! Insert frame of 16 bit samples into buffer */
+ INSERT_INT16_FRAME(b, (int16_t *)(frame->data), frame->samples);
+
+ /*! INNER LOOP -- OPTIMIZATION TARGET */
+ for(pos = GET_BACKLOG_POS(b); pos != (GET_CURRENT_POS(b) - P); pos++){
+
+ /*! Get a desa2 frequency estimate in Hertz */
+ f = TO_HZ(session->rate, desa2(b, pos));
+
+ /*! Don't caculate amplitude if frequency is not within range */
+ if(f < MIN_FREQUENCY || f > MAX_FREQUENCY) {
+ a = 0.0;
+ error += 1.0;
+ } else {
+ a = amplitude(b, pos, f);
+ success += 1.0;
+ if(!ISNAN(a)){
+ amp += a;
+ }
+ }
+
+ /*! Every once in a while we evaluate the desa2 and amplitude results */
+ if(((pos + 1) % sine_len_i) == 0){
+ s_rate = success / (error + success);
+ e_rate = error / (error + success);
+ avg_a = amp / sine_len;
+
+ /*! Results out of these ranges are considered invalid */
+ valid = 0;
+ if( s_rate > 0.60 && avg_a > 0.50) valid = 1;
+ else if(s_rate > 0.65 && avg_a > 0.45) valid = 1;
+ else if(s_rate > 0.70 && avg_a > 0.40) valid = 1;
+ else if(s_rate > 0.80 && avg_a > 0.30) valid = 1;
+ else if(s_rate > 0.95 && avg_a > 0.05) valid = 1;
+ else if(s_rate >= 0.99 && avg_a > 0.04) valid = 1;
+ else if(s_rate == 1.00 && avg_a > 0.02) valid = 1;
+
+ if(valid) APPEND_SMA_VAL(&session->sma_b, s_rate * avg_a);
+ else APPEND_SMA_VAL(&session->sma_b, 0.0 );
+
+ /*! If sma is higher then 0 we have some kind of detection (increase this value to eliminate false positives ex: 0.01) */
+ if(session->sma_b.sma > 0.00){
+ /*! Throw an event to FreeSWITCH */
+ status = switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, AVMD_EVENT_BEEP);
+ if(status != SWITCH_STATUS_SUCCESS) {
+ return;
+ }
+
+ switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Beep-Status", "stop");
+ switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Unique-ID", switch_core_session_get_uuid(session->session));
+ switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "call-command", "avmd");
+
+ if ((switch_event_dup(&event_copy, event)) != SWITCH_STATUS_SUCCESS) {
+ return;
+ }
+
+ switch_core_session_queue_event(session->session, &event);
+ switch_event_fire(&event_copy);
+
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session->session), SWITCH_LOG_INFO, "<<< AVMD - Beep Detected >>>\n");
+ switch_channel_set_variable(channel, "avmd_detect", "TRUE");
+ RESET_SMA_BUFFER(&session->sma_b);
+ session->state.beep_state = BEEP_DETECTED;
+
+ return;
+ }
+
+ amp = 0.0;
+ success = 0.0;
+ error = 0.0;
+ }
+ }
+}
+
+/* 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:
+ */
+