Wed Aug 18 22:33:58 2010

Asterisk developer's documentation


app_amd.c File Reference

Answering machine detection. More...

#include "asterisk.h"
#include "asterisk/module.h"
#include "asterisk/lock.h"
#include "asterisk/channel.h"
#include "asterisk/dsp.h"
#include "asterisk/pbx.h"
#include "asterisk/config.h"
#include "asterisk/app.h"

Go to the source code of this file.

Defines

#define STATE_IN_SILENCE   2
#define STATE_IN_WORD   1

Functions

static void __reg_module (void)
static void __unreg_module (void)
static int amd_exec (struct ast_channel *chan, void *data)
static void isAnsweringMachine (struct ast_channel *chan, void *data)
static int load_config (int reload)
static int load_module (void)
static int reload (void)
static int unload_module (void)

Variables

static struct ast_module_info __mod_info = { .name = AST_MODULE, .flags = AST_MODFLAG_DEFAULT , .description = "Answering Machine Detection Application" , .key = "This paragraph is copyright (c) 2006 by Digium, Inc. \In order for your module to load, it must return this \key via a function called \"key\". Any code which \includes this paragraph must be licensed under the GNU \General Public License version 2 or later (at your \option). In addition to Digium's general reservations \of rights, Digium expressly reserves the right to \allow other parties to license this paragraph under \different terms. Any use of Digium, Inc. trademarks or \logos (including \"Asterisk\" or \"Digium\") without \express written permission of Digium, Inc. is prohibited.\n" , .buildopt_sum = "a9c98e5d177805051735cb5b0b16b0a0" , .load = load_module, .unload = unload_module, .reload = reload, }
static char * app = "AMD"
static struct ast_module_infoast_module_info = &__mod_info
static char * descrip
static int dfltAfterGreetingSilence = 800
static int dfltBetweenWordsSilence = 50
static int dfltGreeting = 1500
static int dfltInitialSilence = 2500
static int dfltMaximumNumberOfWords = 3
static int dfltMaximumWordLength = 5000
static int dfltMaxWaitTimeForFrame = 50
static int dfltMinimumWordLength = 100
static int dfltSilenceThreshold = 256
static int dfltTotalAnalysisTime = 5000
static char * synopsis = "Attempts to detect answering machines"


Detailed Description

Answering machine detection.

Author:
Claude Klimos (claude.klimos@aheeva.com)

Definition in file app_amd.c.


Define Documentation

#define STATE_IN_SILENCE   2

Definition at line 83 of file app_amd.c.

Referenced by isAnsweringMachine().

#define STATE_IN_WORD   1

Definition at line 82 of file app_amd.c.

Referenced by isAnsweringMachine().


Function Documentation

static void __reg_module ( void   )  [static]

Definition at line 453 of file app_amd.c.

static void __unreg_module ( void   )  [static]

Definition at line 453 of file app_amd.c.

static int amd_exec ( struct ast_channel chan,
void *  data 
) [static]

Definition at line 361 of file app_amd.c.

References chan, and isAnsweringMachine().

Referenced by load_module().

00362 {
00363    isAnsweringMachine(chan, data);
00364 
00365    return 0;
00366 }

static void isAnsweringMachine ( struct ast_channel chan,
void *  data 
) [static]

Definition at line 99 of file app_amd.c.

References AST_APP_ARG, ast_codec_get_samples(), ast_debug, AST_DECLARE_APP_ARGS, ast_dsp_free(), ast_dsp_new(), ast_dsp_set_threshold(), ast_dsp_silence(), AST_FORMAT_SLINEAR, AST_FRAME_CNG, AST_FRAME_NULL, AST_FRAME_VOICE, ast_frfree, ast_log(), ast_read(), ast_set_read_format(), AST_STANDARD_APP_ARGS, ast_strdupa, ast_strlen_zero(), ast_verb, ast_waitfor(), chan, ast_channel::cid, ast_callerid::cid_ani, ast_callerid::cid_rdnis, DEFAULT_SAMPLES_PER_MS, f, LOG_WARNING, ast_channel::name, parse(), pbx_builtin_setvar_helper(), ast_channel::readformat, STATE_IN_SILENCE, and STATE_IN_WORD.

Referenced by amd_exec().

