/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 2005, Anthony Minessale II * Copyright (C) 2005 - 2006, Digium, Inc. * * Mark Spencer * Kevin P. Fleming * * Based on app_muxmon.c provided by * Anthony Minessale II * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ /*! \file * * \brief MixMonitor() - Record a call and mix the audio during the recording * \ingroup applications * * \author Mark Spencer * \author Kevin P. Fleming * * \note Based on app_muxmon.c provided by * Anthony Minessale II */ #include "asterisk.h" ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/paths.h" /* use ast_config_AST_MONITOR_DIR */ #include "asterisk/file.h" #include "asterisk/audiohook.h" #include "asterisk/pbx.h" #include "asterisk/module.h" #include "asterisk/cli.h" #include "asterisk/app.h" #include "asterisk/channel.h" #define get_volfactor(x) x ? ((x > 0) ? (1 << x) : ((1 << abs(x)) * -1)) : 0 static const char *app = "MixMonitor"; static const char *synopsis = "Record a call and mix the audio during the recording"; static const char *desc = "" " MixMonitor(.[,[,]]):\n" "Records the audio on the current channel to the specified file.\n" "If the filename is an absolute path, uses that path, otherwise\n" "creates the file in the configured monitoring directory from\n" "asterisk.conf.\n\n" "Valid options:\n" " a - Append to the file instead of overwriting it.\n" " b - Only save audio to the file while the channel is bridged.\n" " Note: Does not include conferences or sounds played to each bridged\n" " party.\n" " v() - Adjust the heard volume by a factor of (range -4 to 4)\n" " V() - Adjust the spoken volume by a factor of (range -4 to 4)\n" " W() - Adjust the both heard and spoken volumes by a factor of \n" " (range -4 to 4)\n\n" " will be executed when the recording is over\n" "Any strings matching ^{X} will be unescaped to ${X}.\n" "All variables will be evaluated at the time MixMonitor is called.\n" "The variable MIXMONITOR_FILENAME will contain the filename used to record.\n" ""; static const char *stop_app = "StopMixMonitor"; static const char *stop_synopsis = "Stop recording a call through MixMonitor"; static const char *stop_desc = "" " StopMixMonitor():\n" "Stops the audio recording that was started with a call to MixMonitor()\n" "on the current channel.\n" ""; struct module_symbols *me; static const char *mixmonitor_spy_type = "MixMonitor"; struct mixmonitor { struct ast_audiohook audiohook; char *filename; char *post_process; char *name; unsigned int flags; struct ast_channel *chan; }; enum { MUXFLAG_APPEND = (1 << 1), MUXFLAG_BRIDGED = (1 << 2), MUXFLAG_VOLUME = (1 << 3), MUXFLAG_READVOLUME = (1 << 4), MUXFLAG_WRITEVOLUME = (1 << 5), } mixmonitor_flags; enum { OPT_ARG_READVOLUME = 0, OPT_ARG_WRITEVOLUME, OPT_ARG_VOLUME, OPT_ARG_ARRAY_SIZE, } mixmonitor_args; AST_APP_OPTIONS(mixmonitor_opts, { AST_APP_OPTION('a', MUXFLAG_APPEND), AST_APP_OPTION('b', MUXFLAG_BRIDGED), AST_APP_OPTION_ARG('v', MUXFLAG_READVOLUME, OPT_ARG_READVOLUME), AST_APP_OPTION_ARG('V', MUXFLAG_WRITEVOLUME, OPT_ARG_WRITEVOLUME), AST_APP_OPTION_ARG('W', MUXFLAG_VOLUME, OPT_ARG_VOLUME), }); static int startmon(struct ast_channel *chan, struct ast_audiohook *audiohook) { struct ast_channel *peer = NULL; int res = 0; if (!chan) return -1; ast_audiohook_attach(chan, audiohook); if (!res && ast_test_flag(chan, AST_FLAG_NBRIDGE) && (peer = ast_bridged_channel(chan))) ast_softhangup(peer, AST_SOFTHANGUP_UNBRIDGE); return res; } #define SAMPLES_PER_FRAME 160 static void *mixmonitor_thread(void *obj) { struct mixmonitor *mixmonitor = obj; struct ast_filestream *fs = NULL; unsigned int oflags; char *ext; int errflag = 0; ast_verb(2, "Begin MixMonitor Recording %s\n", mixmonitor->name); ast_audiohook_lock(&mixmonitor->audiohook); while (mixmonitor->audiohook.status == AST_AUDIOHOOK_STATUS_RUNNING) { struct ast_frame *fr = NULL; ast_audiohook_trigger_wait(&mixmonitor->audiohook); if (mixmonitor->audiohook.status != AST_AUDIOHOOK_STATUS_RUNNING) break; if (!(fr = ast_audiohook_read_frame(&mixmonitor->audiohook, SAMPLES_PER_FRAME, AST_AUDIOHOOK_DIRECTION_BOTH, AST_FORMAT_SLINEAR))) continue; if (!ast_test_flag(mixmonitor, MUXFLAG_BRIDGED) || ast_bridged_channel(mixmonitor->chan)) { /* Initialize the file if not already done so */ if (!fs && !errflag) { oflags = O_CREAT | O_WRONLY; oflags |= ast_test_flag(mixmonitor, MUXFLAG_APPEND) ? O_APPEND : O_TRUNC; if ((ext = strrchr(mixmonitor->filename, '.'))) *(ext++) = '\0'; else ext = "raw"; if (!(fs = ast_writefile(mixmonitor->filename, ext, NULL, oflags, 0, 0644))) { ast_log(LOG_ERROR, "Cannot open %s.%s\n", mixmonitor->filename, ext); errflag = 1; } } /* Write out frame */ if (fs) ast_writestream(fs, fr); } /* All done! free it. */ ast_frame_free(fr, 0); } ast_audiohook_detach(&mixmonitor->audiohook); ast_audiohook_unlock(&mixmonitor->audiohook); ast_audiohook_destroy(&mixmonitor->audiohook); ast_verb(2, "End MixMonitor Recording %s\n", mixmonitor->name); if (fs) ast_closestream(fs); if (mixmonitor->post_process) { ast_verb(2, "Executing [%s]\n", mixmonitor->post_process); ast_safe_system(mixmonitor->post_process); } ast_free(mixmonitor); return NULL; } static void launch_monitor_thread(struct ast_channel *chan, const char *filename, unsigned int flags, int readvol, int writevol, const char *post_process) { pthread_t thread; struct mixmonitor *mixmonitor; char postprocess2[1024] = ""; size_t len; len = sizeof(*mixmonitor) + strlen(chan->name) + strlen(filename) + 2; postprocess2[0] = 0; /* If a post process system command is given attach it to the structure */ if (!ast_strlen_zero(post_process)) { char *p1, *p2; p1 = ast_strdupa(post_process); for (p2 = p1; *p2 ; p2++) { if (*p2 == '^' && *(p2+1) == '{') { *p2 = '$'; } } pbx_substitute_variables_helper(chan, p1, postprocess2, sizeof(postprocess2) - 1); if (!ast_strlen_zero(postprocess2)) len += strlen(postprocess2) + 1; } /* Pre-allocate mixmonitor structure and spy */ if (!(mixmonitor = ast_calloc(1, len))) { return; } /* Copy over flags and channel name */ mixmonitor->flags = flags; mixmonitor->chan = chan; mixmonitor->name = (char *) mixmonitor + sizeof(*mixmonitor); strcpy(mixmonitor->name, chan->name); if (!ast_strlen_zero(postprocess2)) { mixmonitor->post_process = mixmonitor->name + strlen(mixmonitor->name) + strlen(filename) + 2; strcpy(mixmonitor->post_process, postprocess2); } mixmonitor->filename = (char *) mixmonitor + sizeof(*mixmonitor) + strlen(chan->name) + 1; strcpy(mixmonitor->filename, filename); /* Setup the actual spy before creating our thread */ if (ast_audiohook_init(&mixmonitor->audiohook, AST_AUDIOHOOK_TYPE_SPY, mixmonitor_spy_type)) { ast_free(mixmonitor); return; } ast_set_flag(&mixmonitor->audiohook, AST_AUDIOHOOK_TRIGGER_SYNC); if (readvol) mixmonitor->audiohook.options.read_volume = readvol; if (writevol) mixmonitor->audiohook.options.write_volume = writevol; if (startmon(chan, &mixmonitor->audiohook)) { ast_log(LOG_WARNING, "Unable to add '%s' spy to channel '%s'\n", mixmonitor_spy_type, chan->name); ast_audiohook_destroy(&mixmonitor->audiohook); ast_free(mixmonitor); return; } ast_pthread_create_detached_background(&thread, NULL, mixmonitor_thread, mixmonitor); } static int mixmonitor_exec(struct ast_channel *chan, void *data) { int x, readvol = 0, writevol = 0; struct ast_flags flags = {0}; char *parse, *tmp, *slash; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(filename); AST_APP_ARG(options); AST_APP_ARG(post_process); ); if (ast_strlen_zero(data)) { ast_log(LOG_WARNING, "MixMonitor requires an argument (filename)\n"); return -1; } parse = ast_strdupa(data); AST_STANDARD_APP_ARGS(args, parse); if (ast_strlen_zero(args.filename)) { ast_log(LOG_WARNING, "MixMonitor requires an argument (filename)\n"); return -1; } if (args.options) { char *opts[OPT_ARG_ARRAY_SIZE] = { NULL, }; ast_app_parse_options(mixmonitor_opts, &flags, opts, args.options); if (ast_test_flag(&flags, MUXFLAG_READVOLUME)) { if (ast_strlen_zero(opts[OPT_ARG_READVOLUME])) { ast_log(LOG_WARNING, "No volume level was provided for the heard volume ('v') option.\n"); } else if ((sscanf(opts[OPT_ARG_READVOLUME], "%d", &x) != 1) || (x < -4) || (x > 4)) { ast_log(LOG_NOTICE, "Heard volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_READVOLUME]); } else { readvol = get_volfactor(x); } } if (ast_test_flag(&flags, MUXFLAG_WRITEVOLUME)) { if (ast_strlen_zero(opts[OPT_ARG_WRITEVOLUME])) { ast_log(LOG_WARNING, "No volume level was provided for the spoken volume ('V') option.\n"); } else if ((sscanf(opts[OPT_ARG_WRITEVOLUME], "%d", &x) != 1) || (x < -4) || (x > 4)) { ast_log(LOG_NOTICE, "Spoken volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_WRITEVOLUME]); } else { writevol = get_volfactor(x); } } if (ast_test_flag(&flags, MUXFLAG_VOLUME)) { if (ast_strlen_zero(opts[OPT_ARG_VOLUME])) { ast_log(LOG_WARNING, "No volume level was provided for the combined volume ('W') option.\n"); } else if ((sscanf(opts[OPT_ARG_VOLUME], "%d", &x) != 1) || (x < -4) || (x > 4)) { ast_log(LOG_NOTICE, "Combined volume must be a number between -4 and 4, not '%s'\n", opts[OPT_ARG_VOLUME]); } else { readvol = writevol = get_volfactor(x); } } } /* if not provided an absolute path, use the system-configured monitoring directory */ if (args.filename[0] != '/') { char *build; build = alloca(strlen(ast_config_AST_MONITOR_DIR) + strlen(args.filename) + 3); sprintf(build, "%s/%s", ast_config_AST_MONITOR_DIR, args.filename); args.filename = build; } tmp = ast_strdupa(args.filename); if ((slash = strrchr(tmp, '/'))) *slash = '\0'; ast_mkdir(tmp, 0777); pbx_builtin_setvar_helper(chan, "MIXMONITOR_FILENAME", args.filename); launch_monitor_thread(chan, args.filename, flags.flags, readvol, writevol, args.post_process); return 0; } static int stop_mixmonitor_exec(struct ast_channel *chan, void *data) { ast_audiohook_detach_source(chan, mixmonitor_spy_type); return 0; } static char *handle_cli_mixmonitor(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { struct ast_channel *chan; switch (cmd) { case CLI_INIT: e->command = "mixmonitor [start|stop]"; e->usage = "Usage: mixmonitor [args]\n" " The optional arguments are passed to the MixMonitor\n" " application when the 'start' command is used.\n"; return NULL; case CLI_GENERATE: return ast_complete_channels(a->line, a->word, a->pos, a->n, 2); } if (a->argc < 3) return CLI_SHOWUSAGE; if (!(chan = ast_get_channel_by_name_prefix_locked(a->argv[2], strlen(a->argv[2])))) { ast_cli(a->fd, "No channel matching '%s' found.\n", a->argv[2]); /* Technically this is a failure, but we don't want 2 errors printing out */ return CLI_SUCCESS; } if (!strcasecmp(a->argv[1], "start")) { mixmonitor_exec(chan, a->argv[3]); ast_channel_unlock(chan); } else { ast_channel_unlock(chan); ast_audiohook_detach_source(chan, mixmonitor_spy_type); } return CLI_SUCCESS; } static struct ast_cli_entry cli_mixmonitor[] = { AST_CLI_DEFINE(handle_cli_mixmonitor, "Execute a MixMonitor command") }; static int unload_module(void) { int res; ast_cli_unregister_multiple(cli_mixmonitor, sizeof(cli_mixmonitor) / sizeof(struct ast_cli_entry)); res = ast_unregister_application(stop_app); res |= ast_unregister_application(app); return res; } static int load_module(void) { int res; ast_cli_register_multiple(cli_mixmonitor, sizeof(cli_mixmonitor) / sizeof(struct ast_cli_entry)); res = ast_register_application(app, mixmonitor_exec, synopsis, desc); res |= ast_register_application(stop_app, stop_mixmonitor_exec, stop_synopsis, stop_desc); return res; } AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Mixed Audio Monitoring Application");