From 37ab323c13ca95a2abefc7e1af863402d5a4307f Mon Sep 17 00:00:00 2001 From: Dragos Oancea Date: Thu, 12 Mar 2020 14:41:31 +0000 Subject: [PATCH 1/3] [mod_opusfile] add ogg/opus streams, fix Makefile for encoding. [mod_opusfile] add stats, opusctl [mod_opusfile] add unit-test using teletone --- src/mod/formats/mod_opusfile/Makefile.am | 6 + src/mod/formats/mod_opusfile/mod_opusfile.c | 725 +++++++++++++++++- .../formats/mod_opusfile/test/freeswitch.xml | 1 + .../audiocheck.net_sin_1000Hz_-3dBFS_6s.opus | Bin 0 -> 74858 bytes .../audiocheck.net_sin_1000Hz_-3dBFS_6s.wav | Bin 0 -> 576046 bytes .../test/sounds/opusfile-test-ogg.bitstream | Bin 0 -> 31381 bytes .../formats/mod_opusfile/test/test_opusfile.c | 193 +++++ 7 files changed, 905 insertions(+), 20 deletions(-) create mode 100644 src/mod/formats/mod_opusfile/test/sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.opus create mode 100644 src/mod/formats/mod_opusfile/test/sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.wav create mode 100644 src/mod/formats/mod_opusfile/test/sounds/opusfile-test-ogg.bitstream diff --git a/src/mod/formats/mod_opusfile/Makefile.am b/src/mod/formats/mod_opusfile/Makefile.am index 7c776f9d56..6f4919c5b2 100644 --- a/src/mod/formats/mod_opusfile/Makefile.am +++ b/src/mod/formats/mod_opusfile/Makefile.am @@ -22,6 +22,12 @@ noinst_PROGRAMS = test/test_opusfile test_test_opusfile_SOURCES = test/test_opusfile.c test_test_opusfile_CFLAGS = $(AM_CFLAGS) -I./ -I../ -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" $(OPUSFILE_DECODE_CFLAGS) test_test_opusfile_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) $(OPUSFILE_DECODE_LIBS) + +if HAVE_OPUSFILE_ENCODE +test_test_opusfile_CFLAGS += $(OPUSFILE_ENCODE_CFLAGS) -DHAVE_OPUSFILE_ENCODE +test_test_opusfile_LDFLAGS += $(OPUSFILE_ENCODE_LIBS) +endif + test_test_opusfile_LDADD = libopusfilemod.la $(switch_builddir)/libfreeswitch.la TESTS = $(noinst_PROGRAMS) diff --git a/src/mod/formats/mod_opusfile/mod_opusfile.c b/src/mod/formats/mod_opusfile/mod_opusfile.c index 25246a7ced..ea932ed0ff 100644 --- a/src/mod/formats/mod_opusfile/mod_opusfile.c +++ b/src/mod/formats/mod_opusfile/mod_opusfile.c @@ -42,6 +42,18 @@ #define DEFAULT_RATE 48000 /* default fullband */ #define OPUS_MAX_PCM 5760 /* opus recommended max output buf */ +#define OPUSSTREAM_MAX 64*1024 +#define OGG_MIN_PAGE_SIZE 2400 // this much data buffered before trying to open the incoming stream +#define OGG_MAX_PAGE_SIZE 65307 // a bit less than 64k, standard ogg + +#define PAGES_PER_SEC 4 + +//#define LIMIT_DROP + +#ifdef LIMIT_DROP +#define MIN_OGG_PAYLOAD 40 // drop incoming frames smaller than this (decoder) +#endif + //#undef HAVE_OPUSFILE_ENCODE /*don't encode anything */ SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load); @@ -87,6 +99,53 @@ struct opus_file_context { typedef struct opus_file_context opus_file_context; +struct opus_stream_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; + opus_int64 raw_offset; + ogg_int64_t nsamples; + opus_int32 bitrate; + int li; + int prev_li; + switch_mutex_t *audio_mutex; + switch_buffer_t *audio_buffer; + switch_mutex_t *ogg_mutex; + switch_buffer_t *ogg_buffer; + unsigned char ogg_data[OGG_MAX_PAGE_SIZE * 2]; + unsigned int ogg_data_len; + switch_bool_t read_stream; + switch_bool_t dec_page_ready; + 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 dec_channels; + size_t err; + opus_int16 *opusbuf; + switch_size_t opusbuflen; +#ifdef HAVE_OPUSFILE_ENCODE + OggOpusEnc *enc; + OggOpusComments *comments; + unsigned char encode_buf[OPUSSTREAM_MAX]; + int encoded_buflen; + size_t samples_encode; + int enc_channels; + unsigned int enc_pagecount; +#endif + unsigned int dec_count; + switch_thread_t *read_stream_thread; + switch_memory_pool_t *pool; +}; + +typedef struct opus_stream_context opus_stream_context_t; + static struct { int debug; } globals; @@ -169,7 +228,6 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const { opus_file_context *context; char *ext; - unsigned int flags = 0; int ret; if ((ext = strrchr(path, '.')) == 0) { @@ -190,22 +248,11 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const 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"); goto err; } - - flags |= SWITCH_FOPEN_READ; } handle->samples = 0; @@ -289,9 +336,9 @@ static switch_status_t switch_opusfile_open(switch_file_handle_t *handle, const 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); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO , "[OGG/OPUS File] Duration (samples): %u\n", (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); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,"[OGG/OPUS File] Size (bytes): %u\n", (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); @@ -343,7 +390,7 @@ static switch_status_t switch_opusfile_seek(switch_file_handle_t *handle, unsign 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); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"[OGG/OPUS File] seek samples: [%u]\n", (unsigned int)samples); } if (ret == 0) { handle->pos = *cur_sample = samples; @@ -416,7 +463,7 @@ static switch_status_t switch_opusfile_write(switch_file_handle_t *handle, void int err; int mapping_family = 0; - opus_file_context *context = handle->private_info; + opus_file_context *context; if (!handle) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error no handle\n"); @@ -443,17 +490,19 @@ static switch_status_t switch_opusfile_write(switch_file_handle_t *handle, void } if (globals.debug) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"[OGG/OPUS File] write nsamples: [%d]", (int)nsamples); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"[OGG/OPUS File] write nsamples: [%d]\n", (int)nsamples); } 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)); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS File] Can't encode. err: [%d] [%s]\n", err, ope_strerror(err)); return SWITCH_STATUS_FALSE; } handle->sample_count += *len; +#else + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "[OGG/OPUS File] Encoding support not built-in, build the module with libopusenc!\n"); #endif return SWITCH_STATUS_SUCCESS; } @@ -479,7 +528,7 @@ SWITCH_STANDARD_API(mod_opusfile_debug) 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()); + stream->write_function(stream, "Library version (encoding): %s ABI: %s\n", ope_get_version_string(), ope_get_abi_version()); #endif } else if (!strcasecmp(cmd, "off")) { globals.debug = 0; @@ -491,6 +540,603 @@ SWITCH_STANDARD_API(mod_opusfile_debug) return SWITCH_STATUS_SUCCESS; } +static switch_status_t switch_opusstream_set_initial(opus_stream_context_t *context) +{ + /* https://www.opus-codec.org/docs/opusfile_api-0.5/group__stream__info.html#ga9272a4a6ac9e01fbc549008f5ff58b4c */ + + if (context->of) { + int ret; + /* docs: "Obtain the PCM offset of the next sample to be read. " */ + ret = op_pcm_tell(context->of); + if (ret != OP_EINVAL) { + context->pcm_offset = ret; + } + context->pcm_print_offset = context->pcm_offset - context->samplerate; + + /* docs: "Obtain the current value of the position indicator for _of." */ + ret = op_raw_tell(context->of); + if (ret != OP_EINVAL) { + context->raw_offset = ret; + } + + /* docs: "Get the channel count of the given link in a (possibly-chained) Ogg Opus stream. " */ + context->dec_channels = op_channel_count(context->of, -1); + if (context->dec_channels == 0) { + context->dec_channels = 1; + } + + context->samplerate = DEFAULT_RATE; + return SWITCH_STATUS_SUCCESS; + } + + return SWITCH_STATUS_FALSE; +} + +static switch_status_t switch_opusstream_stream_info(opus_stream_context_t *context) +{ + const OpusHead *head; + const OpusTags *tags; + opus_int32 bitrate; + + if (context->of) { + + /* docs: "Get the serial number of the given link in a (possibly-chained) Ogg Opus stream. "*/ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Decode] SerialNO: [%u]\n", op_serialno(context->of, -1)); + bitrate = op_bitrate_instant(context->of); + if (bitrate > 0) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Decode] Bitrate: [%d]\n", bitrate); + } + + if(context->pcm_offset!=0){ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] Non-zero starting PCM offset: [%li]\n", + (long)context->pcm_offset); + } + + /* docs: "Retrieve the index of the current link." */ + context->li = op_current_link(context->of); + + /* docs: "Get the ID header information for the given link in a (possibly chained) Ogg Opus stream. " */ + head = op_head(context->of, context->li); + if (head) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Decode] Channels: [%i]\n", head->channel_count); + if (head->input_sample_rate) { + context->samplerate = head->input_sample_rate; + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Decode] Original sampling rate: [%lu] Hz\n", + (unsigned long)head->input_sample_rate); + } + } + /*docs: "Returns whether or not the data source being read is seekable."*/ + if (op_seekable(context->of)) { + opus_int64 size; + context->duration = op_pcm_total(context->of, context->li); // page duration + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO , "[OGG/OPUS Stream Decode] Duration (samples): [%u]\n", (unsigned int)context->duration); + size = op_raw_total(context->of, context->li); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,"[OGG/OPUS Stream Decode] Size (bytes): [%u]\n", (unsigned int)size); + } + /* docs: "Get the comment header information for the given link in a (possibly chained) Ogg Opus stream." */ + tags = op_tags(context->of, context->li); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Decode] Encoded by: [%s]\n", tags->vendor); + return SWITCH_STATUS_FALSE; + } + return SWITCH_STATUS_SUCCESS; +} + +static switch_status_t switch_opusstream_stream_decode(opus_stream_context_t *context, void *data, int channels) +{ + int ret; + size_t buf_inuse; + switch_status_t status = SWITCH_STATUS_SUCCESS; + + 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)) { + + if (channels == 1) { + ret = op_read(context->of, (opus_int16 *)context->decode_buf, OPUS_MAX_PCM, NULL); + } else if (channels > 1) { + ret = op_read_stereo(context->of, (opus_int16 *)context->decode_buf, OPUS_MAX_PCM); + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream] Invalid number of channels!\n"); + switch_goto_status(SWITCH_STATUS_FALSE, end); + } + + 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.*/ + if (!context->dec_page_ready) { + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Decoder]: incomplete ogg page, will retry\n"); + } + switch_goto_status(SWITCH_STATUS_SUCCESS, end); + } + 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: + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Decoder]: error decoding stream: [%d]\n", ret); + switch_goto_status(SWITCH_STATUS_FALSE, end); + } + } 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*/ + if (globals.debug) { + 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, (opus_int16 *)context->decode_buf, ret * sizeof(opus_int16) * channels); + buf_inuse = switch_buffer_inuse(context->audio_buffer); + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, + "[OGG/OPUS Decoder]: Read samples: %d. Wrote bytes to buffer: [%d] bytes in use: [%u] byte pos stream: [%lu]\n", + ret, (int)(ret * sizeof(int16_t) * channels), (unsigned int)buf_inuse, (long unsigned int)op_raw_tell(context->of)); + } + } + } + +end: + context->eof = FALSE; // for next page + + switch_mutex_unlock(context->audio_mutex); + + return status; +} + +static switch_status_t switch_opusstream_init(switch_codec_t *codec, switch_codec_flag_t flags, const switch_codec_settings_t *codec_settings) +{ + struct opus_stream_context *context = NULL; + int encoding, decoding; +#ifdef HAVE_OPUSFILE_ENCODE + int err; +#endif + + encoding = (flags & SWITCH_CODEC_FLAG_ENCODE); + decoding = (flags & SWITCH_CODEC_FLAG_DECODE); + + if (!(encoding || decoding) || (!(context = switch_core_alloc(codec->memory_pool, sizeof(struct opus_stream_context))))) { + return SWITCH_STATUS_FALSE; + } else { + + memset(context, 0, sizeof(struct opus_stream_context)); + codec->private_info = context; + context->pool = codec->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); + switch_mutex_init(&context->ogg_mutex, SWITCH_MUTEX_NESTED, context->pool); + + 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_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_MEMERR; + } + + if (switch_buffer_create_dynamic(&context->ogg_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_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_MEMERR; + } + + context->samplerate = codec->implementation->actual_samples_per_second; + context->frame_size = codec->implementation->actual_samples_per_second * (codec->implementation->microseconds_per_packet / 1000) / 1000; + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream] frame_size: [%d]\n", (int)context->frame_size); + } +#ifdef HAVE_OPUSFILE_ENCODE + if (encoding) { + if (!context->comments) { + context->comments = ope_comments_create(); + ope_comments_add(context->comments, "METADATA", "Freeswitch/mod_opusfile"); + } + if (!context->enc) { + int mapping_family = 0; + // opus_multistream_surround_encoder_get_size() in libopus will check these + if ((context->enc_channels > 2) && (context->enc_channels <= 8)) { + mapping_family = 1; + } else if ((context->enc_channels > 8) && (context->enc_channels <= 255)) { + // multichannel/multistream mapping family . https://people.xiph.org/~giles/2013/draft-ietf-codec-oggopus.html#rfc.section.5.1.1 + mapping_family = 255; + } + context->enc = ope_encoder_create_pull(context->comments, !context->samplerate?DEFAULT_RATE:context->samplerate, !context->enc_channels?1:context->enc_channels, mapping_family, &err); + + if (!context->enc) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Encode] Can't create stream. err: [%d] [%s]\n", err, ope_strerror(err)); + switch_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_FALSE; + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Encode] Stream opened for encoding\n"); + } + ope_encoder_ctl(context->enc, OPUS_SET_COMPLEXITY_REQUEST, 5); + ope_encoder_ctl(context->enc, OPUS_SET_APPLICATION_REQUEST, OPUS_APPLICATION_VOIP); + } + } +#endif + switch_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_SUCCESS; + } +} + +static switch_status_t switch_opusstream_destroy(switch_codec_t *codec) +{ + struct opus_stream_context *context = codec->private_info; + switch_status_t st; + + switch_thread_rwlock_rdlock(context->rwlock); + + if (context->read_stream_thread) { + switch_thread_join(&st, context->read_stream_thread); + if (st == SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode/Decode] Joined decoding thread\n"); + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode/Decode] Can't join decoding thread\n"); + } + } + + if (context->of) { + op_free(context->of); + } + +#ifdef HAVE_OPUSFILE_ENCODE + if (context->enc) { + ope_encoder_destroy(context->enc); + } + if (context->comments) { + ope_comments_destroy(context->comments); + } + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode/Decode] Encoded pages: [%u]\n", context->enc_pagecount); +#endif + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode/Decode] Decoded chunks: [%u]\n", context->dec_count); + if (context->audio_buffer) { + switch_buffer_destroy(&context->audio_buffer); + } + if (context->ogg_buffer) { + switch_buffer_destroy(&context->ogg_buffer); + } + switch_thread_rwlock_unlock(context->rwlock); + codec->private_info = NULL; + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode/Decode] Stopped processing\n"); + return SWITCH_STATUS_SUCCESS; +} + +static switch_status_t switch_opusstream_encode(switch_codec_t *codec, + switch_codec_t *other_codec, + void *decoded_data, + uint32_t decoded_data_len, + uint32_t decoded_rate, + void *encoded_data, + uint32_t *encoded_data_len, + uint32_t *encoded_rate, + unsigned int *flag) +{ + switch_status_t status = SWITCH_STATUS_SUCCESS; +#ifdef HAVE_OPUSFILE_ENCODE + struct opus_stream_context *context = codec->private_info; + size_t nsamples = (int)decoded_data_len / sizeof(int16_t); + int err, ret; + int len = 0; int thres; + unsigned char *decode_buf = decoded_data; + + if (!context) { + return SWITCH_STATUS_FALSE; + } + + globals.debug = 0; + switch_thread_rwlock_rdlock(context->rwlock); + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, + "[OGG/OPUS Stream Encode] : switch_opusfile_stream_encode() decoded_data [%x][%x][%x][%x] nsamples: [%d]\n", + decode_buf[0], decode_buf[1], decode_buf[2], decode_buf[3], (int)nsamples); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode] stream write nsamples: [%d]\n", (int)nsamples); + } + if (context->enc_channels == 0) { + context->enc_channels = 1; + } + if (!context->samplerate) { + context->samplerate = DEFAULT_RATE; + } + + if (context->enc) { + // we reach here every 20 ms. + // decoded_data - this can be an interleaved buffer, to do multistream. we’ll need the exact number of channels too. + err = ope_encoder_write(context->enc, (opus_int16 *)decoded_data, nsamples / context->enc_channels); + if (err != OPE_OK) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Encode] can't encode, ret: [%d] [%s]\n", err, ope_strerror(err)); + switch_goto_status(SWITCH_STATUS_FALSE, end); + } + context->samples_encode += nsamples; + } + + thres = context->samplerate/PAGES_PER_SEC; + + if (!(context->samples_encode % thres) && context->samples_encode > context->samplerate) { + if (context->enc) { + unsigned char *vb = context->encode_buf; + int req_flush = 1; + /* OPE_EXPORT int ope_encoder_get_page(OggOpusEnc *enc, unsigned char **page, opus_int32 *len, int flush); */ + ret = ope_encoder_get_page(context->enc, &vb, &len, req_flush); + if (ret == 0) { + /* ope_encoder_get_page(): ret is 1 if there is a page available, 0 if not. */ + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode] can't retrieve encoded page, page not ready. ret: [%d]\n", ret); + } + switch_goto_status(SWITCH_STATUS_SUCCESS, end); + } else { + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Encode] retrieved page from encoder. ret [%d] len: [%d] [%p]\n", + ret, len, context->encode_buf); + } + if (len > OGG_MAX_PAGE_SIZE) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Encode] retrieved page bigger than ogg max size!\n"); + switch_goto_status(SWITCH_STATUS_FALSE, end); + } + memcpy(encoded_data, vb, len); + *encoded_data_len = len; + context->enc_pagecount++; + switch_thread_rwlock_unlock(context->rwlock); + return SWITCH_STATUS_SUCCESS; + } + } else { + switch_goto_status(SWITCH_STATUS_FALSE, end); + } + } +end: + *encoded_data_len = 0; + switch_thread_rwlock_unlock(context->rwlock); +#else + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "[OGG/OPUS Stream Encode] Encoding support not built-in, build the module with libopusenc!\n"); +#endif + return status; +} + +// decode_stream_cb(): nbytes is OP_READ_SIZE (builtin limit - libopusfile). +// this is being called by op_read() or op_read_stereo() - we’re giving chunks of pages to be decoded. +static int decode_stream_cb(void *dcontext, unsigned char *data, int nbytes) +{ + opus_stream_context_t *context = (opus_stream_context_t *)dcontext; + unsigned int ret = 0; + + if (!context) { + return 0; + } + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] decode CB called: context: %p data: %p packet_len: %d\n", + (void *)context, data, nbytes); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] decode_stream_cb(): switch_thread_self(): %lx\n", switch_thread_self()); + } + + switch_mutex_lock(context->ogg_mutex); + ret = switch_buffer_read(context->ogg_buffer, context->ogg_data, nbytes); + if (ret == 0) { + data = NULL; + switch_mutex_unlock(context->ogg_mutex); + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] No data. Wanted: [%d] bytes\n", nbytes); + } + return ret; + } + context->dec_count++; + memcpy(data, context->ogg_data, ret); + + if (switch_buffer_inuse(context->ogg_buffer)) { + context->dec_page_ready = 0; + } else { + context->dec_page_ready = 1; + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] buffer is empty, all pages passed to the decoder\n"); + } + + } + switch_mutex_unlock(context->ogg_mutex); + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] decode_stream_cb(): ret: %u\n", ret); + } + return ret; +} + +const OpusFileCallbacks cb={decode_stream_cb, NULL, NULL, NULL}; + +static void *SWITCH_THREAD_FUNC read_stream_thread(switch_thread_t *thread, void *obj) +{ + opus_stream_context_t *context = (opus_stream_context_t *) obj; + int err = 0; + OggOpusFile *temp_of = NULL; + int buffered_ogg_bytes; + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] read_stream_thread(): switch_thread_self(): 0x%lx\n", switch_thread_self()); + } + switch_thread_rwlock_rdlock(context->rwlock); + + if ((buffered_ogg_bytes = switch_buffer_inuse(context->ogg_buffer))) { + if (buffered_ogg_bytes <= OGG_MAX_PAGE_SIZE) { + switch_buffer_peek(context->ogg_buffer, context->ogg_data, buffered_ogg_bytes); + context->ogg_data_len = buffered_ogg_bytes; + } + } + + /* https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__open__close.html#gad183ecf5fbec5add3a5ccf1e3b1d2593 */ + /* docs: "Open a stream using the given set of callbacks to access it." */ + temp_of = op_open_callbacks(context, &cb, (const unsigned char *)context->ogg_data, context->ogg_data_len, &err); + if (temp_of && (err == 0)) { + context->dec_page_ready = 1; + context->of = temp_of; + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "[OGG/OPUS Stream Decode] Opened stream, installed decoding callback!\n"); + } + switch_opusstream_set_initial(context); + switch_opusstream_stream_info(context); + } else if (err != 0) { + switch (err) { + case OP_EREAD: + // An underlying read, seek, or tell operation failed when it should have succeeded, or we failed to find data in the stream we had seen before. + case OP_EFAULT: + // There was a memory allocation failure, or an internal library error. + case OP_EIMPL: + // The stream used a feature that is not implemented, such as an unsupported channel family. + case OP_EINVAL: + // seek() was implemented and succeeded on this source, but tell() did not, or the starting position indicator was not equal to _initial_bytes. + case OP_ENOTFORMAT: + // The stream contained a link that did not have any logical Opus streams in it. + case OP_EBADHEADER: + // A required header packet was not properly formatted, contained illegal values, or was missing altogether. + case OP_EVERSION: + // An ID header contained an unrecognized version number. + case OP_EBADLINK: + // We failed to find data we had seen before after seeking. + case OP_EBADTIMESTAMP: + // The first or last timestamp in a link failed basic validity checks + default: + context->dec_page_ready = 0; + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Decode] error opening stream: [%d]\n", err); + } + } + + switch_thread_rwlock_unlock(context->rwlock); + return NULL; +} + +static void launch_read_stream_thread(opus_stream_context_t *context) +{ + switch_threadattr_t *thd_attr = NULL; + + switch_threadattr_create(&thd_attr, context->pool); + switch_threadattr_stacksize_set(thd_attr, SWITCH_THREAD_STACKSIZE); + switch_thread_create(&context->read_stream_thread, thd_attr, read_stream_thread, context, context->pool); +} + +static switch_status_t switch_opusstream_decode(switch_codec_t *codec, + switch_codec_t *other_codec, + void *encoded_data, + uint32_t encoded_data_len, + uint32_t encoded_rate, + void *decoded_data, + uint32_t *decoded_data_len, + uint32_t *decoded_rate, + unsigned int *flag) +{ + struct opus_stream_context *context = codec->private_info; + size_t bytes = 0; + int ogg_bytes = OGG_MIN_PAGE_SIZE; // min page size before trying to open the incoming stream + size_t rb = 0; + unsigned char *encode_buf = encoded_data; + size_t buffered_ogg_bytes = 0; + switch_status_t status = SWITCH_STATUS_SUCCESS; + + if (!context) { + return SWITCH_STATUS_FALSE; + } + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, + "[OGG/OPUS Stream Decode] : switch_opusstream_decode() encoded_data [%x][%x][%x][%x] encoded_data_len: [%u]\n", + encode_buf[0], encode_buf[1], encode_buf[2], encode_buf[3], encoded_data_len); + } +#ifdef LIMIT_DROP + if ((encoded_data_len <= MIN_OGG_PAYLOAD) && (encoded_data_len > 0)) { + *decoded_data_len = 0; + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] switch_opusstream_decode(): drop [%u]", (unsigned int)encoded_data_len); + } + return SWITCH_STATUS_SUCCESS; + } +#endif + + switch_thread_rwlock_rdlock(context->rwlock); + memset(context->ogg_data, 0, sizeof(context->ogg_data)); + if (encoded_data_len <= SWITCH_RECOMMENDED_BUFFER_SIZE) { + switch_mutex_lock(context->ogg_mutex); + switch_buffer_write(context->ogg_buffer, encode_buf, encoded_data_len); + + if ((buffered_ogg_bytes = switch_buffer_inuse(context->ogg_buffer)) >= ogg_bytes) { + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, + "[OGG/OPUS Stream Decode] switch_opusstream_decode() encoded_data [%x][%x][%x][%x] encoded_data_len: %u buffered_ogg_bytes: [%u]\n", + encode_buf[0], encode_buf[1], encode_buf[2], encode_buf[3], encoded_data_len, (unsigned int)buffered_ogg_bytes); + } + if (buffered_ogg_bytes <= OGG_MAX_PAGE_SIZE) { + switch_buffer_peek(context->ogg_buffer, context->ogg_data, buffered_ogg_bytes); + context->ogg_data_len = buffered_ogg_bytes; + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Decode] buffered ogg data bigger than max OGG page size, will flush\n"); + *decoded_data_len = 0; + switch_buffer_zero(context->ogg_buffer); + switch_mutex_unlock(context->ogg_mutex); + switch_goto_status(SWITCH_STATUS_SUCCESS, end); + } + } + + switch_mutex_unlock(context->ogg_mutex); + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "[OGG/OPUS Stream Decode] too much data to buffer, flushing buffer!\n"); + *decoded_data_len = 0; + switch_buffer_zero(context->ogg_buffer); + switch_goto_status(SWITCH_STATUS_SUCCESS, end); + } + + if ((buffered_ogg_bytes >= ogg_bytes) && encoded_data_len) { + + if (!(op_test(NULL, context->ogg_data, buffered_ogg_bytes))) { + if (!context->read_stream && buffered_ogg_bytes > OGG_MIN_PAGE_SIZE) { + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] launching decoding thread\n"); + } + launch_read_stream_thread(context); + context->read_stream = 1; // mark thread started + } + } + } + if (context->of) { + if (switch_opusstream_stream_decode(context, context->ogg_data, context->dec_channels) == SWITCH_STATUS_FALSE) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Decode] Cannot decode stream\n"); + *decoded_data_len = 0; + switch_goto_status(SWITCH_STATUS_FALSE, end); + } + } + switch_mutex_lock(context->audio_mutex); + bytes = switch_buffer_inuse(context->audio_buffer); + rb = switch_buffer_read(context->audio_buffer, decoded_data, context->frame_size * sizeof(int16_t)); + switch_mutex_unlock(context->audio_mutex); + + if (globals.debug) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] rb (read from audio_buffer): [%d] bytes in audio buffer: [%d]\n", (int)rb, (int)bytes); + } + + *decoded_data_len = rb ; // bytes +end: + + switch_thread_rwlock_unlock(context->rwlock); + return status; +} + /* Registration */ static char *supported_formats[SWITCH_MAX_CODECS] = { 0 }; @@ -499,6 +1145,10 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_opusfile_load) { switch_file_interface_t *file_interface; switch_api_interface_t *commands_api_interface; + switch_codec_interface_t *codec_interface; + int mpf = 10000, spf = 80, bpf = 160, count = 2; + int RATES[] = {8000, 16000, 24000, 48000}; + int i; supported_formats[0] = "opus"; @@ -522,13 +1172,48 @@ 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_ADD_CODEC(codec_interface, "OPUSSTREAM"); + + for (i = 0; i < sizeof(RATES) / sizeof(RATES[0]); i++) { +// mono + switch_core_codec_add_implementation(pool, codec_interface, SWITCH_CODEC_TYPE_AUDIO, + 98, /* the IANA code number */ // does not matter + "OPUSSTREAM", /* the IANA code name */ // we just say OPUSSTREAM is an ogg/opus stream + NULL, /* default fmtp to send (can be overridden by the init function) */ + RATES[i], /* samples transferred per second */ // 48000 ! + RATES[i], /* actual samples transferred per second */ + 16 * RATES[i] / 8000, /* bits transferred per second */ + mpf * count, /* number of microseconds per frame */ + spf * RATES[i] / 8000, /* number of samples per frame */ + bpf * RATES[i] / 8000, /* number of bytes per frame decompressed */ + 0, /* number of bytes per frame compressed */ + 1, /* number of channels represented */ + 1, /* number of frames per network packet */ + switch_opusstream_init, switch_opusstream_encode, switch_opusstream_decode, switch_opusstream_destroy); +// stereo + switch_core_codec_add_implementation(pool, codec_interface, SWITCH_CODEC_TYPE_AUDIO, + 98, /* the IANA code number */ // does not matter + "OPUSSTREAM", /* the IANA code name */ // we just say OPUSSTREAM is an ogg/opus stream + NULL, /* default fmtp to send (can be overridden by the init function) */ + RATES[i], /* samples transferred per second */ + RATES[i], /* actual samples transferred per second */ + 16 * RATES[i] / 8000 * 2, /* bits transferred per second */ + mpf * count, /* number of microseconds per frame */ + spf * RATES[i] / 8000 * 2, /* number of samples per frame */ + bpf * RATES[i] / 8000 * 2, /* number of bytes per frame decompressed */ + 0, /* number of bytes per frame compressed */ + 2, /* number of channels represented */ + 1, /* number of frames per network packet */ + switch_opusstream_init, switch_opusstream_encode, switch_opusstream_decode, switch_opusstream_destroy); + } + 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 diff --git a/src/mod/formats/mod_opusfile/test/freeswitch.xml b/src/mod/formats/mod_opusfile/test/freeswitch.xml index 80591e86ac..c1fa064524 100644 --- a/src/mod/formats/mod_opusfile/test/freeswitch.xml +++ b/src/mod/formats/mod_opusfile/test/freeswitch.xml @@ -5,6 +5,7 @@ + diff --git a/src/mod/formats/mod_opusfile/test/sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.opus b/src/mod/formats/mod_opusfile/test/sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.opus new file mode 100644 index 0000000000000000000000000000000000000000..e18be2ec6d5567cc6a29505acc50b5b2605c9532 GIT binary patch literal 74858 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