00100 {
00101    int res = 0;
00102    struct ast_frame *f = NULL;
00103    struct ast_dsp *silenceDetector = NULL;
00104    int dspsilence = 0, readFormat, framelength = 0;
00105    int inInitialSilence = 1;
00106    int inGreeting = 0;
00107    int voiceDuration = 0;
00108    int silenceDuration = 0;
00109    int iTotalTime = 0;
00110    int iWordsCount = 0;
00111    int currentState = STATE_IN_WORD;
00112    int previousState = STATE_IN_SILENCE;
00113    int consecutiveVoiceDuration = 0;
00114    char amdCause[256] = "", amdStatus[256] = "";
00115    char *parse = ast_strdupa(data);
00116 
00117    /* Lets set the initial values of the variables that will control the algorithm.
00118       The initial values are the default ones. If they are passed as arguments
00119       when invoking the application, then the default values will be overwritten
00120       by the ones passed as parameters. */
00121    int initialSilence       = dfltInitialSilence;
00122    int greeting             = dfltGreeting;
00123    int afterGreetingSilence = dfltAfterGreetingSilence;
00124    int totalAnalysisTime    = dfltTotalAnalysisTime;
00125    int minimumWordLength    = dfltMinimumWordLength;
00126    int betweenWordsSilence  = dfltBetweenWordsSilence;
00127    int maximumNumberOfWords = dfltMaximumNumberOfWords;
00128    int silenceThreshold     = dfltSilenceThreshold;
00129    int maximumWordLength    = dfltMaximumWordLength;
00130    int maxWaitTimeForFrame  = dfltMaxWaitTimeForFrame;
00131 
00132    AST_DECLARE_APP_ARGS(args,
00133       AST_APP_ARG(argInitialSilence);
00134       AST_APP_ARG(argGreeting);
00135       AST_APP_ARG(argAfterGreetingSilence);
00136       AST_APP_ARG(argTotalAnalysisTime);
00137       AST_APP_ARG(argMinimumWordLength);
00138       AST_APP_ARG(argBetweenWordsSilence);
00139       AST_APP_ARG(argMaximumNumberOfWords);
00140       AST_APP_ARG(argSilenceThreshold);
00141       AST_APP_ARG(argMaximumWordLength);
00142    );
00143 
00144    ast_verb(3, "AMD: %s %s %s (Fmt: %d)\n", chan->name ,chan->cid.cid_ani, chan->cid.cid_rdnis, chan->readformat);
00145 
00146    /* Lets parse the arguments. */
00147    if (!ast_strlen_zero(parse)) {
00148       /* Some arguments have been passed. Lets parse them and overwrite the defaults. */
00149       AST_STANDARD_APP_ARGS(args, parse);
00150       if (!ast_strlen_zero(args.argInitialSilence))
00151          initialSilence = atoi(args.argInitialSilence);
00152       if (!ast_strlen_zero(args.argGreeting))
00153          greeting = atoi(args.argGreeting);
00154       if (!ast_strlen_zero(args.argAfterGreetingSilence))
00155          afterGreetingSilence = atoi(args.argAfterGreetingSilence);
00156       if (!ast_strlen_zero(args.argTotalAnalysisTime))
00157          totalAnalysisTime = atoi(args.argTotalAnalysisTime);
00158       if (!ast_strlen_zero(args.argMinimumWordLength))
00159          minimumWordLength = atoi(args.argMinimumWordLength);
00160       if (!ast_strlen_zero(args.argBetweenWordsSilence))
00161          betweenWordsSilence = atoi(args.argBetweenWordsSilence);
00162       if (!ast_strlen_zero(args.argMaximumNumberOfWords))
00163          maximumNumberOfWords = atoi(args.argMaximumNumberOfWords);
00164       if (!ast_strlen_zero(args.argSilenceThreshold))
00165          silenceThreshold = atoi(args.argSilenceThreshold);
00166       if (!ast_strlen_zero(args.argMaximumWordLength))
00167          maximumWordLength = atoi(args.argMaximumWordLength);
00168    } else {
00169       ast_debug(1, "AMD using the default parameters.\n");
00170    }
00171 
00172    /* Find lowest ms value, that will be max wait time for a frame */
00173    if (maxWaitTimeForFrame > initialSilence)
00174       maxWaitTimeForFrame = initialSilence;
00175    if (maxWaitTimeForFrame > greeting)
00176       maxWaitTimeForFrame = greeting;
00177    if (maxWaitTimeForFrame > afterGreetingSilence)
00178       maxWaitTimeForFrame = afterGreetingSilence;
00179    if (maxWaitTimeForFrame > totalAnalysisTime)
00180       maxWaitTimeForFrame = totalAnalysisTime;
00181    if (maxWaitTimeForFrame > minimumWordLength)
00182       maxWaitTimeForFrame = minimumWordLength;
00183    if (maxWaitTimeForFrame > betweenWordsSilence)
00184       maxWaitTimeForFrame = betweenWordsSilence;
00185 
00186    /* Now we're ready to roll! */
00187    ast_verb(3, "AMD: initialSilence [%d] greeting [%d] afterGreetingSilence [%d] "
00188       "totalAnalysisTime [%d] minimumWordLength [%d] betweenWordsSilence [%d] maximumNumberOfWords [%d] silenceThreshold [%d] maximumWordLength [%d] \n",
00189             initialSilence, greeting, afterGreetingSilence, totalAnalysisTime,
00190             minimumWordLength, betweenWordsSilence, maximumNumberOfWords, silenceThreshold, maximumWordLength);
00191 
00192    /* Set read format to signed linear so we get signed linear frames in */
00193    readFormat = chan->readformat;
00194    if (ast_set_read_format(chan, AST_FORMAT_SLINEAR) < 0 ) {
00195       ast_log(LOG_WARNING, "AMD: Channel [%s]. Unable to set to linear mode, giving up\n", chan->name );
00196       pbx_builtin_setvar_helper(chan , "AMDSTATUS", "");
00197       pbx_builtin_setvar_helper(chan , "AMDCAUSE", "");
00198       return;
00199    }
00200 
00201    /* Create a new DSP that will detect the silence */
00202    if (!(silenceDetector = ast_dsp_new())) {
00203       ast_log(LOG_WARNING, "AMD: Channel [%s]. Unable to create silence detector :(\n", chan->name );
00204       pbx_builtin_setvar_helper(chan , "AMDSTATUS", "");
00205       pbx_builtin_setvar_helper(chan , "AMDCAUSE", "");
00206       return;
00207    }
00208 
00209    /* Set silence threshold to specified value */
00210    ast_dsp_set_threshold(silenceDetector, silenceThreshold);
00211 
00212    /* Now we go into a loop waiting for frames from the channel */
00213    while ((res = ast_waitfor(chan, 2 * maxWaitTimeForFrame)) > -1) {
00214 
00215       /* If we fail to read in a frame, that means they hung up */
00216       if (!(f = ast_read(chan))) {
00217          ast_verb(3, "AMD: Channel [%s]. HANGUP\n", chan->name);
00218          ast_debug(1, "Got hangup\n");
00219          strcpy(amdStatus, "HANGUP");
00220          res = 1;
00221          break;
00222       }
00223 
00224       if (f->frametype == AST_FRAME_VOICE || f->frametype == AST_FRAME_NULL || f->frametype == AST_FRAME_CNG) {
00225          /* If the total time exceeds the analysis time then give up as we are not too sure */
00226          if (f->frametype == AST_FRAME_VOICE)
00227             framelength = (ast_codec_get_samples(f) / DEFAULT_SAMPLES_PER_MS);
00228          else
00229             framelength += 2 * maxWaitTimeForFrame;
00230 
00231          iTotalTime += framelength;
00232          if (iTotalTime >= totalAnalysisTime) {
00233             ast_verb(3, "AMD: Channel [%s]. Too long...\n", chan->name );
00234             ast_frfree(f);
00235             strcpy(amdStatus , "NOTSURE");
00236             sprintf(amdCause , "TOOLONG-%d", iTotalTime);
00237             break;
00238          }
00239 
00240          /* Feed the frame of audio into the silence detector and see if we get a result */
00241          if (f->frametype != AST_FRAME_VOICE)
00242             dspsilence += 2 * maxWaitTimeForFrame;
00243          else {
00244             dspsilence = 0;
00245             ast_dsp_silence(silenceDetector, f, &dspsilence);
00246          }
00247 
00248          if (dspsilence > 0) {
00249             silenceDuration = dspsilence;
00250             
00251             if (silenceDuration >= betweenWordsSilence) {
00252                if (currentState != STATE_IN_SILENCE ) {
00253                   previousState = currentState;
00254                   ast_verb(3, "AMD: Channel [%s]. Changed state to STATE_IN_SILENCE\n", chan->name);
00255                }
00256                /* Find words less than word duration */
00257                if (consecutiveVoiceDuration < minimumWordLength && consecutiveVoiceDuration > 0){
00258                   ast_verb(3, "AMD: Channel [%s]. Short Word Duration: %d\n", chan->name, consecutiveVoiceDuration);
00259                }
00260                currentState  = STATE_IN_SILENCE;
00261                consecutiveVoiceDuration = 0;
00262             }
00263 
00264             if (inInitialSilence == 1  && silenceDuration >= initialSilence) {
00265                ast_verb(3, "AMD: Channel [%s]. ANSWERING MACHINE: silenceDuration:%d initialSilence:%d\n",
00266                   chan->name, silenceDuration, initialSilence);
00267                ast_frfree(f);
00268                strcpy(amdStatus , "MACHINE");
00269                sprintf(amdCause , "INITIALSILENCE-%d-%d", silenceDuration, initialSilence);
00270                res = 1;
00271                break;
00272             }
00273             
00274             if (silenceDuration >= afterGreetingSilence  &&  inGreeting == 1) {
00275                ast_verb(3, "AMD: Channel [%s]. HUMAN: silenceDuration:%d afterGreetingSilence:%d\n",
00276                   chan->name, silenceDuration, afterGreetingSilence);
00277                ast_frfree(f);
00278                strcpy(amdStatus , "HUMAN");
00279                sprintf(amdCause , "HUMAN-%d-%d", silenceDuration, afterGreetingSilence);
00280                res = 1;
00281                break;
00282             }
00283             
00284          } else {
00285             consecutiveVoiceDuration += framelength;
00286             voiceDuration += framelength;
00287 
00288             /* If I have enough consecutive voice to say that I am in a Word, I can only increment the
00289                number of words if my previous state was Silence, which means that I moved into a word. */
00290             if (consecutiveVoiceDuration >= minimumWordLength && currentState == STATE_IN_SILENCE) {
00291                iWordsCount++;
00292                ast_verb(3, "AMD: Channel [%s]. Word detected. iWordsCount:%d\n", chan->name, iWordsCount);
00293                previousState = currentState;
00294                currentState = STATE_IN_WORD;
00295             }
00296             if (consecutiveVoiceDuration >= maximumWordLength){
00297                ast_verb(3, "AMD: Channel [%s]. Maximum Word Length detected. [%d]\n", chan->name, consecutiveVoiceDuration);
00298                ast_frfree(f);
00299                strcpy(amdStatus , "MACHINE");
00300                sprintf(amdCause , "MAXWORDLENGTH-%d", consecutiveVoiceDuration);
00301                break;
00302             }
00303             if (iWordsCount >= maximumNumberOfWords) {
00304                ast_verb(3, "AMD: Channel [%s]. ANSWERING MACHINE: iWordsCount:%d\n", chan->name, iWordsCount);
00305                ast_frfree(f);
00306                strcpy(amdStatus , "MACHINE");
00307                sprintf(amdCause , "MAXWORDS-%d-%d", iWordsCount, maximumNumberOfWords);
00308                res = 1;
00309                break;
00310             }
00311 
00312             if (inGreeting == 1 && voiceDuration >= greeting) {
00313                ast_verb(3, "AMD: Channel [%s]. ANSWERING MACHINE: voiceDuration:%d greeting:%d\n", chan->name, voiceDuration, greeting);
00314                ast_frfree(f);
00315                strcpy(amdStatus , "MACHINE");
00316                sprintf(amdCause , "LONGGREETING-%d-%d", voiceDuration, greeting);
00317                res = 1;
00318                break;
00319             }
00320 
00321             if (voiceDuration >= minimumWordLength ) {
00322                if (silenceDuration > 0)
00323                   ast_verb(3, "AMD: Channel [%s]. Detected Talk, previous silence duration: %d\n", chan->name, silenceDuration);
00324                silenceDuration = 0;
00325             }
00326             if (consecutiveVoiceDuration >= minimumWordLength && inGreeting == 0) {
00327                /* Only go in here once to change the greeting flag when we detect the 1st word */
00328                if (silenceDuration > 0)
00329                   ast_verb(3, "AMD: Channel [%s]. Before Greeting Time:  silenceDuration: %d voiceDuration: %d\n", chan->name, silenceDuration, voiceDuration);
00330                inInitialSilence = 0;
00331                inGreeting = 1;
00332             }
00333             
00334          }
00335       }
00336       ast_frfree(f);
00337    }
00338    
00339    if (!res) {
00340       /* It took too long to get a frame back. Giving up. */
00341       ast_verb(3, "AMD: Channel [%s]. Too long...\n", chan->name);
00342       strcpy(amdStatus , "NOTSURE");
00343       sprintf(amdCause , "TOOLONG-%d", iTotalTime);
00344    }
00345 
00346    /* Set the status and cause on the channel */
00347    pbx_builtin_setvar_helper(chan , "AMDSTATUS" , amdStatus);
00348    pbx_builtin_setvar_helper(chan , "AMDCAUSE" , amdCause);
00349 
00350    /* Restore channel read format */
00351    if (readFormat && ast_set_read_format(chan, readFormat))
00352       ast_log(LOG_WARNING, "AMD: Unable to restore read format on '%s'\n", chan->name);
00353 
00354    /* Free the DSP used to detect silence */
00355    ast_dsp_free(silenceDetector);
00356 
00357    return;
00358 }

