+#include "opus_common.h"
+
+/** 120ms at 48000 */
+#define MAX_FRAME_SIZE (960*6)
+
+struct opusdec_context {
+ OpusMSDecoder *st;
+ opus_int64 packet_count;
+ int total_links;
+ bool stream_init;
+ ogg_sync_state oy;
+ ogg_stream_state os;
+ ogg_page ogg_page;
+ bool eos;
+ int channels;
+ int preskip;
+ bool have_opus_stream;
+ bool have_more;
+ ogg_int32_t opus_serialno;
+};
+
+static int opusdec_execute(struct btr_node *btrn, const char *cmd,
+ char **result)
+{
+ struct filter_node *fn = btr_context(btrn);
+ struct opusdec_context *ctx = fn->private_data;
+
+ return decoder_execute(cmd, 48000, ctx->channels, result);
+}
+
+static void opusdec_open(struct filter_node *fn)
+{
+ struct opusdec_context *ctx = para_calloc(sizeof(*ctx));
+
+ ogg_sync_init(&ctx->oy);
+ fn->private_data = ctx;
+}
+
+static void opusdec_close(struct filter_node *fn)
+{
+ struct opusdec_context *ctx = fn->private_data;
+
+ if (ctx->st) {
+ opus_multistream_decoder_destroy(ctx->st);
+ if (ctx->stream_init)
+ ogg_stream_clear(&ctx->os);
+ ogg_sync_clear(&ctx->oy);
+ }
+ free(ctx);
+ fn->private_data = NULL;
+}
+
+/* Process an Opus header and setup the opus decoder based on it. */
+static int opusdec_init(ogg_packet *op, struct opusdec_context *ctx)
+{
+ int ret;
+ struct opus_header header;
+
+ ctx->st = NULL;
+ ret = opus_parse_header((char *)op->packet, op->bytes, &header);
+ if (ret < 0)
+ return ret;
+ PARA_INFO_LOG("detected header v%d\n", header.version);
+ ctx->channels = header.channels;
+ ctx->preskip = header.preskip;
+ ctx->st = opus_multistream_decoder_create(48000, header.channels,
+ header.nb_streams, header.nb_coupled, header.stream_map, &ret);
+ if (ret != OPUS_OK || !ctx->st) {
+ PARA_ERROR_LOG("%s\n", opus_strerror(ret));
+ return -E_CREATE_OPUS_DECODER;
+ }
+ if (header.gain != 0) {
+ ret = opus_multistream_decoder_ctl(ctx->st,
+ OPUS_SET_GAIN(header.gain));
+ if (ret != OPUS_OK) {
+ PARA_ERROR_LOG("%s\n", opus_strerror(ret));
+ return -E_OPUS_SET_GAIN;
+ }
+ PARA_INFO_LOG("playback gain: %fdB\n", header.gain / 256.);
+ }
+ PARA_INFO_LOG("%d channel(s), 48KHz\n", ctx->channels);
+ return 1;
+}
+
+static void opusdec_add_output(short *pcm, int frames_available,
+ struct btr_node *btrn, struct opusdec_context *ctx)
+{
+ int tmp_skip, num_frames, bytes;
+
+ tmp_skip = PARA_MIN(ctx->preskip, frames_available);
+ ctx->preskip -= tmp_skip;
+ num_frames = frames_available - tmp_skip;
+ if (num_frames <= 0)
+ return;
+ bytes = sizeof(short) * num_frames * ctx->channels;
+
+ if (tmp_skip > 0) {
+ short *in = pcm + ctx->channels * tmp_skip;
+ short *out = para_malloc(bytes);
+ memcpy(out, in, bytes);
+ free(pcm);
+ pcm = out;
+ }
+ btr_add_output((char *)pcm, bytes, btrn);
+}
+
+/* returns > 1 if packet was decoded, 0 if it was ignored, negative on errors. */
+static int decode_packet(struct opusdec_context *ctx, ogg_packet *op,
+ struct btr_node *btrn)
+{
+ int ret;
+ short *output;
+
+ /*
+ * OggOpus streams are identified by a magic string in the initial
+ * stream header.
+ */
+ if (op->b_o_s && op->bytes >= 8 && !memcmp(op->packet, "OpusHead", 8)) {
+ if (!ctx->have_opus_stream) {
+ ctx->opus_serialno = ctx->os.serialno;
+ ctx->have_opus_stream = true;
+ ctx->packet_count = 0;
+ ctx->eos = false;
+ ctx->total_links++;
+ } else
+ PARA_NOTICE_LOG("ignoring opus stream %llu\n",
+ (long long unsigned)ctx->os.serialno);
+ }
+ if (!ctx->have_opus_stream || ctx->os.serialno != ctx->opus_serialno)
+ return 0;
+ /* If first packet in a logical stream, process the Opus header. */
+ if (ctx->packet_count == 0)
+ return opusdec_init(op, ctx);
+ if (ctx->packet_count == 1)
+ return 1;
+ /* don't care for anything except opus eos */
+ if (op->e_o_s && ctx->os.serialno == ctx->opus_serialno)
+ ctx->eos = true;
+ output = para_malloc(sizeof(short) * MAX_FRAME_SIZE * ctx->channels);
+ ret = opus_multistream_decode(ctx->st, (unsigned char *)op->packet,
+ op->bytes, output, MAX_FRAME_SIZE, 0);
+ if (ret < 0) {
+ PARA_ERROR_LOG("%s\n", opus_strerror(ret));
+ free(output);
+ return -E_OPUS_DECODE;
+ }
+ opusdec_add_output(output, ret, btrn, ctx);
+ return 1;
+}
+
+#define OPUSDEC_MAX_OUTPUT_SIZE (1024 * 1024)
+
+static int opusdec_post_select(__a_unused struct sched *s, struct task *t)
+{
+ struct filter_node *fn = container_of(t, struct filter_node, task);
+ struct opusdec_context *ctx = fn->private_data;
+ struct btr_node *btrn = fn->btrn;
+ int ret;
+ ogg_packet op;
+
+ ret = btr_node_status(btrn, fn->min_iqs, BTR_NT_INTERNAL);
+ if (ret < 0) {
+ if (ret != -E_BTR_EOF) /* fatal error */
+ goto out;
+ if (!ctx->have_more) /* EOF */
+ goto out;
+ } else if (ret == 0 && !ctx->have_more) /* nothing to do */
+ goto out;
+ if (btr_get_output_queue_size(btrn) > OPUSDEC_MAX_OUTPUT_SIZE)
+ return 0;
+ for (;;) {
+ int serial;
+ if (ctx->stream_init) {
+ ret = ogg_stream_packetout(&ctx->os, &op);
+ if (ret == 1)
+ break;
+ }
+ while (ogg_sync_pageout(&ctx->oy, &ctx->ogg_page) != 1) {
+ char *btr_buf, *data;
+ size_t nbytes = btr_next_buffer(btrn, &btr_buf);
+ nbytes = PARA_MIN(nbytes, (size_t)24 * 1024);
+ //PARA_CRIT_LOG("nbytes: %d\n", nbytes);
+ ctx->have_more = false;
+ if (nbytes == 0)
+ return 0;
+ data = ogg_sync_buffer(&ctx->oy, nbytes);
+ memcpy(data, btr_buf, nbytes);
+ btr_consume(btrn, nbytes);
+ ogg_sync_wrote(&ctx->oy, nbytes);
+ }
+ ctx->have_more = true;
+ serial = ogg_page_serialno(&ctx->ogg_page);
+ if (ctx->stream_init) {
+ if (serial != ctx->os.serialno)
+ ogg_stream_reset_serialno(&ctx->os, serial);
+ } else {
+ ogg_stream_init(&ctx->os, serial);
+ ctx->stream_init = true;
+ }
+ /* Add page to the bitstream */
+ ogg_stream_pagein(&ctx->os, &ctx->ogg_page);
+ }
+ ret = decode_packet(ctx, &op, btrn);
+ if (ret < 0)
+ goto out;
+ ctx->packet_count++;
+ if (ctx->eos)
+ ctx->have_opus_stream = false;
+out:
+ if (ret < 0)
+ btr_remove_node(&fn->btrn);
+ return ret;
+}
+
+static void opusdec_pre_select(struct sched *s, struct task *t)
+{
+ struct filter_node *fn = container_of(t, struct filter_node, task);
+ struct opusdec_context *ctx = fn->private_data;
+ int ret = btr_node_status(fn->btrn, fn->min_iqs, BTR_NT_INTERNAL);
+
+ if (ret != 0)
+ return sched_min_delay(s);
+ if (ctx->have_more)
+ return;
+ if (btr_get_output_queue_size(fn->btrn) <= OPUSDEC_MAX_OUTPUT_SIZE)
+ return sched_min_delay(s);
+}