!p;zcU 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

f#1k~i`GG0ptcE5Gk3lMWdX+9z~{X`^w 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 Yx%2F0wwUZy##$NhT-~mc)A>680mp%U-~a#s literal 0 HcmV?d00001 diff --git a/src/mod/formats/mod_opusfile/test/sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.wav b/src/mod/formats/mod_opusfile/test/sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.wav new file mode 100644 index 0000000000000000000000000000000000000000..98d4181e47931b5d0cee4ab08a0f023e026b0984 GIT binary patch literal 576046 zcmeI$>vs?49RTo0BtIb{K}u-RWEgQNi8j_EF+#hQcIbljGS!oD+O$1ecWkHkuJp2< zcB`&EOIyyS9LKOyF?VK^B1*I_ja!otnuJtyBa+O2u{WRdee=9{&hz3q&-1-}rra}O zLjQfSA$RQ0bMo?^xiv9_(B?+GwE4zjUYigV62oJ&o|zSOV^0W&;_|yMNgX?+M`p-A zIPuZRi>K!2Zkh3inOkP$=PsW5=;VVFLw1kMu|t-m=66377s9aGg%=(=wY|7vZ`hH( zVbPjdD;F*wzwG&?t1ZXYr>xzz`iqqlR}NkE z;=cxe+JD2eZ3VlJ7M(l#;o0JEr`Lt>VTbEUb-iZ}h#p=v^4+Y_Icx4Yb#LM1^!px| zoOIL;7^iM|NPIs-kH7QKig() zOWEGEW+LkL;1lM>q{PwLa!FAN?z{Li;HjQr(YJ4Ypt zest8bJF7B(JYwT*d1ED%y$`jv}HHMJbt}gyBNqc%ecWXhv_Guf3d@$@E>7QrB zjhLHpVffp(JwEu&0io|Vy~p<$-?gdZf=CFjUCsP{Q1$Z_Ii&@s-a3(f^xY#{53f7) z?4hoQ=M^vh@}*<(B^hVVmbG4(cWHQSmun%E#w?2K**Ux0wVoM$N>a=EjTulr@K9Py z+S6%i1Ji%_Zr=w}3VJ=gJBUzv3lmcb}7dD!DxQ zh2+)Uf8VVlDReDLoY84b+}YULm}^lXbZH)Q?d^udx}qyZHOZIPSFfmQs9apRq_VYg zZB=1)^yT+!@~+IOeW$*pab(j+%^~DQy%)VcvMTo19YV+Z;~(iXAYny9dBU-TR}(IG zx}{TM{NcE~4##5C+RcsmsO^C^A&hPL>-CtX_pjd9n9(q?{!ew0y29GO)qYajSo?Hc zm->?W&l-vvE3fuwnsWW^=KU=p3}}-TmDRR?bWL~j{3e$PMbv`gr{1oTb^s_)l%GC(EQux**D@h&F?gCZNAbxwB@OmwJo(R T8Lj!Pi(5Z$t!Ta08p8hoW3;s; literal 0 HcmV?d00001 diff --git a/src/mod/formats/mod_opusfile/test/sounds/opusfile-test-ogg.bitstream b/src/mod/formats/mod_opusfile/test/sounds/opusfile-test-ogg.bitstream new file mode 100644 index 0000000000000000000000000000000000000000..65c3889027c1a98fb155fc352e4564c887c5cc47 GIT binary patch literal 31381 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%0D#RzlJ8y9OEgnNFoL+bLG!)U<+ZkIOkxq zpq}TUdw$XWaT7R5L~j&AJIT{<02oZd5+6qfFvU?orYrjRcGHjy&L7sbZy zNOnGeI?*SQ*czi52Ll^2u2)%P%08r{qclXCW0-s{v|@SZI^OBK!>eebWTVAZN~JQm zo?Tbmc3aTbuh2jXRNt!-o=!r;@}0G9_p(;<1mk~%ZaosmhAutz1*7u&P)69BzV!NN zM8x5Z{1&43>8Q#RZsM|X!%grXag!U^`VcEnK0+TgfhUOyITz^t5J`lfKSdj8B3l*{ z^SMMyjC22#wSi&jovL9`(EF)G?7*fE-&C()q9QMy*GA%Pe>~E#P^Fl-()xac_$90{ zqdn&9#i(6JQOvpTD1O&`&vIL&Q595(*|2w??F`B>;x{OW!_a2Eloi>}>c7u8k6Fx3 z#(5wUk9Q*UmT3i&VTA8$y`nWcQy}bB5TDP9yHO2mWy0G!K|qYP-u}-RNGC3i!nn1g z25ojT>!Ia8__Qp9)A?hd#=kPF4(Hl zx;mGp`0?0Q!MY_nOh1nJvqx22`0BTZYhT%9t(dHiP^%}Oezi_`uF84jgqpT6lQAgl zVb$10)=NJMjNR5e!mq|P)Dc%It50K8=&D4RvCx#}?#e|JnH4)w+QGxBuUE)L{;s)o z>P0oK!dz&A=O97kerKxb*tW-Tn3WS3~?fI|hog+oJ`bcB}tb4#DyzgZ>KIr;`{NCZp;zVgzV0sljX}+E1E*%6-shW0ybIB|`ZGc)Qn| zPia=S5P_RS1&CZ=fkRzem0rYyL!Hto{Cp4boHGFld-yqkm>*+cK*t+3{Zc+Zz}^Bc z1JVFU#wdek�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 literal 0 HcmV?d00001 diff --git a/src/mod/formats/mod_opusfile/test/test_opusfile.c b/src/mod/formats/mod_opusfile/test/test_opusfile.c index 0cb5e8b00b..f21738bdb7 100644 --- a/src/mod/formats/mod_opusfile/test/test_opusfile.c +++ b/src/mod/formats/mod_opusfile/test/test_opusfile.c @@ -32,7 +32,42 @@ #include #include +#include +#include +//#undef HAVE_OPUSFILE_ENCODE + +#define OGG_MIN_PAGE_SIZE 2400 + +static switch_status_t test_detect_tone_in_file(const char *filepath, int rate, int freq) { + teletone_multi_tone_t mt; + teletone_tone_map_t map; + int16_t data[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; + size_t len = rate * 2 / 100; // in samples + switch_status_t status; + switch_file_handle_t fh = { 0 }; + + status = switch_core_file_open(&fh, filepath, 1, rate, SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT, NULL); + if (status != SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Cannot open file [%s]\n", filepath); + return SWITCH_STATUS_FALSE; + } + + mt.sample_rate = rate; + map.freqs[0] = (teletone_process_t)freq; + + teletone_multi_tone_init(&mt, &map); + + while (switch_core_file_read(&fh, &data, &len) == SWITCH_STATUS_SUCCESS) { + if (teletone_multi_tone_detect(&mt, data, len)) { + switch_core_file_close(&fh); + return SWITCH_STATUS_SUCCESS; + } + } + + switch_core_file_close(&fh); + return SWITCH_STATUS_FALSE; +} FST_CORE_BEGIN(".") { @@ -42,6 +77,7 @@ FST_CORE_BEGIN(".") { fst_requires_module("mod_loopback"); fst_requires_module("mod_opusfile"); + fst_requires_module("mod_sndfile"); } FST_SETUP_END() @@ -115,6 +151,163 @@ FST_CORE_BEGIN(".") switch_sleep(1000000); } + FST_TEST_END() + + FST_TEST_BEGIN(opusfile_stream) + { + switch_codec_t read_codec = { 0 }; + switch_status_t status; + switch_codec_settings_t codec_settings = {{ 0 }}; + unsigned char buf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; + switch_file_handle_t fhw = { 0 }; + uint32_t flags = 0; + uint32_t rate; + static char tmp_filename[] = "/tmp/opusfile-stream-unit_test.wav"; + char path[4096]; + uint32_t filerate = 48000; + uint32_t torate = 8000; + /*decode*/ + uint32_t decoded_len; + size_t write_len; + unsigned char decbuf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; +#ifdef HAVE_OPUSFILE_ENCODE + switch_file_handle_t fh = { 0 }; + unsigned char encbuf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; + switch_codec_t write_codec = { 0 }; + switch_size_t len = 960; + uint32_t encoded_len; + unsigned int pages = 0; + /* + General + Complete name : sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.wav + Format : Wave + File size : 563 KiB + Duration : 6 s 0 ms + Overall bit rate mode : Constant + Overall bit rate : 768 kb/s + + Audio + Format : PCM + Format settings, Endianness : Little + Format settings, Sign : Signed + Codec ID : 1 + Duration : 6 s 0 ms + Bit rate mode : Constant + Bit rate : 768 kb/s + Channel(s) : 1 channel + Sampling rate : 48.0 kHz + Bit depth : 16 bits + Stream size : 563 KiB (100%) + */ + static char filename[] = "sounds/audiocheck.net_sin_1000Hz_-3dBFS_6s.wav"; +#else + static char opus_filename[] = "sounds/opusfile-test-ogg.bitstream"; + switch_file_t *fd; + switch_size_t flen; +#endif + + status = switch_core_codec_init(&read_codec, + "OPUSSTREAM", + "mod_opusfile", + NULL, + filerate, + 20, + 1, SWITCH_CODEC_FLAG_ENCODE | SWITCH_CODEC_FLAG_DECODE, + &codec_settings, fst_pool); + fst_check(status == SWITCH_STATUS_SUCCESS); + +#ifdef HAVE_OPUSFILE_ENCODE + status = switch_core_codec_init(&write_codec, + "OPUSSTREAM", + "mod_opusfile", + NULL, + filerate, + 20, + 1, SWITCH_CODEC_FLAG_ENCODE | SWITCH_CODEC_FLAG_DECODE, + &codec_settings, fst_pool); + fst_check(status == SWITCH_STATUS_SUCCESS); + + sprintf(path, "%s%s%s", SWITCH_GLOBAL_dirs.conf_dir, SWITCH_PATH_SEPARATOR, filename); + + status = switch_core_file_open(&fh, path, 1, filerate, SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT, NULL); + fst_requires(status == SWITCH_STATUS_SUCCESS); + + status = switch_core_file_open(&fhw, tmp_filename, 1, torate, SWITCH_FILE_FLAG_WRITE | SWITCH_FILE_DATA_SHORT, NULL); + fst_requires(status == SWITCH_STATUS_SUCCESS); + + fhw.native_rate = filerate; // make sure we resample to 8000 Hz, because teletone wants this rate + + while (switch_core_file_read(&fh, &buf, &len) == SWITCH_STATUS_SUCCESS) { + + status = switch_core_codec_encode(&write_codec, NULL, &buf, len * sizeof(int16_t), filerate, &encbuf, &encoded_len, &rate, &flags); + fst_check(status == SWITCH_STATUS_SUCCESS); + + if (encoded_len) { + pages++; + status = switch_core_codec_decode(&read_codec, NULL, &encbuf, encoded_len, filerate, &decbuf, &decoded_len, &rate, &flags); + fst_check(status == SWITCH_STATUS_SUCCESS); + write_len = decoded_len / sizeof(int16_t); + if (write_len) switch_core_file_write(&fhw, &decbuf, &write_len); + } + } + + // continue reading, encoded pages are buffered + while (switch_core_codec_decode(&read_codec, NULL, &encbuf, 0, filerate, &decbuf, &decoded_len, &rate, &flags) == SWITCH_STATUS_SUCCESS && decoded_len) { + write_len = decoded_len / sizeof(int16_t); + status = switch_core_file_write(&fhw, &decbuf, &write_len); + fst_check(status == SWITCH_STATUS_SUCCESS); + } + + switch_core_codec_destroy(&write_codec); + + status = switch_core_file_close(&fh); + fst_check(status == SWITCH_STATUS_SUCCESS); + +#else + // the test will perform only decoding + + sprintf(path, "%s%s%s", SWITCH_GLOBAL_dirs.conf_dir, SWITCH_PATH_SEPARATOR, opus_filename); + + // open the file raw and buffer data to the decoder + status = switch_file_open(&fd, path, SWITCH_FOPEN_READ, SWITCH_FPROT_UREAD, fst_pool); + fst_requires(status == SWITCH_STATUS_SUCCESS); + + status = switch_core_file_open(&fhw, tmp_filename, 1, torate, SWITCH_FILE_FLAG_WRITE | SWITCH_FILE_DATA_SHORT, NULL); + fst_requires(status == SWITCH_STATUS_SUCCESS); + + fhw.native_rate = filerate; + + flen = OGG_MIN_PAGE_SIZE; + while (switch_file_read(fd, &buf, &flen) == SWITCH_STATUS_SUCCESS || flen != 0) { + status = SWITCH_STATUS_SUCCESS; + while (status == SWITCH_STATUS_SUCCESS) { + status = switch_core_codec_decode(&read_codec, NULL, &buf, flen, filerate, &decbuf, &decoded_len, &rate, &flags); + fst_requires(status == SWITCH_STATUS_SUCCESS); + write_len = decoded_len / sizeof(int16_t); + if (write_len) switch_core_file_write(&fhw, &decbuf, &write_len); + else break; + } + } +#endif + + // continue reading, encoded pages are buffered + while (switch_core_codec_decode(&read_codec, NULL, &buf, 0, filerate, &decbuf, &decoded_len, &rate, &flags) == SWITCH_STATUS_SUCCESS && decoded_len) { + write_len = decoded_len / sizeof(int16_t); + status = switch_core_file_write(&fhw, &decbuf, &write_len); + fst_check(status == SWITCH_STATUS_SUCCESS); + } + + status = switch_core_file_close(&fhw); + fst_check(status == SWITCH_STATUS_SUCCESS); + + switch_core_codec_destroy(&read_codec); + + // final test + status = test_detect_tone_in_file(tmp_filename, torate, 1000 /*Hz*/); + fst_requires(status == SWITCH_STATUS_SUCCESS); + + unlink(tmp_filename); + } FST_TEST_END() } From 97a0b0fbaea0e98e973e0e3ca69ebdea0a0f61e8 Mon Sep 17 00:00:00 2001 From: Dragos Oancea Date: Fri, 13 Mar 2020 18:42:43 +0000 Subject: [PATCH 2/3] [mod_opusfile] protect ogg data buff --- src/mod/formats/mod_opusfile/mod_opusfile.c | 7 ++++--- src/mod/formats/mod_opusfile/test/test_opusfile.c | 11 +++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/mod/formats/mod_opusfile/mod_opusfile.c b/src/mod/formats/mod_opusfile/mod_opusfile.c index ea932ed0ff..38f5d1f442 100644 --- a/src/mod/formats/mod_opusfile/mod_opusfile.c +++ b/src/mod/formats/mod_opusfile/mod_opusfile.c @@ -976,6 +976,7 @@ static void *SWITCH_THREAD_FUNC read_stream_thread(switch_thread_t *thread, void switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "[OGG/OPUS Stream Decode] read_stream_thread(): switch_thread_self(): 0x%lx\n", switch_thread_self()); } switch_thread_rwlock_rdlock(context->rwlock); + switch_mutex_lock(context->ogg_mutex); if ((buffered_ogg_bytes = switch_buffer_inuse(context->ogg_buffer))) { if (buffered_ogg_bytes <= OGG_MAX_PAGE_SIZE) { @@ -1021,6 +1022,7 @@ static void *SWITCH_THREAD_FUNC read_stream_thread(switch_thread_t *thread, void } } + switch_mutex_unlock(context->ogg_mutex); switch_thread_rwlock_unlock(context->rwlock); return NULL; } @@ -1071,9 +1073,9 @@ static switch_status_t switch_opusstream_decode(switch_codec_t *codec, #endif switch_thread_rwlock_rdlock(context->rwlock); + switch_mutex_lock(context->ogg_mutex); memset(context->ogg_data, 0, sizeof(context->ogg_data)); if (encoded_data_len <= SWITCH_RECOMMENDED_BUFFER_SIZE) { - switch_mutex_lock(context->ogg_mutex); switch_buffer_write(context->ogg_buffer, encode_buf, encoded_data_len); if ((buffered_ogg_bytes = switch_buffer_inuse(context->ogg_buffer)) >= ogg_bytes) { @@ -1089,12 +1091,10 @@ static switch_status_t switch_opusstream_decode(switch_codec_t *codec, switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "[OGG/OPUS Stream Decode] buffered ogg data bigger than max OGG page size, will flush\n"); *decoded_data_len = 0; switch_buffer_zero(context->ogg_buffer); - switch_mutex_unlock(context->ogg_mutex); switch_goto_status(SWITCH_STATUS_SUCCESS, end); } } - switch_mutex_unlock(context->ogg_mutex); } else { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "[OGG/OPUS Stream Decode] too much data to buffer, flushing buffer!\n"); *decoded_data_len = 0; @@ -1134,6 +1134,7 @@ static switch_status_t switch_opusstream_decode(switch_codec_t *codec, end: switch_thread_rwlock_unlock(context->rwlock); + switch_mutex_unlock(context->ogg_mutex); return status; } diff --git a/src/mod/formats/mod_opusfile/test/test_opusfile.c b/src/mod/formats/mod_opusfile/test/test_opusfile.c index f21738bdb7..feb74619e5 100644 --- a/src/mod/formats/mod_opusfile/test/test_opusfile.c +++ b/src/mod/formats/mod_opusfile/test/test_opusfile.c @@ -170,6 +170,7 @@ FST_CORE_BEGIN(".") uint32_t decoded_len; size_t write_len; unsigned char decbuf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; + switch_stream_handle_t stream = { 0 }; #ifdef HAVE_OPUSFILE_ENCODE switch_file_handle_t fh = { 0 }; unsigned char encbuf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; @@ -216,6 +217,12 @@ FST_CORE_BEGIN(".") &codec_settings, fst_pool); fst_check(status == SWITCH_STATUS_SUCCESS); + SWITCH_STANDARD_STREAM(stream); + + switch_api_execute("opusfile_debug", "on", NULL, &stream); + + switch_safe_free(stream.data); + #ifdef HAVE_OPUSFILE_ENCODE status = switch_core_codec_init(&write_codec, "OPUSSTREAM", @@ -277,12 +284,12 @@ FST_CORE_BEGIN(".") fhw.native_rate = filerate; - flen = OGG_MIN_PAGE_SIZE; + flen = 4096; while (switch_file_read(fd, &buf, &flen) == SWITCH_STATUS_SUCCESS || flen != 0) { status = SWITCH_STATUS_SUCCESS; while (status == SWITCH_STATUS_SUCCESS) { status = switch_core_codec_decode(&read_codec, NULL, &buf, flen, filerate, &decbuf, &decoded_len, &rate, &flags); - fst_requires(status == SWITCH_STATUS_SUCCESS); + fst_check(status == SWITCH_STATUS_SUCCESS); write_len = decoded_len / sizeof(int16_t); if (write_len) switch_core_file_write(&fhw, &decbuf, &write_len); else break; From 0cf323c339c788f71bde4d1b2ecc115e61ccbb1d Mon Sep 17 00:00:00 2001 From: Dragos Oancea Date: Wed, 15 Apr 2020 11:34:10 +0000 Subject: [PATCH 3/3] [mod_opusfile] unit-tests: add timer to fix random failures due to the fact that the decoding callback is not installed immediatelly --- src/mod/formats/mod_opusfile/test/test_opusfile.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mod/formats/mod_opusfile/test/test_opusfile.c b/src/mod/formats/mod_opusfile/test/test_opusfile.c index feb74619e5..339047c576 100644 --- a/src/mod/formats/mod_opusfile/test/test_opusfile.c +++ b/src/mod/formats/mod_opusfile/test/test_opusfile.c @@ -171,6 +171,7 @@ FST_CORE_BEGIN(".") size_t write_len; unsigned char decbuf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; switch_stream_handle_t stream = { 0 }; + switch_timer_t timer; #ifdef HAVE_OPUSFILE_ENCODE switch_file_handle_t fh = { 0 }; unsigned char encbuf[SWITCH_RECOMMENDED_BUFFER_SIZE] = { 0 }; @@ -223,6 +224,8 @@ FST_CORE_BEGIN(".") switch_safe_free(stream.data); + switch_core_timer_init(&timer, "soft", 20, 960, fst_pool); + #ifdef HAVE_OPUSFILE_ENCODE status = switch_core_codec_init(&write_codec, "OPUSSTREAM", @@ -252,6 +255,7 @@ FST_CORE_BEGIN(".") if (encoded_len) { pages++; status = switch_core_codec_decode(&read_codec, NULL, &encbuf, encoded_len, filerate, &decbuf, &decoded_len, &rate, &flags); + switch_core_timer_next(&timer); fst_check(status == SWITCH_STATUS_SUCCESS); write_len = decoded_len / sizeof(int16_t); if (write_len) switch_core_file_write(&fhw, &decbuf, &write_len); @@ -260,6 +264,7 @@ FST_CORE_BEGIN(".") // continue reading, encoded pages are buffered while (switch_core_codec_decode(&read_codec, NULL, &encbuf, 0, filerate, &decbuf, &decoded_len, &rate, &flags) == SWITCH_STATUS_SUCCESS && decoded_len) { + switch_core_timer_next(&timer); write_len = decoded_len / sizeof(int16_t); status = switch_core_file_write(&fhw, &decbuf, &write_len); fst_check(status == SWITCH_STATUS_SUCCESS); @@ -288,6 +293,7 @@ FST_CORE_BEGIN(".") while (switch_file_read(fd, &buf, &flen) == SWITCH_STATUS_SUCCESS || flen != 0) { status = SWITCH_STATUS_SUCCESS; while (status == SWITCH_STATUS_SUCCESS) { + switch_core_timer_next(&timer); status = switch_core_codec_decode(&read_codec, NULL, &buf, flen, filerate, &decbuf, &decoded_len, &rate, &flags); fst_check(status == SWITCH_STATUS_SUCCESS); write_len = decoded_len / sizeof(int16_t); @@ -296,14 +302,14 @@ FST_CORE_BEGIN(".") } } #endif - // continue reading, encoded pages are buffered while (switch_core_codec_decode(&read_codec, NULL, &buf, 0, filerate, &decbuf, &decoded_len, &rate, &flags) == SWITCH_STATUS_SUCCESS && decoded_len) { + switch_core_timer_next(&timer); write_len = decoded_len / sizeof(int16_t); status = switch_core_file_write(&fhw, &decbuf, &write_len); fst_check(status == SWITCH_STATUS_SUCCESS); } - + status = switch_core_file_close(&fhw); fst_check(status == SWITCH_STATUS_SUCCESS); @@ -313,6 +319,8 @@ FST_CORE_BEGIN(".") status = test_detect_tone_in_file(tmp_filename, torate, 1000 /*Hz*/); fst_requires(status == SWITCH_STATUS_SUCCESS); + switch_core_timer_destroy(&timer); + unlink(tmp_filename); }