static int load_config ( int  reload  )  [static]

Definition at line 368 of file app_amd.c.

References ast_category_browse(), ast_config_destroy(), ast_config_load, ast_dsp_get_threshold_from_settings(), ast_log(), ast_variable_browse(), ast_verb, CONFIG_FLAG_FILEUNCHANGED, config_flags, CONFIG_STATUS_FILEUNCHANGED, LOG_ERROR, LOG_WARNING, THRESHOLD_SILENCE, and var.

00369 {
00370    struct ast_config *cfg = NULL;
00371    char *cat = NULL;
00372    struct ast_variable *var = NULL;
00373    struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
00374 
00375    dfltSilenceThreshold = ast_dsp_get_threshold_from_settings(THRESHOLD_SILENCE);
00376 
00377    if (!(cfg = ast_config_load("amd.conf", config_flags))) {
00378       ast_log(LOG_ERROR, "Configuration file amd.conf missing.\n");
00379       return -1;
00380    } else if (cfg == CONFIG_STATUS_FILEUNCHANGED)
00381       return 0;
00382 
00383    cat = ast_category_browse(cfg, NULL);
00384 
00385    while (cat) {
00386       if (!strcasecmp(cat, "general") ) {
00387          var = ast_variable_browse(cfg, cat);
00388          while (var) {
00389             if (!strcasecmp(var->name, "initial_silence")) {
00390                dfltInitialSilence = atoi(var->value);
00391             } else if (!strcasecmp(var->name, "greeting")) {
00392                dfltGreeting = atoi(var->value);
00393             } else if (!strcasecmp(var->name, "after_greeting_silence")) {
00394                dfltAfterGreetingSilence = atoi(var->value);
00395             } else if (!strcasecmp(var->name, "silence_threshold")) {
00396                dfltSilenceThreshold = atoi(var->value);
00397             } else if (!strcasecmp(var->name, "total_analysis_time")) {
00398                dfltTotalAnalysisTime = atoi(var->value);
00399             } else if (!strcasecmp(var->name, "min_word_length")) {
00400                dfltMinimumWordLength = atoi(var->value);
00401             } else if (!strcasecmp(var->name, "between_words_silence")) {
00402                dfltBetweenWordsSilence = atoi(var->value);
00403             } else if (!strcasecmp(var->name, "maximum_number_of_words")) {
00404                dfltMaximumNumberOfWords = atoi(var->value);
00405             } else if (!strcasecmp(var->name, "maximum_word_length")) {
00406                dfltMaximumWordLength = atoi(var->value);
00407 
00408             } else {
00409                ast_log(LOG_WARNING, "%s: Cat:%s. Unknown keyword %s at line %d of amd.conf\n",
00410                   app, cat, var->name, var->lineno);
00411             }
00412             var = var->next;
00413          }
00414       }
00415       cat = ast_category_browse(cfg, cat);
00416    }
00417 
00418    ast_config_destroy(cfg);
00419 
00420    ast_verb(3, "AMD defaults: initialSilence [%d] greeting [%d] afterGreetingSilence [%d] "
00421       "totalAnalysisTime [%d] minimumWordLength [%d] betweenWordsSilence [%d] maximumNumberOfWords [%d] silenceThreshold [%d] maximumWordLength [%d]\n",
00422       dfltInitialSilence, dfltGreeting, dfltAfterGreetingSilence, dfltTotalAnalysisTime,
00423       dfltMinimumWordLength, dfltBetweenWordsSilence, dfltMaximumNumberOfWords, dfltSilenceThreshold, dfltMaximumWordLength);
00424 
00425    return 0;
00426 }

