From 2a4cf1f04eb2e79c118445da95d970d5be12d89b Mon Sep 17 00:00:00 2001 From: Dragos Oancea Date: Mon, 10 Jul 2017 16:29:31 -0400 Subject: [PATCH 1/2] FS-10530: [mod_opusfile] new module, read and write ogg/opus files (".opus" extension) --- build/modules.conf.in | 1 + .../testing/autoload_configs/modules.conf.xml | 1 + .../vanilla/autoload_configs/modules.conf.xml | 1 + configure.ac | 9 + debian/control-modules | 5 + freeswitch.spec | 17 + src/mod/formats/mod_opusfile/Makefile.am | 19 + src/mod/formats/mod_opusfile/mod_opusfile.c | 489 ++++++++++++++++++ 8 files changed, 542 insertions(+) create mode 100644 src/mod/formats/mod_opusfile/Makefile.am create mode 100644 src/mod/formats/mod_opusfile/mod_opusfile.c diff --git a/build/modules.conf.in b/build/modules.conf.in index d998e5dba8..5d54937254 100644 --- a/build/modules.conf.in +++ b/build/modules.conf.in @@ -124,6 +124,7 @@ formats/mod_sndfile #formats/mod_ssml formats/mod_tone_stream #formats/mod_vlc +#formats/mod_opusfile #languages/mod_basic #languages/mod_java languages/mod_lua diff --git a/conf/testing/autoload_configs/modules.conf.xml b/conf/testing/autoload_configs/modules.conf.xml index 9849ce2d94..7c65886a9e 100644 --- a/conf/testing/autoload_configs/modules.conf.xml +++ b/conf/testing/autoload_configs/modules.conf.xml @@ -26,6 +26,7 @@ + diff --git a/conf/vanilla/autoload_configs/modules.conf.xml b/conf/vanilla/autoload_configs/modules.conf.xml index 2c465d1231..11ad9f394f 100644 --- a/conf/vanilla/autoload_configs/modules.conf.xml +++ b/conf/vanilla/autoload_configs/modules.conf.xml @@ -104,6 +104,7 @@ + diff --git a/configure.ac b/configure.ac index ff1efdc533..0f67caeb55 100644 --- a/configure.ac +++ b/configure.ac @@ -1383,6 +1383,14 @@ PKG_CHECK_MODULES([OPENCV], [opencv >= 2.4.5],[ AM_CONDITIONAL([HAVE_OPENCV],[true])],[ AC_MSG_RESULT([no]); AM_CONDITIONAL([HAVE_OPENCV],[false])]) +PKG_CHECK_MODULES([OPUSFILE_DECODE], [opusfile >= 0.5],[ + AM_CONDITIONAL([HAVE_OPUSFILE_DECODE],[true])],[ + AC_MSG_RESULT([no]); AM_CONDITIONAL([HAVE_OPUSFILE_DECODE],[false])]) +PKG_CHECK_MODULES([OPUSFILE_ENCODE], [libopusenc >= 0.1],[ + AM_CONDITIONAL([HAVE_OPUSFILE_ENCODE],[true])],[ + AC_MSG_RESULT([no]); AM_CONDITIONAL([HAVE_OPUSFILE_ENCODE],[false])]) + + PKG_CHECK_MODULES([MAGICK], [ImageMagick >= 6.0.0],[ AM_CONDITIONAL([HAVE_MAGICK],[true])],[ AC_MSG_RESULT([no]); AM_CONDITIONAL([HAVE_MAGICK],[false])]) @@ -1893,6 +1901,7 @@ AC_CONFIG_FILES([Makefile src/mod/formats/mod_imagick/Makefile src/mod/formats/mod_local_stream/Makefile src/mod/formats/mod_native_file/Makefile + src/mod/formats/mod_opusfile/Makefile src/mod/formats/mod_shell_stream/Makefile src/mod/formats/mod_shout/Makefile src/mod/formats/mod_sndfile/Makefile diff --git a/debian/control-modules b/debian/control-modules index 0053c7dfd9..6706937943 100644 --- a/debian/control-modules +++ b/debian/control-modules @@ -616,6 +616,11 @@ Module: formats/mod_webm Description: Adds mod_webm Adds mod_webm. +Module: formats/mod_opusfile +Description: mod_opusfile + Adds mod_opusfile. +Build-Depends: libopusfile-dev + ## mod/languages Module: languages/mod_basic diff --git a/freeswitch.spec b/freeswitch.spec index 759553e03b..b71b0824d1 100644 --- a/freeswitch.spec +++ b/freeswitch.spec @@ -38,6 +38,7 @@ %define build_mod_rayo 1 %define build_mod_ssml 1 %define build_mod_shout 0 +%define build_mod_opusfile 0 %{?with_sang_tc:%define build_sng_tc 1 } %{?with_sang_isdn:%define build_sng_isdn 1 } @@ -46,6 +47,7 @@ %{?with_timerfd:%define build_timerfd 1 } %{?with_mod_esl:%define build_mod_esl 1 } %{?with_mod_shout:%define build_mod_shout 1 } +%{?with_mod_opusfile:%define build_mod_opusfile 1 } %define version 1.7.0 %define release 1 @@ -1149,6 +1151,18 @@ Mod Shout is a FreeSWITCH module to allow you to stream audio from MP3s or a i shoutcast stream. %endif +%if %{build_mod_opusfile} +%package format-mod-opusfile +Summary: Plays Opus encoded files +Group: System/Libraries +Requires: %{name} = %{version}-%{release} +Requires: opusfile >= 0.5 +BuildRequires: opusfile-devel >= 0.5 + +%description format-mod-opusfile +Mod Opusfile is a FreeSWITCH module to allow you to play Opus encoded files +%endif + %if %{build_mod_ssml} %package format-ssml Summary: Adds Speech Synthesis Markup Language (SSML) parser format for the FreeSWITCH open source telephony platform @@ -1537,6 +1551,9 @@ FORMATS_MODULES+=" formats/mod_shout " %if %{build_mod_ssml} FORMATS_MODULES+=" formats/mod_ssml" %endif +%if %{build_mod_opusfile} +FORMATS_MODULES+=" formats/mod_opusfile" +%endif ###################################################################################################################### # diff --git a/src/mod/formats/mod_opusfile/Makefile.am b/src/mod/formats/mod_opusfile/Makefile.am new file mode 100644 index 0000000000..f2a293674a --- /dev/null +++ b/src/mod/formats/mod_opusfile/Makefile.am @@ -0,0 +1,19 @@ +include $(top_srcdir)/build/modmake.rulesam +MODNAME=mod_opusfile + +mod_LTLIBRARIES = mod_opusfile.la +mod_opusfile_la_SOURCES = mod_opusfile.c +mod_opusfile_la_CFLAGS = $(AM_CFLAGS) +mod_opusfile_la_LIBADD = $(switch_builddir)/libfreeswitch.la +mod_opusfile_la_LDFLAGS = -avoid-version -module -no-undefined -shared + +if HAVE_OPUSFILE_DECODE +mod_opusfile_la_CFLAGS += -I$(OPUSFILE_DECODE_CFLAGS) +mod_opusfile_la_LIBADD += $(OPUSFILE_DECODE_LIBS) +endif + +if HAVE_OPUSFILE_ENCODE +mod_opusfile_la_CFLAGS += -I$(OPUSFILE_ENCODE_CFLAGS) -DHAVE_OPUSFILE_ENCODE +mod_opusfile_la_LIBADD += $(OPUSFILE_ENCODE_LIBS) +endif + diff --git a/src/mod/formats/mod_opusfile/mod_opusfile.c b/src/mod/formats/mod_opusfile/mod_opusfile.c new file mode 100644 index 0000000000..9487044628 --- /dev/null +++ b/src/mod/formats/mod_opusfile/mod_opusfile.c @@ -0,0 +1,489 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2014, Anthony Minessale II + * + * 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. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Dragos Oancea (mod_opusfile.c) + * + * + * mod_opusfile.c -- Read and Write OGG/Opus files . Some parts inspired from mod_shout.c, libopusfile, libopusenc + * + */ +#include + +#include + +#ifdef HAVE_OPUSFILE_ENCODE +#include +#endif + +#define OPUSFILE_MAX 32*1024 +#define TC_BUFFER_SIZE 1024 * 128 + + +SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load); +SWITCH_MODULE_DEFINITION(mod_opusfile, mod_opusfile_load, NULL, NULL); + +struct opus_file_context { + switch_file_t *fd; + OggOpusFile *of; + ogg_int64_t duration; + int output_seekable; + ogg_int64_t pcm_offset; + ogg_int64_t pcm_print_offset; + ogg_int64_t next_pcm_offset; + ogg_int64_t nsamples; + opus_int32 bitrate; + int li; + int prev_li; + switch_mutex_t *audio_mutex; + switch_buffer_t *audio_buffer; + unsigned char decode_buf[OPUSFILE_MAX]; + int eof; + switch_thread_rwlock_t *rwlock; + switch_file_handle_t *handle; + size_t samplerate; + int channels; + size_t buffer_seconds; + opus_int16 *opusbuf; + switch_size_t opusbuflen; + FILE *fp; +#ifdef HAVE_OPUSFILE_ENCODE + OggOpusEnc *enc; + OggOpusComments *comments; +#endif + switch_memory_pool_t *pool; +}; + +typedef struct opus_file_context opus_file_context; + +static struct { + int debug; +} globals; + +static switch_status_t switch_opusfile_decode(opus_file_context *context, void *data, size_t bytes, int channels) +{ + int ret; + + while (!(context->eof) && switch_buffer_inuse(context->audio_buffer) < bytes) { + + if (channels == 1) { + ret = op_read(context->of, (opus_int16 *)context->decode_buf, sizeof(context->decode_buf), NULL); + } else if (channels > 1) { + ret = op_read_stereo(context->of, (opus_int16 *)context->decode_buf, sizeof(context->decode_buf)); + + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Invalid number of channels"); + return SWITCH_STATUS_FALSE; + } + + if (ret < 0) { + switch(ret) { + case OP_HOLE: /* There was a hole in the data, and some samples may have been skipped. Call this function again to continue decoding past the hole.*/ + case OP_EREAD: /*An underlying read operation failed. This may signal a truncation attack from an source.*/ + + case OP_EFAULT: /* An internal memory allocation failed. */ + + case OP_EIMPL: /*An unseekable stream encountered a new link that used a feature that is not implemented, such as an unsupported channel family.*/ + + case OP_EINVAL: /* The stream was only partially open. */ + + case OP_ENOTFORMAT: /* An unseekable stream encountered a new link that did not have any logical Opus streams in it. */ + + case OP_EBADHEADER: /*An unseekable stream encountered a new link with a required header packet that was not properly formatted, contained illegal values, or was missing altogether.*/ + + case OP_EVERSION: /*An unseekable stream encountered a new link with an ID header that contained an unrecognized version number.*/ + + case OP_EBADPACKET: /*Failed to properly decode the next packet.*/ + + case OP_EBADLINK: /*We failed to find data we had seen before.*/ + + case OP_EBADTIMESTAMP: /*An unseekable stream encountered a new link with a starting timestamp that failed basic validity checks.*/ + + default: + goto err; + break; + } + } else if (ret == 0) { + /*The number of samples returned may be 0 if the buffer was too small to store even a single sample for both channels, or if end-of-file was reached*/ + context->eof = 1; + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS file]: EOF reached\n"); + } + break; + } else /* (ret > 0)*/ { + /*The number of samples read per channel on success*/ + switch_buffer_write(context->audio_buffer, context->decode_buf, ret * sizeof(int16_t) * channels); + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, + "[OGG/OPUS File]: Read samples: [%d]. Wrote bytes to audio buffer: [%d]\n", ret, (int)(ret * sizeof(int16_t) * channels)); + } + + } + } + return SWITCH_STATUS_SUCCESS; + +err: + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File]: error decoding file: [%d]\n", ret); + + return SWITCH_STATUS_FALSE; +} + + +static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const char *path) +{ + opus_file_context *context; + char *ext; + unsigned int flags = 0; + int ret; + switch_status_t status = SWITCH_STATUS_SUCCESS; + + if ((ext = strrchr(path, '.')) == 0) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Invalid Format\n"); + return SWITCH_STATUS_GENERR; + } + ext++; + + if ((context = switch_core_alloc(handle->memory_pool, sizeof(*context))) == 0) { + return SWITCH_STATUS_MEMERR; + } + + context->pool = handle->memory_pool; + + switch_thread_rwlock_create(&(context->rwlock), context->pool); + + switch_thread_rwlock_rdlock(context->rwlock); + + switch_mutex_init(&context->audio_mutex, SWITCH_MUTEX_NESTED, context->pool); + + if (switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE)) { + flags |= SWITCH_FOPEN_WRITE | SWITCH_FOPEN_CREATE; + if (switch_test_flag(handle, SWITCH_FILE_WRITE_APPEND) || switch_test_flag(handle, SWITCH_FILE_WRITE_OVER)) { + flags |= SWITCH_FOPEN_READ; + } else { + flags |= SWITCH_FOPEN_TRUNCATE; + } + } + + if (switch_test_flag(handle, SWITCH_FILE_FLAG_READ)) { + if (switch_buffer_create_dynamic(&context->audio_buffer, TC_BUFFER_SIZE, TC_BUFFER_SIZE * 2, 0) != SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Memory Error!\n"); + switch_goto_status(SWITCH_STATUS_GENERR, out); + } + + flags |= SWITCH_FOPEN_READ; + } + + handle->samples = 0; + handle->samplerate = context->samplerate = 48000; + handle->format = 0; + handle->sections = 0; + handle->seekable = 1; + handle->speed = 0; + handle->pos = 0; + handle->private_info = context; + context->handle = handle; + memcpy(handle->file_path, path, strlen(path)); + +#ifdef HAVE_OPUSFILE_ENCODE + if (switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE)) { + int err_open; + + context->channels = handle->channels; + context->samplerate = handle->samplerate; + handle->seekable = 0; + context->comments = ope_comments_create(); + ope_comments_add(context->comments, "METADATA", "Freeswitch/mod_opusfile"); + context->enc = ope_encoder_create_file(handle->file_path, context->comments, context->samplerate, context->channels, 0, &err_open); + if (!context->enc) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Can't open file for writing\n"); + switch_goto_status(SWITCH_STATUS_FALSE, out); + } + goto out; + } +#endif + + context->of = op_open_file(path, &ret); + if (!context->of) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Error opening %s\n", path); + switch_goto_status(SWITCH_STATUS_GENERR, out); + } + + if (switch_test_flag(handle, SWITCH_FILE_WRITE_APPEND)) { + op_pcm_seek(context->of, 0); // overwrite + handle->pos = 0; + } + + context->prev_li = -1; + context->nsamples = 0; + + handle->channels = context->channels = op_channel_count(context->of, -1); + context->pcm_offset = op_pcm_tell(context->of); + + if(context->pcm_offset!=0){ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS File] Non-zero starting PCM offset: [%li]\n", (long)context->pcm_offset); + } + context->pcm_print_offset = context->pcm_offset - 48000; + context->bitrate = 0; + context->buffer_seconds = 1; + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Opening File [%s] %dhz\n", path, handle->samplerate); + + context->li = op_current_link(context->of); + + if (context->li != context->prev_li) { + const OpusHead *head; + const OpusTags *tags; + head=op_head(context->of, context->li); + if (head) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Channels: %i\n", head->channel_count); + if (head->input_sample_rate) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Original sampling rate: %lu Hz\n", (unsigned long)head->input_sample_rate); + } + } + if (op_seekable(context->of)) { + ogg_int64_t duration; + opus_int64 size; + duration = op_pcm_total(context->of, context->li); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO , "[OGG/OPUS File] Duration (samples): %u", (unsigned int)duration); + size = op_raw_total(context->of, context->li); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,"[OGG/OPUS File] Size (bytes): %u", (unsigned int)size); + } + tags = op_tags(context->of, context->li); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Encoded by: %s\n", tags->vendor); + } + +out: + switch_thread_rwlock_unlock(context->rwlock); + + return status; +} + +static switch_status_t switch_opusfile_close(switch_file_handle_t *handle) +{ + opus_file_context *context = handle->private_info; + + if (context->of) { + op_free(context->of); + } +#ifdef HAVE_OPUSFILE_ENCODE + if (context->enc) { + ope_encoder_drain(context->enc); + ope_encoder_destroy(context->enc); + } + if (context->comments) { + ope_comments_destroy(context->comments); + } +#endif + if (context->audio_buffer) { + switch_buffer_destroy(&context->audio_buffer); + } + return SWITCH_STATUS_SUCCESS; +} + +static switch_status_t switch_opusfile_seek(switch_file_handle_t *handle, unsigned int *cur_sample, int64_t samples, int whence) +{ + int ret; + opus_file_context *context = handle->private_info; + + if (handle->handler || switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE)) { + return SWITCH_STATUS_FALSE; + } else { + if (whence == SWITCH_SEEK_CUR) { + samples -= switch_buffer_inuse(context->audio_buffer) / sizeof(int16_t); + } + switch_buffer_zero(context->audio_buffer); + ret = op_pcm_seek(context->of, samples); + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"[OGG/OPUS File] seek samples: [%u]", (unsigned int)samples); + } + if (ret == 0) { + handle->pos = *cur_sample = samples; + return SWITCH_STATUS_SUCCESS; + } + } + return SWITCH_STATUS_FALSE; +} + +static switch_status_t switch_opusfile_read(switch_file_handle_t *handle, void *data, size_t *len) +{ + opus_file_context *context = handle->private_info; + size_t bytes = *len * sizeof(int16_t) * handle->real_channels; + size_t rb = 0, newbytes; + + if (!context) { + return SWITCH_STATUS_FALSE; + } + + if (!handle->handler) { + if (switch_opusfile_decode(context, data, bytes , handle->real_channels) == SWITCH_STATUS_FALSE) { + context->eof = 1; + } + } + switch_mutex_lock(context->audio_mutex); + rb = switch_buffer_read(context->audio_buffer, data, bytes); + switch_mutex_unlock(context->audio_mutex); + + if (!rb && (context->eof)) { + return SWITCH_STATUS_FALSE; + } + if (rb) { + *len = rb / sizeof(int16_t) / handle->real_channels; + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS File] rb: [%d] *len: [%d]\n", (int)rb, (int)*len); + } + } else { + newbytes = (2 * handle->samplerate * handle->real_channels) * context->buffer_seconds; + if (newbytes < bytes) { + bytes = newbytes; + } + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS File] Padding with empty audio. seconds: [%d] bytes: [%d] newbytes: [%d] real_channels: [%d]\n", (int)context->buffer_seconds, (int)bytes, (int)newbytes, (int)handle->real_channels); + } + memset(data, 255, bytes); + *len = bytes / sizeof(int16_t) / handle->real_channels; + } + + handle->pos += *len; + handle->sample_count += *len; + + return SWITCH_STATUS_SUCCESS; +} + +static switch_status_t switch_opusfile_write(switch_file_handle_t *handle, void *data, size_t *len) +{ +#ifdef HAVE_OPUSFILE_ENCODE + size_t nsamples = *len; + int err_open; + int ret; + + opus_file_context *context = handle->private_info; + + if (!handle) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error no handle\n"); + return SWITCH_STATUS_FALSE; + } + + if (!(context = handle->private_info)) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error no context\n"); + return SWITCH_STATUS_FALSE; + } + context->comments = ope_comments_create(); + ope_comments_add(context->comments, "METADATA", "Freeswitch/mod_opusfile"); + if (!context->enc) { + context->enc = ope_encoder_create_file(handle->file_path, context->comments, handle->samplerate, handle->channels, 0, &err_open); + if (!context->enc) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Can't open file for writing\n"); + return SWITCH_STATUS_FALSE; + } + } + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"[OGG/OPUS File] write nsamples: [%d]", (int)nsamples); + } + + ret = ope_encoder_write(context->enc, (opus_int16 *)data, nsamples); + if (ret != OPE_OK) { + return SWITCH_STATUS_FALSE; + } + + handle->sample_count += *len; +#endif + return SWITCH_STATUS_SUCCESS; +} + +static switch_status_t switch_opusfile_set_string(switch_file_handle_t *handle, switch_audio_col_t col, const char *string) +{ + return SWITCH_STATUS_FALSE; +} + +static switch_status_t switch_opusfile_get_string(switch_file_handle_t *handle, switch_audio_col_t col, const char **string) +{ + return SWITCH_STATUS_FALSE; +} + +#define OPUSFILE_DEBUG_SYNTAX "" +SWITCH_STANDARD_API(mod_opusfile_debug) +{ + if (zstr(cmd)) { + stream->write_function(stream, "-USAGE: %s\n", OPUSFILE_DEBUG_SYNTAX); + } else { + if (!strcasecmp(cmd, "on")) { + globals.debug = 1; + stream->write_function(stream, "OPUSFILE Debug: on\n"); + } else if (!strcasecmp(cmd, "off")) { + globals.debug = 0; + stream->write_function(stream, "OPUSFILE Debug: off\n"); + } else { + stream->write_function(stream, "-USAGE: %s\n", OPUSFILE_DEBUG_SYNTAX); + } + } + return SWITCH_STATUS_SUCCESS; +} + +/* Registration */ + +static char *supported_formats[SWITCH_MAX_CODECS] = { 0 }; + +SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load) +{ + switch_file_interface_t *file_interface; + switch_api_interface_t *commands_api_interface; + + supported_formats[0] = "opus"; + + *module_interface = switch_loadable_module_create_module_interface(pool, modname); + + SWITCH_ADD_API(commands_api_interface, "opusfile_debug", "Set OPUSFILE Debug", mod_opusfile_debug, OPUSFILE_DEBUG_SYNTAX); + + switch_console_set_complete("add opusfile_debug on"); + switch_console_set_complete("add opusfile_debug off"); + + file_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_FILE_INTERFACE); + file_interface->interface_name = modname; + file_interface->extens = supported_formats; + file_interface->file_open = switch_opusfile_open; + file_interface->file_close = switch_opusfile_close; + file_interface->file_read = switch_opusfile_read; + file_interface->file_write = switch_opusfile_write; + file_interface->file_seek = switch_opusfile_seek; + file_interface->file_set_string = switch_opusfile_set_string; + file_interface->file_get_string = switch_opusfile_get_string; + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "mod_opusfile loaded\n"); + + /* indicate that the module should continue to be loaded */ + return SWITCH_STATUS_SUCCESS; +} + +/* 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 noet: + */ From ed2b3b7bb4a6ff0251452e519ff8cf309620770f Mon Sep 17 00:00:00 2001 From: Dragos Oancea Date: Fri, 13 Apr 2018 04:52:59 -0400 Subject: [PATCH 2/2] FS-10530: [mod_opusfile]: multichannel --- src/mod/formats/mod_opusfile/mod_opusfile.c | 144 +++++++++++++------- 1 file changed, 94 insertions(+), 50 deletions(-) diff --git a/src/mod/formats/mod_opusfile/mod_opusfile.c b/src/mod/formats/mod_opusfile/mod_opusfile.c index 9487044628..418628b8e4 100644 --- a/src/mod/formats/mod_opusfile/mod_opusfile.c +++ b/src/mod/formats/mod_opusfile/mod_opusfile.c @@ -37,9 +37,12 @@ #include #endif -#define OPUSFILE_MAX 32*1024 -#define TC_BUFFER_SIZE 1024 * 128 +#define OPUSFILE_MAX 32*1024 +#define TC_BUFFER_SIZE 1024 * 256 /* max ammount of decoded audio we can have at a time (bytes)*/ +#define DEFAULT_RATE 48000 /* default fullband */ +#define OPUS_MAX_PCM 5760 /* opus recommended max output buf */ +//#undef HAVE_OPUSFILE_ENCODE /*don't encode anything */ SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load); SWITCH_MODULE_DEFINITION(mod_opusfile, mod_opusfile_load, NULL, NULL); @@ -58,19 +61,26 @@ struct opus_file_context { int prev_li; switch_mutex_t *audio_mutex; switch_buffer_t *audio_buffer; - unsigned char decode_buf[OPUSFILE_MAX]; - int eof; + switch_mutex_t *ogg_mutex; + switch_buffer_t *ogg_buffer; + opus_int16 decode_buf[OPUS_MAX_PCM]; + switch_bool_t eof; switch_thread_rwlock_t *rwlock; switch_file_handle_t *handle; size_t samplerate; + int frame_size; int channels; size_t buffer_seconds; + size_t err; opus_int16 *opusbuf; switch_size_t opusbuflen; FILE *fp; #ifdef HAVE_OPUSFILE_ENCODE OggOpusEnc *enc; OggOpusComments *comments; + unsigned char encode_buf[OPUSFILE_MAX]; + int encoded_buflen; + size_t samples_encode; #endif switch_memory_pool_t *pool; }; @@ -81,22 +91,30 @@ static struct { int debug; } globals; -static switch_status_t switch_opusfile_decode(opus_file_context *context, void *data, size_t bytes, int channels) +static switch_status_t switch_opusfile_decode(opus_file_context *context, void *data, size_t max_bytes, int channels) { - int ret; + int ret = 0; + size_t buf_inuse; - while (!(context->eof) && switch_buffer_inuse(context->audio_buffer) < bytes) { + if (!context->of) { + return SWITCH_STATUS_FALSE; + } + + memset(context->decode_buf, 0, sizeof(context->decode_buf)); + switch_mutex_lock(context->audio_mutex); + while (!(context->eof) && (buf_inuse = switch_buffer_inuse(context->audio_buffer)) <= max_bytes) { if (channels == 1) { - ret = op_read(context->of, (opus_int16 *)context->decode_buf, sizeof(context->decode_buf), NULL); - } else if (channels > 1) { - ret = op_read_stereo(context->of, (opus_int16 *)context->decode_buf, sizeof(context->decode_buf)); - - } else { + ret = op_read(context->of, (opus_int16 *)context->decode_buf, OPUS_MAX_PCM, NULL); + } else if (channels == 2) { + ret = op_read_stereo(context->of, (opus_int16 *)context->decode_buf, OPUS_MAX_PCM); + } else if (channels > 2) { + ret = op_read(context->of, (opus_int16 *)context->decode_buf, OPUS_MAX_PCM, NULL); + } else if ((channels > 255) || (channels < 1)) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Invalid number of channels"); + switch_mutex_unlock(context->audio_mutex); return SWITCH_STATUS_FALSE; } - if (ret < 0) { switch(ret) { case OP_HOLE: /* There was a hole in the data, and some samples may have been skipped. Call this function again to continue decoding past the hole.*/ @@ -121,33 +139,30 @@ static switch_status_t switch_opusfile_decode(opus_file_context *context, void * case OP_EBADTIMESTAMP: /*An unseekable stream encountered a new link with a starting timestamp that failed basic validity checks.*/ default: - goto err; - break; + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Decoder]: error decoding file: [%d]\n", ret); + switch_mutex_unlock(context->audio_mutex); + return SWITCH_STATUS_FALSE; } } else if (ret == 0) { /*The number of samples returned may be 0 if the buffer was too small to store even a single sample for both channels, or if end-of-file was reached*/ - context->eof = 1; if (globals.debug) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS file]: EOF reached\n"); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Decoder]: EOF reached [%d]\n", ret); } + context->eof = TRUE; break; } else /* (ret > 0)*/ { /*The number of samples read per channel on success*/ - switch_buffer_write(context->audio_buffer, context->decode_buf, ret * sizeof(int16_t) * channels); + switch_buffer_write(context->audio_buffer, (opus_int16 *)context->decode_buf, ret * sizeof(opus_int16) * channels); if (globals.debug) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, - "[OGG/OPUS File]: Read samples: [%d]. Wrote bytes to audio buffer: [%d]\n", ret, (int)(ret * sizeof(int16_t) * channels)); + "[OGG/OPUS Decoder]: Read samples: %d. Wrote bytes to buffer: [%d] bytes in use: [%u]\n", ret, (int)(ret * sizeof(int16_t) * channels), (unsigned int)buf_inuse); } - } } + switch_mutex_unlock(context->audio_mutex); + context->eof = FALSE; // for next page return SWITCH_STATUS_SUCCESS; - -err: - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File]: error decoding file: [%d]\n", ret); - - return SWITCH_STATUS_FALSE; } @@ -157,7 +172,6 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const char *ext; unsigned int flags = 0; int ret; - switch_status_t status = SWITCH_STATUS_SUCCESS; if ((ext = strrchr(path, '.')) == 0) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Invalid Format\n"); @@ -189,14 +203,14 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const if (switch_test_flag(handle, SWITCH_FILE_FLAG_READ)) { if (switch_buffer_create_dynamic(&context->audio_buffer, TC_BUFFER_SIZE, TC_BUFFER_SIZE * 2, 0) != SWITCH_STATUS_SUCCESS) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Memory Error!\n"); - switch_goto_status(SWITCH_STATUS_GENERR, out); + goto err; } flags |= SWITCH_FOPEN_READ; } handle->samples = 0; - handle->samplerate = context->samplerate = 48000; + handle->samplerate = context->samplerate = DEFAULT_RATE; /*open files at 48 khz always*/ handle->format = 0; handle->sections = 0; handle->seekable = 1; @@ -208,26 +222,34 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const #ifdef HAVE_OPUSFILE_ENCODE if (switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE)) { - int err_open; + int err; int mapping_family = 0; context->channels = handle->channels; context->samplerate = handle->samplerate; handle->seekable = 0; context->comments = ope_comments_create(); ope_comments_add(context->comments, "METADATA", "Freeswitch/mod_opusfile"); - context->enc = ope_encoder_create_file(handle->file_path, context->comments, context->samplerate, context->channels, 0, &err_open); - if (!context->enc) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Can't open file for writing\n"); - switch_goto_status(SWITCH_STATUS_FALSE, out); + // opus_multistream_surround_encoder_get_size() in libopus will check these + if ((context->channels > 2) && (context->channels <= 8)) { + mapping_family = 1; + } else if ((context->channels > 8) && (context->channels <= 255)) { + mapping_family = 255; } - goto out; + context->enc = ope_encoder_create_file(handle->file_path, context->comments, context->samplerate, context->channels, mapping_family, &err); + if (!context->enc) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Can't open file for writing [%d] [%s]\n", err, ope_strerror(err)); + switch_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_FALSE; + } + switch_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_SUCCESS; } #endif context->of = op_open_file(path, &ret); if (!context->of) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Error opening %s\n", path); - switch_goto_status(SWITCH_STATUS_GENERR, out); + return SWITCH_STATUS_GENERR; } if (switch_test_flag(handle, SWITCH_FILE_WRITE_APPEND)) { @@ -244,7 +266,7 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const if(context->pcm_offset!=0){ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS File] Non-zero starting PCM offset: [%li]\n", (long)context->pcm_offset); } - context->pcm_print_offset = context->pcm_offset - 48000; + context->pcm_print_offset = context->pcm_offset - DEFAULT_RATE; context->bitrate = 0; context->buffer_seconds = 1; @@ -260,6 +282,7 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Channels: %i\n", head->channel_count); if (head->input_sample_rate) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Original sampling rate: %lu Hz\n", (unsigned long)head->input_sample_rate); + handle->samplerate = context->samplerate = head->input_sample_rate; } } if (op_seekable(context->of)) { @@ -274,31 +297,35 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS File] Encoded by: %s\n", tags->vendor); } -out: + switch_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_SUCCESS; + +err: switch_thread_rwlock_unlock(context->rwlock); - return status; + return SWITCH_STATUS_FALSE; } static switch_status_t switch_opusfile_close(switch_file_handle_t *handle) { opus_file_context *context = handle->private_info; + switch_thread_rwlock_rdlock(context->rwlock); if (context->of) { op_free(context->of); } #ifdef HAVE_OPUSFILE_ENCODE if (context->enc) { - ope_encoder_drain(context->enc); ope_encoder_destroy(context->enc); } if (context->comments) { ope_comments_destroy(context->comments); } -#endif +#endif if (context->audio_buffer) { switch_buffer_destroy(&context->audio_buffer); } + switch_thread_rwlock_unlock(context->rwlock); return SWITCH_STATUS_SUCCESS; } @@ -337,7 +364,7 @@ static switch_status_t switch_opusfile_read(switch_file_handle_t *handle, void * } if (!handle->handler) { - if (switch_opusfile_decode(context, data, bytes , handle->real_channels) == SWITCH_STATUS_FALSE) { + if (switch_opusfile_decode(context, data, bytes, handle->real_channels) == SWITCH_STATUS_FALSE) { context->eof = 1; } } @@ -359,7 +386,9 @@ static switch_status_t switch_opusfile_read(switch_file_handle_t *handle, void * bytes = newbytes; } if (globals.debug) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS File] Padding with empty audio. seconds: [%d] bytes: [%d] newbytes: [%d] real_channels: [%d]\n", (int)context->buffer_seconds, (int)bytes, (int)newbytes, (int)handle->real_channels); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, + "[OGG/OPUS File] Padding with empty audio. seconds: [%d] bytes: [%d] newbytes: [%d] real_channels: [%d]\n", + (int)context->buffer_seconds, (int)bytes, (int)newbytes, (int)handle->real_channels); } memset(data, 255, bytes); *len = bytes / sizeof(int16_t) / handle->real_channels; @@ -375,8 +404,8 @@ static switch_status_t switch_opusfile_write(switch_file_handle_t *handle, void { #ifdef HAVE_OPUSFILE_ENCODE size_t nsamples = *len; - int err_open; - int ret; + int err; + int mapping_family = 0; opus_file_context *context = handle->private_info; @@ -389,12 +418,17 @@ static switch_status_t switch_opusfile_write(switch_file_handle_t *handle, void switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error no context\n"); return SWITCH_STATUS_FALSE; } - context->comments = ope_comments_create(); - ope_comments_add(context->comments, "METADATA", "Freeswitch/mod_opusfile"); + if (!context->comments) { + context->comments = ope_comments_create(); + ope_comments_add(context->comments, "METADATA", "Freeswitch/mod_opusfile"); + } + if (context->channels > 2) { + mapping_family = 1; + } if (!context->enc) { - context->enc = ope_encoder_create_file(handle->file_path, context->comments, handle->samplerate, handle->channels, 0, &err_open); + context->enc = ope_encoder_create_file(handle->file_path, context->comments, handle->samplerate, handle->channels, mapping_family, &err); if (!context->enc) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Can't open file for writing\n"); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Can't open file for writing. err: [%d] [%s]\n", err, ope_strerror(err)); return SWITCH_STATUS_FALSE; } } @@ -403,8 +437,10 @@ static switch_status_t switch_opusfile_write(switch_file_handle_t *handle, void switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"[OGG/OPUS File] write nsamples: [%d]", (int)nsamples); } - ret = ope_encoder_write(context->enc, (opus_int16 *)data, nsamples); - if (ret != OPE_OK) { + err = ope_encoder_write(context->enc, (opus_int16 *)data, nsamples); + + if (err != OPE_OK) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Can't encode. err: [%d] [%s]", err, ope_strerror(err)); return SWITCH_STATUS_FALSE; } @@ -423,6 +459,7 @@ static switch_status_t switch_opusfile_get_string(switch_file_handle_t *handle, return SWITCH_STATUS_FALSE; } + #define OPUSFILE_DEBUG_SYNTAX "" SWITCH_STANDARD_API(mod_opusfile_debug) { @@ -432,6 +469,9 @@ SWITCH_STANDARD_API(mod_opusfile_debug) if (!strcasecmp(cmd, "on")) { globals.debug = 1; stream->write_function(stream, "OPUSFILE Debug: on\n"); +#ifdef HAVE_OPUSFILE_ENCODE + stream->write_function(stream, "Library version (encoding): %s\n", ope_get_version_string()); +#endif } else if (!strcasecmp(cmd, "off")) { globals.debug = 0; stream->write_function(stream, "OPUSFILE Debug: off\n"); @@ -460,6 +500,8 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load) switch_console_set_complete("add opusfile_debug on"); switch_console_set_complete("add opusfile_debug off"); + globals.debug = 0; + file_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_FILE_INTERFACE); file_interface->interface_name = modname; file_interface->extens = supported_formats; @@ -471,6 +513,7 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load) file_interface->file_set_string = switch_opusfile_set_string; file_interface->file_get_string = switch_opusfile_get_string; + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "mod_opusfile loaded\n"); /* indicate that the module should continue to be loaded */ @@ -487,3 +530,4 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load) * For VIM: * vim:set softtabstop=4 shiftwidth=4 tabstop=4 noet: */ +