diff --git a/main/audiohook.c b/main/audiohook.c index 7e2477002f..f73d6fabc3 100644 --- a/main/audiohook.c +++ b/main/audiohook.c @@ -39,11 +39,20 @@ #include "asterisk/frame.h" #include "asterisk/translate.h" #include "asterisk/format_cache.h" +#include "asterisk/framehook.h" +#include "asterisk/timing.h" #include "asterisk/test.h" #define AST_AUDIOHOOK_SYNC_TOLERANCE 100 /*!< Tolerance in milliseconds for audiohooks synchronization */ #define AST_AUDIOHOOK_SMALL_QUEUE_TOLERANCE 100 /*!< When small queue is enabled, this is the maximum amount of audio that can remain queued at a time. */ #define AST_AUDIOHOOK_LONG_QUEUE_TOLERANCE 500 /*!< Otheriwise we still don't want the queue to grow indefinitely */ +#define AST_WHISPER_TIMER_INTERVAL 20 +#define AST_WHISPER_TIMER_INTERVAL_MIN 10 +#define AST_WHISPER_TIMER_INTERVAL_MAX 60 +#define AST_WHISPER_TIMER_CLAMP(interval) \ + ((interval) < AST_WHISPER_TIMER_INTERVAL_MIN ? AST_WHISPER_TIMER_INTERVAL_MIN : \ + ((interval) > AST_WHISPER_TIMER_INTERVAL_MAX ? AST_WHISPER_TIMER_INTERVAL_MAX : (interval))) +#define AST_WHISPER_TIMER_SRC "audiohook whisper timer" #define DEFAULT_INTERNAL_SAMPLE_RATE 8000 @@ -52,6 +61,14 @@ struct ast_audiohook_translate { struct ast_format *format; }; +struct whisper_framehook_data { + struct ast_timer *timer; + int timer_fd; + int timer_fd_slot; + int timer_interval; + struct timeval last_external_write_tv; +}; + struct ast_audiohook_list { /* If all the audiohooks in this list are capable * of processing slinear at any sample rate, this @@ -65,8 +82,215 @@ struct ast_audiohook_list { AST_LIST_HEAD_NOLOCK(, ast_audiohook) spy_list; AST_LIST_HEAD_NOLOCK(, ast_audiohook) whisper_list; AST_LIST_HEAD_NOLOCK(, ast_audiohook) manipulate_list; + int whisper_framehook_id; + struct whisper_framehook_data *whisper_framehook_data; }; +static void whisper_framehook_timer_update(struct ast_audiohook_list *audiohook_list, + struct ast_frame *start_frame, + int internal_sample_rate, + int samples) +{ + struct whisper_framehook_data *hook_data; + int interval; + + hook_data = audiohook_list->whisper_framehook_data; + if (!hook_data || internal_sample_rate <= 0 || samples <= 0) { + return; + } + + if (start_frame->src && !strcmp(start_frame->src, AST_WHISPER_TIMER_SRC)) { + return; + } + + interval = AST_WHISPER_TIMER_CLAMP((samples * 1000) / internal_sample_rate); + if (interval == hook_data->timer_interval) { + return; + } + + hook_data->timer_interval = interval; + if (hook_data->timer) { + ast_timer_set_rate(hook_data->timer, 1000 / interval); + } +} + +static void whisper_framehook_destroy_cb(void *data) +{ + ast_free(data); +} + +static struct ast_frame *whisper_framehook_event_cb(struct ast_channel *chan, + struct ast_frame *frame, + enum ast_framehook_event event, + void *data) +{ + struct whisper_framehook_data *hook_data = data; + struct ast_audiohook_list *audiohook_list; + int rate; + int samples; + int write_res; + short buf[AST_WHISPER_TIMER_INTERVAL_MAX * 48]; + struct ast_frame tmp_frame = { + .frametype = AST_FRAME_VOICE, + .offset = AST_FRIENDLY_OFFSET, + .data.ptr = buf, + .datalen = sizeof(buf), + .src = AST_WHISPER_TIMER_SRC, + }; + struct ast_frame *dup; + + switch (event) { + case AST_FRAMEHOOK_EVENT_READ: + /* We only care about timer driven READ events */ + if (ast_channel_fdno(chan) != hook_data->timer_fd_slot + || !hook_data->timer + || ast_timer_ack(hook_data->timer, 1) < 0) { + return frame; + } + break; + case AST_FRAMEHOOK_EVENT_WRITE: + /* + * Track WRITEs that are not triggered by the framehook so we can + * avoid overlapping with WRITEs that are triggered by the framehook. + */ + if (frame && frame->frametype == AST_FRAME_VOICE + && (!frame->src || strcmp(frame->src, AST_WHISPER_TIMER_SRC))) { + hook_data->last_external_write_tv = ast_tvnow(); + } + return frame; + case AST_FRAMEHOOK_EVENT_ATTACHED: + /* Initialize the timer */ + if (!hook_data->timer) { + hook_data->timer = ast_timer_open(); + if (!hook_data->timer) { + return frame; + } + hook_data->timer_fd = ast_timer_fd(hook_data->timer); + hook_data->timer_fd_slot = ast_channel_fd_add(chan, hook_data->timer_fd); + if (hook_data->timer_fd_slot < 0) { + ast_timer_close(hook_data->timer); + hook_data->timer = NULL; + hook_data->timer_fd = -1; + return frame; + } + ast_timer_set_rate(hook_data->timer, 1000 / hook_data->timer_interval); + } + return frame; + case AST_FRAMEHOOK_EVENT_DETACHED: + /* Clean up the timer */ + if (hook_data->timer_fd_slot >= 0) { + ast_channel_set_fd(chan, hook_data->timer_fd_slot, -1); + } + if (hook_data->timer) { + ast_timer_close(hook_data->timer); + } + hook_data->timer = NULL; + hook_data->timer_fd = -1; + hook_data->timer_fd_slot = -1; + return frame; + } + + /* + * If normal outbound media frames are actively flowing (determined by tracking the + * WRITE events above,) skip timer-based fallback injection. This avoids overlapping + * WRITE frames and the resulting garbled audio. + */ + if (!ast_tvzero(hook_data->last_external_write_tv) + && ast_tvdiff_ms(ast_tvnow(), hook_data->last_external_write_tv) + <= (hook_data->timer_interval * 2)) { + return frame; + } + + audiohook_list = ast_channel_audiohooks(chan); + + /* + * As this framehook is dependant on the whisper audiohook(s) existing, this check + * should never fail. + */ + if (!audiohook_list || AST_LIST_EMPTY(&audiohook_list->whisper_list)) { + return frame; + } + + rate = audiohook_list->list_internal_samp_rate; + if (rate <= 0) { + rate = DEFAULT_INTERNAL_SAMPLE_RATE; + } + samples = (rate / 1000) * hook_data->timer_interval; + if (!samples || samples > ARRAY_LEN(buf)) { + return frame; + } + + memset(buf, 0, samples * sizeof(buf[0])); + tmp_frame.subclass.format = ast_format_cache_get_slin_by_rate(rate); + tmp_frame.samples = samples; + tmp_frame.datalen = samples * sizeof(buf[0]); + + dup = ast_frdup(&tmp_frame); + if (!dup) { + return frame; + } + + /* + * Invoke a write on the channel with our frame to trigger mixing in + * audio from the whisper audiohook list. + */ + ast_channel_unlock(chan); + write_res = ast_write(chan, dup); + ast_channel_lock(chan); + + if (write_res < 0) { + ast_frfree(dup); + } + + return &ast_null_frame; +} + +static int whisper_framehook_attach(struct ast_channel *chan, struct ast_audiohook_list *audiohook_list) +{ + struct ast_framehook_interface interface = { + .version = AST_FRAMEHOOK_INTERFACE_VERSION, + .event_cb = whisper_framehook_event_cb, + .destroy_cb = whisper_framehook_destroy_cb, + }; + struct whisper_framehook_data *hook_data; + int framehook_id; + + if (audiohook_list->whisper_framehook_id >= 0) { + return 0; + } + + hook_data = ast_calloc(1, sizeof(*hook_data)); + if (!hook_data) { + return -1; + } + hook_data->timer_fd = -1; + hook_data->timer_fd_slot = -1; + hook_data->timer_interval = AST_WHISPER_TIMER_INTERVAL; + + interface.data = hook_data; + framehook_id = ast_framehook_attach(chan, &interface); + if (framehook_id < 0) { + ast_free(hook_data); + return -1; + } + + audiohook_list->whisper_framehook_id = framehook_id; + audiohook_list->whisper_framehook_data = hook_data; + + return 0; +} + +static void whisper_framehook_detach(struct ast_channel *chan, struct ast_audiohook_list *audiohook_list) +{ + if (audiohook_list->whisper_framehook_id < 0) { + return; + } + + ast_framehook_detach(chan, audiohook_list->whisper_framehook_id); + audiohook_list->whisper_framehook_id = -1; + audiohook_list->whisper_framehook_data = NULL; +} + static int audiohook_set_internal_rate(struct ast_audiohook *audiohook, int rate, int reset) { struct ast_format *slin; @@ -544,13 +768,23 @@ int ast_audiohook_attach(struct ast_channel *chan, struct ast_audiohook *audioho AST_LIST_HEAD_INIT_NOLOCK(&ast_channel_audiohooks(chan)->manipulate_list); /* This sample rate will adjust as necessary when writing to the list. */ ast_channel_audiohooks(chan)->list_internal_samp_rate = DEFAULT_INTERNAL_SAMPLE_RATE; + ast_channel_audiohooks(chan)->whisper_framehook_id = -1; } /* Drop into respective list */ if (audiohook->type == AST_AUDIOHOOK_TYPE_SPY) { AST_LIST_INSERT_TAIL(&ast_channel_audiohooks(chan)->spy_list, audiohook, list); } else if (audiohook->type == AST_AUDIOHOOK_TYPE_WHISPER) { + int was_empty = AST_LIST_EMPTY(&ast_channel_audiohooks(chan)->whisper_list); AST_LIST_INSERT_TAIL(&ast_channel_audiohooks(chan)->whisper_list, audiohook, list); + /* Only attach the framehook on the first whisper audiohook */ + if (was_empty) { + if (whisper_framehook_attach(chan, ast_channel_audiohooks(chan))) { + AST_LIST_REMOVE(&ast_channel_audiohooks(chan)->whisper_list, audiohook, list); + ast_channel_unlock(chan); + return -1; + } + } } else if (audiohook->type == AST_AUDIOHOOK_TYPE_MANIPULATE) { AST_LIST_INSERT_TAIL(&ast_channel_audiohooks(chan)->manipulate_list, audiohook, list); } @@ -768,6 +1002,10 @@ int ast_audiohook_remove(struct ast_channel *chan, struct ast_audiohook *audioho AST_LIST_REMOVE(&ast_channel_audiohooks(chan)->spy_list, audiohook, list); } else if (audiohook->type == AST_AUDIOHOOK_TYPE_WHISPER) { AST_LIST_REMOVE(&ast_channel_audiohooks(chan)->whisper_list, audiohook, list); + /* Detach the framehook if this was the last whisper audiohook */ + if (AST_LIST_EMPTY(&ast_channel_audiohooks(chan)->whisper_list)) { + whisper_framehook_detach(chan, ast_channel_audiohooks(chan)); + } } else if (audiohook->type == AST_AUDIOHOOK_TYPE_MANIPULATE) { AST_LIST_REMOVE(&ast_channel_audiohooks(chan)->manipulate_list, audiohook, list); } @@ -1100,6 +1338,14 @@ static struct ast_frame *audio_audiohook_write_list(struct ast_channel *chan, st * was removed then the list's internal rate is reset to the default. */ audiohook_list->list_internal_samp_rate = internal_sample_rate; + /* + * Update the whisper timer with the internal sample rate on WRITE only to + * avoid overlapping writes. + */ + if (direction == AST_AUDIOHOOK_DIRECTION_WRITE) { + whisper_framehook_timer_update(audiohook_list, start_frame, + internal_sample_rate, samples); + } } return end_frame;