static int load_module ( void   )  [static]

Definition at line 433 of file app_amd.c.

References amd_exec(), AST_MODULE_LOAD_DECLINE, AST_MODULE_LOAD_FAILURE, AST_MODULE_LOAD_SUCCESS, ast_register_application, and load_config().

00434 {
00435    if (load_config(0))
00436       return AST_MODULE_LOAD_DECLINE;
00437    if (ast_register_application(app, amd_exec, synopsis, descrip))
00438       return AST_MODULE_LOAD_FAILURE;
00439    return AST_MODULE_LOAD_SUCCESS;
00440 }

static int reload ( void   )  [static]

Definition at line 442 of file app_amd.c.

References AST_MODULE_LOAD_DECLINE, AST_MODULE_LOAD_SUCCESS, and load_config().

00443 {
00444    if (load_config(1))
00445       return AST_MODULE_LOAD_DECLINE;
00446    return AST_MODULE_LOAD_SUCCESS;
00447 }

static int unload_module ( void   )  [static]

Definition at line 428 of file app_amd.c.

References ast_unregister_application().

00429 {
00430    return ast_unregister_application(app);
00431 }


Variable Documentation

struct ast_module_info __mod_info = { .name = AST_MODULE, .flags = AST_MODFLAG_DEFAULT , .description = "Answering Machine Detection Application" , .key = "This paragraph is copyright (c) 2006 by Digium, Inc. \In order for your module to load, it must return this \key via a function called \"key\". Any code which \includes this paragraph must be licensed under the GNU \General Public License version 2 or later (at your \option). In addition to Digium's general reservations \of rights, Digium expressly reserves the right to \allow other parties to license this paragraph under \different terms. Any use of Digium, Inc. trademarks or \logos (including \"Asterisk\" or \"Digium\") without \express written permission of Digium, Inc. is prohibited.\n" , .buildopt_sum = "a9c98e5d177805051735cb5b0b16b0a0" , .load = load_module, .unload = unload_module, .reload = reload, } [static]

Definition at line 453 of file app_amd.c.

char* app = "AMD" [static]

Definition at line 43 of file app_amd.c.

struct ast_module_info* ast_module_info = &__mod_info [static]

Definition at line 453 of file app_amd.c.

char* descrip [static]

Definition at line 45 of file app_amd.c.

int dfltAfterGreetingSilence = 800 [static]

Definition at line 88 of file app_amd.c.

int dfltBetweenWordsSilence = 50 [static]

Definition at line 91 of file app_amd.c.

int dfltGreeting = 1500 [static]

Definition at line 87 of file app_amd.c.

int dfltInitialSilence = 2500 [static]

Definition at line 86 of file app_amd.c.

int dfltMaximumNumberOfWords = 3 [static]

Definition at line 92 of file app_amd.c.

int dfltMaximumWordLength = 5000 [static]

Definition at line 94 of file app_amd.c.

int dfltMaxWaitTimeForFrame = 50 [static]

Definition at line 97 of file app_amd.c.

int dfltMinimumWordLength = 100 [static]

Definition at line 90 of file app_amd.c.

int dfltSilenceThreshold = 256 [static]

Definition at line 93 of file app_amd.c.

int dfltTotalAnalysisTime = 5000 [static]

Definition at line 89 of file app_amd.c.

char* synopsis = "Attempts to detect answering machines" [static]

Definition at line 44 of file app_amd.c.


Generated on Wed Aug 18 22:33:58 2010 for Asterisk - the Open Source PBX by  doxygen 1.4.7