diff --git a/subprojects/gst-plugins-bad/ext/lc3/gstlc3common.h b/subprojects/gst-plugins-bad/ext/lc3/gstlc3common.h new file mode 100644 index 0000000000..98e216fb0c --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/gstlc3common.h @@ -0,0 +1,30 @@ +/* GStreamer LC3 Bluetooth LE audio decoder + * Copyright (C) 2023 Asymptotic Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#define SAMPLE_RATES "8000, 16000, 24000, 32000, 48000" +#define FORMAT "S16LE" + +#define FRAME_DURATION_10000US 10000 + +#define FRAME_DURATIONS "10000, 7500" +#define FRAME_BYTES_RANGE "20, 400" diff --git a/subprojects/gst-plugins-bad/ext/lc3/gstlc3dec.c b/subprojects/gst-plugins-bad/ext/lc3/gstlc3dec.c new file mode 100644 index 0000000000..57126b85d4 --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/gstlc3dec.c @@ -0,0 +1,351 @@ +/* GStreamer LC3 Bluetooth LE audio decoder + * Copyright (C) 2023 Asymptotic Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Suite 500, + * Boston, MA 02110-1335, USA. + */ + +/** + * SECTION:element-lc3dec + * + * The lc3dec decodes LC3 data into raw audio. + * + * + * Example launch line + * |[ + * gst-launch-1.0 -v filesrc location=encoded.lc3 blocksize=200 ! \ + * audio/x-lc3,frame-bytes=100,frame-duration-us=10000,channels=2,rate=48000,channel-mask=\(bitmask\)0x00000000000000003 !\ + * lc3dec ! wavenc ! filesink location=decoded.wav + * ]| + * + * Decodes the LC3 frames each with 100 bytes of size, converts it to raw audio and saves into a .wav file + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "gstlc3common.h" +#include "gstlc3dec.h" + +GST_DEBUG_CATEGORY_STATIC (gst_lc3_dec_debug_category); +#define GST_CAT_DEFAULT gst_lc3_dec_debug_category + +#define parent_class gst_lc3_dec_parent_class +G_DEFINE_TYPE (GstLc3Dec, gst_lc3_dec, GST_TYPE_AUDIO_DECODER); +GST_ELEMENT_REGISTER_DEFINE (lc3dec, "lc3dec", GST_RANK_NONE, GST_TYPE_LC3_DEC); + +/* prototypes */ +static gboolean gst_lc3_dec_start (GstAudioDecoder * decoder); +static gboolean gst_lc3_dec_stop (GstAudioDecoder * decoder); +static gboolean gst_lc3_dec_set_format (GstAudioDecoder * decoder, + GstCaps * caps); +static GstFlowReturn gst_lc3_dec_handle_frame (GstAudioDecoder * decoder, + GstBuffer * buffer); + +/* pad templates */ +static GstStaticPadTemplate gst_lc3_dec_src_template = +GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/x-raw, " + "format = " FORMAT ", layout=interleaved, " + "rate = { " SAMPLE_RATES " }, channels = [1,MAX]") + ); + +static GstStaticPadTemplate gst_lc3_dec_sink_template = +GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/x-lc3, rate = { " SAMPLE_RATES " }, " + "channels = [1,MAX]," + "frame-bytes = (int) [" FRAME_BYTES_RANGE "], " + "frame-duration-us = (int) { " FRAME_DURATIONS " }, " + "framed=(boolean) true") + ); + +/* class initialization */ +static void +gst_lc3_dec_class_init (GstLc3DecClass * klass) +{ + GstAudioDecoderClass *audio_decoder_class = GST_AUDIO_DECODER_CLASS (klass); + + gst_element_class_add_static_pad_template (GST_ELEMENT_CLASS (klass), + &gst_lc3_dec_src_template); + gst_element_class_add_static_pad_template (GST_ELEMENT_CLASS (klass), + &gst_lc3_dec_sink_template); + + gst_element_class_set_static_metadata (GST_ELEMENT_CLASS (klass), + "LC3 Bluetooth Audio decoder", "Codec/Decoder/Audio", + "Decodes an LC3 Audio stream to raw audio", + "Taruntej Kanakamalla "); + + GST_DEBUG_CATEGORY_INIT (gst_lc3_dec_debug_category, "lc3dec", 0, + "debug category for lc3dec element"); + + audio_decoder_class->start = GST_DEBUG_FUNCPTR (gst_lc3_dec_start); + audio_decoder_class->stop = GST_DEBUG_FUNCPTR (gst_lc3_dec_stop); + audio_decoder_class->set_format = GST_DEBUG_FUNCPTR (gst_lc3_dec_set_format); + audio_decoder_class->handle_frame = + GST_DEBUG_FUNCPTR (gst_lc3_dec_handle_frame); +} + +static void +gst_lc3_dec_init (GstLc3Dec * lc3_dec) +{ +} + +static gboolean +gst_lc3_dec_start (GstAudioDecoder * decoder) +{ + /* let the baseclass convert the segment data + * from 'bytes' to 'time' format + */ + gst_audio_decoder_set_estimate_rate (decoder, TRUE); + + /* Inform the base class that the LC3 lib can do PLC */ + gst_audio_decoder_set_plc_aware (decoder, TRUE); + + return TRUE; +} + +static gboolean +gst_lc3_dec_stop (GstAudioDecoder * decoder) +{ + GstLc3Dec *lc3_dec = GST_LC3_DEC (decoder); + + if (lc3_dec->dec_ch != NULL) { + for (int ich = 0; ich < lc3_dec->channels; ich++) { + g_free (lc3_dec->dec_ch[ich]); + lc3_dec->dec_ch[ich] = NULL; + } + + g_free (lc3_dec->dec_ch); + lc3_dec->dec_ch = NULL; + } + + return TRUE; +} + +static gboolean +gst_lc3_dec_set_format (GstAudioDecoder * decoder, GstCaps * caps) +{ + GstLc3Dec *lc3_dec = GST_LC3_DEC (decoder); + GstAudioInfo info; + GstStructure *s; + GstAudioChannelPosition pos[64] = { GST_AUDIO_CHANNEL_POSITION_INVALID, }; + gint in_ch, in_rate; + guint64 in_chmsk = 0; + GstClockTime latency; + + GST_DEBUG_OBJECT (lc3_dec, "set_format"); + GST_DEBUG_OBJECT (lc3_dec, "input caps %" GST_PTR_FORMAT, caps); + + s = gst_caps_get_structure (caps, 0); + if (FALSE == gst_structure_get_int (s, "frame-duration-us", + &lc3_dec->frame_duration_us)) { + GST_ERROR_OBJECT (lc3_dec, + "sink caps does not contain 'frame-duration-us'"); + return FALSE; + } + + if (FALSE == gst_structure_get_int (s, "frame-bytes", &lc3_dec->frame_bytes)) { + GST_ERROR_OBJECT (lc3_dec, "sink caps does not contain 'frame-bytes'"); + return FALSE; + } + /* use rate and channel from input caps to create filter caps */ + gst_structure_get_int (s, "rate", &in_rate); + gst_structure_get_int (s, "channels", &in_ch); + if (!gst_structure_get (s, "channel-mask", GST_TYPE_BITMASK, &in_chmsk, NULL)) { + GST_INFO_OBJECT (lc3_dec, + "channel-mask not present in the sink caps, getting fallback mask"); + in_chmsk = gst_audio_channel_get_fallback_mask (in_ch); + } + s = NULL; + + gst_audio_channel_positions_from_mask (in_ch, in_chmsk, pos); + gst_audio_info_set_format (&info, GST_AUDIO_FORMAT_S16LE, in_rate, in_ch, + pos); + + + /* get rate, format, channels from the output caps */ + lc3_dec->rate = GST_AUDIO_INFO_RATE (&info); + lc3_dec->channels = GST_AUDIO_INFO_CHANNELS (&info); + + switch (GST_AUDIO_INFO_FORMAT (&info)) { + case GST_AUDIO_FORMAT_S16LE: + lc3_dec->format = LC3_PCM_FORMAT_S16; + break; + case GST_AUDIO_FORMAT_S24LE: + lc3_dec->format = LC3_PCM_FORMAT_S24_3LE; + break; + case GST_AUDIO_FORMAT_F32: + lc3_dec->format = LC3_PCM_FORMAT_FLOAT; + break; + case GST_AUDIO_FORMAT_S24_32LE: + default: + lc3_dec->format = LC3_PCM_FORMAT_S24; + break; + } + + GST_INFO_OBJECT (lc3_dec, "lc3dec params " + "rate: %" G_GINT32_FORMAT ", channels: %" G_GINT32_FORMAT + ", lc3_pcm_format = %" G_GINT32_FORMAT " frame len: %" G_GINT32_FORMAT + ", frame_duration " "%" G_GINT32_FORMAT, lc3_dec->rate, lc3_dec->channels, + lc3_dec->format, lc3_dec->frame_bytes, lc3_dec->frame_duration_us); + + lc3_dec->frame_samples = + lc3_frame_samples (lc3_dec->frame_duration_us, lc3_dec->rate); + lc3_dec->bpf = GST_AUDIO_INFO_BPF (&info); + + latency = + gst_util_uint64_scale_int (lc3_dec->frame_bytes, GST_SECOND, + lc3_dec->rate); + gst_audio_decoder_set_latency (decoder, latency, latency); + + /* Setup and Init decoder handle */ + if (lc3_dec->dec_ch != NULL) { + for (int ich = 0; ich < lc3_dec->channels; ich++) { + g_free (lc3_dec->dec_ch[ich]); + lc3_dec->dec_ch[ich] = NULL; + } + g_free (lc3_dec->dec_ch); + lc3_dec->dec_ch = NULL; + } + + lc3_dec->dec_ch = g_new0 (lc3_decoder_t, lc3_dec->channels); + + for (guint8 i = 0; i < lc3_dec->channels; i++) { + /* The decoder can resample for us. + * But we leave the resampling to before + * decoding explicitly for now. So pass the same + * sample rate for sr_hz and sr_pcm_hz + */ + lc3_dec->dec_ch[i] = + lc3_setup_decoder (lc3_dec->frame_duration_us, lc3_dec->rate, + lc3_dec->rate, g_malloc (lc3_decoder_size (lc3_dec->frame_duration_us, + lc3_dec->rate))); + + if (lc3_dec->dec_ch[i] == NULL) { + GST_ERROR_OBJECT (lc3_dec, + "Failed to create decoder handle for channel %" G_GUINT32_FORMAT, i); + return FALSE; + } + } + + gst_audio_decoder_set_output_format (decoder, &info); + return TRUE; +} + +static GstFlowReturn +gst_lc3_dec_handle_frame (GstAudioDecoder * decoder, GstBuffer * inbuf) +{ + GstLc3Dec *lc3_dec = GST_LC3_DEC (decoder); + GstBuffer *outbuf = NULL; + GstMapInfo out_map; + GstMapInfo in_map; + gssize output_size; + GstAudioClippingMeta *audio_meta; + gboolean do_plc = gst_audio_decoder_get_plc (decoder) && + gst_audio_decoder_get_plc_aware (decoder); + + /* no fancy draining */ + if (G_UNLIKELY (inbuf == NULL)) + return GST_FLOW_OK; + + gst_buffer_map (inbuf, &in_map, GST_MAP_READ); + + if (G_UNLIKELY (in_map.size == 0 && !do_plc)) + return GST_FLOW_ERROR; + + GST_LOG_OBJECT (lc3_dec, "received %lu bytes ", in_map.size); + + /* we expect exactly one frame each time */ + if (G_UNLIKELY (in_map.size == 0 && !do_plc) && + (in_map.size != (lc3_dec->frame_bytes * lc3_dec->channels))) + goto mixed_frames; + + output_size = lc3_dec->frame_samples * lc3_dec->bpf; + GST_LOG_OBJECT (lc3_dec, "allocating %lu bytes to output buff", output_size); + outbuf = gst_audio_decoder_allocate_output_buffer (decoder, output_size); + + if (outbuf == NULL) + goto no_buffer; + + gst_buffer_map (outbuf, &out_map, GST_MAP_WRITE); + + for (guint c = 0; c < lc3_dec->channels; c++) { + gint ret = 0; + void *in = in_map.data ? in_map.data + (c * lc3_dec->frame_bytes) : NULL; + ret = + lc3_decode (lc3_dec->dec_ch[c], + in, lc3_dec->frame_bytes, + lc3_dec->format, + out_map.data + (c * lc3_dec->bpf / lc3_dec->channels), + lc3_dec->channels); + + if (ret < 0) { + GST_ERROR_OBJECT (lc3_dec, + "Failed to decode frame for buffer %" GST_PTR_FORMAT, inbuf); + return GST_FLOW_ERROR; + } else if (ret == 1) { + GST_INFO_OBJECT (lc3_dec, "PLC operated for channel: %d", c + 1); + } + + } + + audio_meta = gst_buffer_get_audio_clipping_meta (inbuf); + if (audio_meta) { + switch (audio_meta->format) { + case GST_FORMAT_DEFAULT: + { + output_size = + output_size - (audio_meta->start * lc3_dec->bpf) - + (audio_meta->end * lc3_dec->bpf); + gst_buffer_resize (outbuf, (audio_meta->start * lc3_dec->bpf), + output_size); + } + break; + default: + GST_WARNING_OBJECT (lc3_dec, "audio meta format: %d not handled", + audio_meta->format); + break; + } + } + + gst_buffer_unmap (outbuf, &out_map); + gst_buffer_unmap (inbuf, &in_map); + + return gst_audio_decoder_finish_frame (decoder, outbuf, 1); + +/* ERRORS */ +mixed_frames: + { + GST_WARNING_OBJECT (lc3_dec, + "inconsistent input data/frames, Need to be %" + G_GINT32_FORMAT " bytes", lc3_dec->frame_bytes * lc3_dec->channels); + return GST_FLOW_ERROR; + } + +no_buffer: + { + GST_ERROR_OBJECT (lc3_dec, "could not allocate output buffer"); + return GST_FLOW_ERROR; + } +} diff --git a/subprojects/gst-plugins-bad/ext/lc3/gstlc3dec.h b/subprojects/gst-plugins-bad/ext/lc3/gstlc3dec.h new file mode 100644 index 0000000000..9b8a866c1b --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/gstlc3dec.h @@ -0,0 +1,54 @@ +/* GStreamer LC3 Bluetooth LE audio decoder + * Copyright (C) 2023 Asymptotic Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef _GST_LC3DEC_H_ +#define _GST_LC3DEC_H_ + +#include +#include + +G_BEGIN_DECLS +#define GST_TYPE_LC3_DEC (gst_lc3_dec_get_type()) +G_DECLARE_FINAL_TYPE (GstLc3Dec, gst_lc3_dec, GST, LC3_DEC, GstAudioDecoder) + +struct _GstLc3Dec +{ + GstAudioDecoder base; + lc3_decoder_t *dec_ch; + gint channels; + gint rate; + gint frame_duration_us; + /* byte count per channel, same for all channels */ + gint frame_bytes; + gint frame_samples; + enum lc3_pcm_format format; + int bitrate; + /* bytes per sample */ + gint bpf; +}; + +struct _GstLc3DecClass +{ + GstAudioDecoderClass base_class; +}; + +GST_ELEMENT_REGISTER_DECLARE (lc3dec); + +G_END_DECLS +#endif diff --git a/subprojects/gst-plugins-bad/ext/lc3/gstlc3enc.c b/subprojects/gst-plugins-bad/ext/lc3/gstlc3enc.c new file mode 100644 index 0000000000..b8b54434de --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/gstlc3enc.c @@ -0,0 +1,426 @@ +/* GStreamer LC3 Bluetooth LE audio encoder + * Copyright (C) 2023 Asymptotic Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Suite 500, + * Boston, MA 02110-1335, USA. + */ + +/** + * SECTION:element-lc3enc + * + * The lc3enc element encodes raw audio using the Low Complexity Communication + * Codec (LC3). + * + * + * Example launch pipeline + * |[ + * gst-launch-1.0 audiotestsrc ! lc3enc ! audio/x-lc3,channels=2,rate=48000,frame-duration-us=10000 !\ + * filesink location=audio.lc3 + * ]| + * Encodes a sine wave into LC3 format using the config params frame-duration-us + * specified by the caps downstream and save it to file audio.lc3 + * + */ + +#include "gst/gstbuffer.h" +#include "gst/gstevent.h" +#include "gst/gstpad.h" +#include +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "gstlc3common.h" +#include "gstlc3enc.h" + +GST_DEBUG_CATEGORY_STATIC (gst_lc3_enc_debug_category); +#define GST_CAT_DEFAULT gst_lc3_enc_debug_category + +#define parent_class gst_lc3_enc_parent_class +G_DEFINE_TYPE (GstLc3Enc, gst_lc3_enc, GST_TYPE_AUDIO_ENCODER); +GST_ELEMENT_REGISTER_DEFINE (lc3enc, "lc3enc", GST_RANK_NONE, GST_TYPE_LC3_ENC); + +static gboolean gst_lc3_enc_start (GstAudioEncoder * encoder); +static gboolean gst_lc3_enc_stop (GstAudioEncoder * encoder); +static gboolean gst_lc3_enc_set_format (GstAudioEncoder * encoder, + GstAudioInfo * info); +static GstFlowReturn gst_lc3_enc_handle_frame (GstAudioEncoder * encoder, + GstBuffer * buffer); + +#define DEFAULT_BITRATE_PER_CHANNEL 160000 + +static GstStaticPadTemplate gst_lc3_enc_src_template = +GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/x-lc3, " + "rate = (int) { " SAMPLE_RATES " }, " + "channels = (int) [1, MAX], " + "frame-bytes = (int) [" FRAME_BYTES_RANGE "], " + "frame-duration-us = (int) { " FRAME_DURATIONS "}, " + "framed=(boolean) true") + ); + +static GstStaticPadTemplate gst_lc3_enc_sink_template = +GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/x-raw, " + "format = " FORMAT ", " + "rate = (int) { " SAMPLE_RATES " }, channels = (int) [1, MAX]") + ); + +static void +gst_lc3_enc_class_init (GstLc3EncClass * klass) +{ + GstAudioEncoderClass *audio_encoder_class = GST_AUDIO_ENCODER_CLASS (klass); + + audio_encoder_class->start = GST_DEBUG_FUNCPTR (gst_lc3_enc_start); + audio_encoder_class->stop = GST_DEBUG_FUNCPTR (gst_lc3_enc_stop); + audio_encoder_class->set_format = GST_DEBUG_FUNCPTR (gst_lc3_enc_set_format); + audio_encoder_class->handle_frame = + GST_DEBUG_FUNCPTR (gst_lc3_enc_handle_frame); + + gst_element_class_add_static_pad_template (GST_ELEMENT_CLASS (klass), + &gst_lc3_enc_src_template); + gst_element_class_add_static_pad_template (GST_ELEMENT_CLASS (klass), + &gst_lc3_enc_sink_template); + + gst_element_class_set_static_metadata (GST_ELEMENT_CLASS (klass), + "LC3 Bluetooth Audio encoder", "Codec/Encoder/Audio", + "Encodes a raw audio stream to LC3", + "Taruntej Kanakamalla "); + + GST_DEBUG_CATEGORY_INIT (gst_lc3_enc_debug_category, "lc3enc", 0, + "debug category for lc3enc element"); +} + +static void +gst_lc3_enc_init (GstLc3Enc * lc3_enc) +{ +} + +static gboolean +gst_lc3_enc_start (GstAudioEncoder * encoder) +{ + GstLc3Enc *lc3_enc = GST_LC3_ENC (encoder); + + lc3_enc->enc_ch = NULL; + lc3_enc->frame_bytes = 0; + /* Set to true at the start of processing */ + lc3_enc->first_frame = TRUE; + lc3_enc->pending_bytes = 0; + + return TRUE; +} + +static gboolean +gst_lc3_enc_stop (GstAudioEncoder * encoder) +{ + GstLc3Enc *lc3_enc = GST_LC3_ENC (encoder); + + if (lc3_enc->enc_ch != NULL) { + for (int ich = 0; ich < lc3_enc->channels; ich++) { + g_free (lc3_enc->enc_ch[ich]); + lc3_enc->enc_ch[ich] = NULL; + } + + g_free (lc3_enc->enc_ch); + lc3_enc->enc_ch = NULL; + } + + return TRUE; +} + +static gboolean +gst_lc3_enc_set_format (GstAudioEncoder * encoder, GstAudioInfo * info) +{ + GstLc3Enc *lc3_enc = GST_LC3_ENC (encoder); + GstCaps *caps = NULL, *filter_caps = NULL; + GstCaps *output_caps = NULL; + GstStructure *s; + GstClockTime latency; + + lc3_enc->bpf = GST_AUDIO_INFO_BPF (info); + + switch (GST_AUDIO_INFO_FORMAT (info)) { + case GST_AUDIO_FORMAT_S16LE: + lc3_enc->format = LC3_PCM_FORMAT_S16; + break; + case GST_AUDIO_FORMAT_S24LE: + lc3_enc->format = LC3_PCM_FORMAT_S24_3LE; + break; + case GST_AUDIO_FORMAT_F32: + lc3_enc->format = LC3_PCM_FORMAT_FLOAT; + break; + case GST_AUDIO_FORMAT_S24_32LE: + default: + lc3_enc->format = LC3_PCM_FORMAT_S24; + break; + } + + caps = gst_pad_get_allowed_caps (GST_AUDIO_ENCODER_SRC_PAD (lc3_enc)); + if (caps == NULL) + caps = gst_static_pad_template_get_caps (&gst_lc3_enc_src_template); + else if (gst_caps_is_empty (caps)) + goto failure; + + filter_caps = gst_caps_new_simple ("audio/x-lc3", "rate", G_TYPE_INT, + GST_AUDIO_INFO_RATE (info), "channels", G_TYPE_INT, + GST_AUDIO_INFO_CHANNELS (info), NULL); + + output_caps = gst_caps_intersect (caps, filter_caps); + + if (output_caps == NULL || gst_caps_is_empty (output_caps)) { + GST_WARNING_OBJECT (lc3_enc, + "Couldn't negotiate filter caps %" GST_PTR_FORMAT + " and allowed output caps %" GST_PTR_FORMAT, filter_caps, caps); + + goto failure; + } + + gst_caps_unref (filter_caps); + filter_caps = NULL; + gst_caps_unref (caps); + caps = NULL; + + GST_DEBUG_OBJECT (lc3_enc, "fixating caps %" GST_PTR_FORMAT, output_caps); + output_caps = gst_caps_truncate (output_caps); + GST_DEBUG_OBJECT (lc3_enc, "truncated caps %" GST_PTR_FORMAT, output_caps); + + s = gst_caps_get_structure (output_caps, 0); + + gst_structure_get_int (s, "rate", &lc3_enc->rate); + gst_structure_get_int (s, "channels", &lc3_enc->channels); + gst_structure_get_int (s, "frame-bytes", &lc3_enc->frame_bytes); + + if (gst_structure_fixate_field (s, "frame-duration-us")) { + gst_structure_get_int (s, "frame-duration-us", &lc3_enc->frame_duration_us); + } else { + lc3_enc->frame_duration_us = FRAME_DURATION_10000US; + + GST_INFO_OBJECT (lc3_enc, "Frame duration not fixed, setting to %d", + lc3_enc->frame_duration_us); + gst_caps_set_simple (output_caps, "frame-duration-us", G_TYPE_INT, + lc3_enc->frame_duration_us, NULL); + } + + if (lc3_enc->frame_bytes == 0) { + /* fixate_field() is always setting the frame_bytes to 20 which is not desired + * since we can get the value using frame duration and default bitrate + * compute the frame bytes and set the value to the caps + */ + + lc3_enc->frame_bytes = lc3_frame_bytes (lc3_enc->frame_duration_us, + DEFAULT_BITRATE_PER_CHANNEL); + GST_INFO_OBJECT (lc3_enc, "frame bytes computed %d using duration %d", + lc3_enc->frame_bytes, lc3_enc->frame_duration_us); + + gst_caps_set_simple (output_caps, "frame-bytes", G_TYPE_INT, + lc3_enc->frame_bytes, NULL); + } + + GST_INFO_OBJECT (lc3_enc, "output caps %" GST_PTR_FORMAT, output_caps); + + lc3_enc->frame_samples = + lc3_frame_samples (lc3_enc->frame_duration_us, lc3_enc->rate); + + gst_audio_encoder_set_frame_samples_min (encoder, lc3_enc->frame_samples); + gst_audio_encoder_set_frame_samples_max (encoder, lc3_enc->frame_samples); + gst_audio_encoder_set_frame_max (encoder, 1); + + latency = + gst_util_uint64_scale_int (lc3_enc->frame_samples, GST_SECOND, + lc3_enc->rate); + gst_audio_encoder_set_latency (encoder, latency, latency); + + /* Free the encoder handles if it was initialised previously */ + if (lc3_enc->enc_ch != NULL) { + for (int ich = 0; ich < lc3_enc->channels; ich++) { + g_free (lc3_enc->enc_ch[ich]); + lc3_enc->enc_ch[ich] = NULL; + } + g_free (lc3_enc->enc_ch); + lc3_enc->enc_ch = NULL; + } + + lc3_enc->enc_ch = + (lc3_encoder_t *) g_malloc (sizeof (lc3_encoder_t) * lc3_enc->channels); + + for (guint8 i = 0; i < lc3_enc->channels; i++) { + /* The encoder can resample for us. But we leave the resampling to + * happen before encoding explicitly for now. So pass the same sample rate + * for sr_hz and sr_pcm_hz + */ + lc3_enc->enc_ch[i] = + lc3_setup_encoder (lc3_enc->frame_duration_us, lc3_enc->rate, + lc3_enc->rate, g_malloc (lc3_encoder_size (lc3_enc->frame_duration_us, + lc3_enc->rate))); + + if (lc3_enc->enc_ch[i] == NULL) { + GST_ERROR_OBJECT (lc3_enc, + "Failed to create encoder handle for channel %" G_GUINT32_FORMAT, i); + goto failure; + } + } + + if (!gst_audio_encoder_set_output_format (encoder, output_caps)) + goto failure; + + gst_caps_unref (output_caps); + + return gst_audio_encoder_negotiate (encoder); + +failure: + if (output_caps) + gst_caps_unref (output_caps); + if (caps) + gst_caps_unref (caps); + if (filter_caps) + gst_caps_unref (filter_caps); + return FALSE; +} + +static GstFlowReturn +gst_lc3_enc_handle_frame (GstAudioEncoder * encoder, GstBuffer * buffer) +{ + GstLc3Enc *lc3_enc = GST_LC3_ENC (encoder); + GstMapInfo in_map = GST_MAP_INFO_INIT, out_map = GST_MAP_INFO_INIT; + GstBuffer *outbuf = NULL; + guint samplesize, stride, req_samples, req_bytes, frame_bytes; + guint8 *pcm_in; + gint ret = -1; + guint64 trim_start = 0, trim_end = 0; + + if (buffer == NULL && !lc3_enc->pending_bytes) + return GST_FLOW_OK; + + if (G_UNLIKELY (lc3_enc->channels == 0)) + return GST_FLOW_ERROR; + + if (buffer && !gst_buffer_map (buffer, &in_map, GST_MAP_READ)) + goto map_failed; + + GST_TRACE_OBJECT (lc3_enc, + "encoding %" G_GSIZE_FORMAT " frame samples of %" G_GSIZE_FORMAT + " bytes", in_map.size / lc3_enc->bpf, in_map.size); + + frame_bytes = lc3_enc->frame_bytes; + + /* allocate frame_bytes for each channel in the output buffer */ + outbuf = + gst_audio_encoder_allocate_output_buffer (encoder, + frame_bytes * lc3_enc->channels); + + if (outbuf == NULL) + goto no_buffer; + + if (!gst_buffer_map (outbuf, &out_map, GST_MAP_WRITE)) + goto map_failed; + + stride = lc3_enc->channels; + samplesize = lc3_enc->bpf / lc3_enc->channels; + + /* Calculate the expected bytes */ + req_samples = lc3_enc->frame_samples; + req_bytes = req_samples * lc3_enc->bpf; + + if (lc3_enc->first_frame) { + /* LC3 encoder introduces extra samples as a part of the + * algorithmic delay at the beginning of the frame + */ + lc3_enc->pending_bytes = + lc3_enc->bpf * lc3_delay_samples (lc3_enc->frame_duration_us, + lc3_enc->rate); + + /* trim start 'delay_samples' bytes for the first frame */ + trim_start = lc3_enc->pending_bytes / lc3_enc->bpf; + lc3_enc->first_frame = FALSE; + } + + if (in_map.size < req_bytes) { + /* update the pending bytes and trim_end */ + if (in_map.size + lc3_enc->pending_bytes > req_bytes) { + lc3_enc->pending_bytes = in_map.size + lc3_enc->pending_bytes - req_bytes; + } else { + trim_end = + (req_bytes - in_map.size - lc3_enc->pending_bytes) / lc3_enc->bpf; + lc3_enc->pending_bytes = 0; + } + + /* The encoder always expects fixed number of bytes in the input + * If we get less bytes than req_bytes, most likely in the last iteration, + * add zero-padding bytes at the end + */ + pcm_in = (guint8 *) g_malloc0 (req_bytes); + if (in_map.size && in_map.data) + memcpy (pcm_in, in_map.data, in_map.size); + } else { + pcm_in = in_map.data; + } + + if (trim_start || trim_end) { + GST_TRACE_OBJECT (lc3_enc, + "Adding trim-start %" G_GUINT64_FORMAT " trim-end %" G_GUINT64_FORMAT, + trim_start, trim_end); + gst_buffer_add_audio_clipping_meta (outbuf, GST_FORMAT_DEFAULT, trim_start, + trim_end); + } + + for (guint8 ch = 0; ch < lc3_enc->channels; ch++) { + ret = lc3_encode (lc3_enc->enc_ch[ch], lc3_enc->format, + pcm_in + (ch * samplesize), stride, frame_bytes, + out_map.data + (ch * frame_bytes)); + + if (ret < 0) { + GST_WARNING_OBJECT (lc3_enc, + "encoding error: invalid enc handle or frame_bytes"); + break; + } + } + + if (in_map.size < req_bytes) + g_free (pcm_in); + + gst_buffer_unmap (outbuf, &out_map); + if (buffer) + gst_buffer_unmap (buffer, &in_map); + + if (ret < 0) + return GST_FLOW_ERROR; + + return gst_audio_encoder_finish_frame (encoder, outbuf, req_samples); + +no_buffer: + { + if (buffer) + gst_buffer_unmap (buffer, &in_map); + GST_ELEMENT_ERROR (lc3_enc, STREAM, FAILED, (NULL), + ("Could not allocate output buffer")); + return GST_FLOW_ERROR; + } + +map_failed: + { + if (buffer) + gst_buffer_unmap (buffer, &in_map); + GST_ELEMENT_ERROR (lc3_enc, STREAM, FAILED, (NULL), + ("Failed to get the buffer memory map")); + return GST_FLOW_ERROR; + } +} diff --git a/subprojects/gst-plugins-bad/ext/lc3/gstlc3enc.h b/subprojects/gst-plugins-bad/ext/lc3/gstlc3enc.h new file mode 100644 index 0000000000..7d1620eb71 --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/gstlc3enc.h @@ -0,0 +1,57 @@ +/* GStreamer LC3 Bluetooth LE audio encoder + * Copyright (C) 2023 Asymptotic Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef _GST_LC3ENC_H_ +#define _GST_LC3ENC_H_ + +#include + +#include + +G_BEGIN_DECLS +#define GST_TYPE_LC3_ENC (gst_lc3_enc_get_type()) +G_DECLARE_FINAL_TYPE (GstLc3Enc, gst_lc3_enc, GST, LC3_ENC, GstAudioEncoder) + +struct _GstLc3Enc +{ + GstAudioEncoder base; + lc3_encoder_t *enc_ch; + enum lc3_pcm_format format; + int rate; + int channels; + int frame_duration_us; + /* byte count per channel, same for all channels */ + int frame_bytes; + /* bytes per sample */ + int bpf; + /* pcm samples per encoded frame */ + int frame_samples; + gboolean first_frame; + int pending_bytes; +}; + +struct _GstLc3EncClass +{ + GstAudioEncoderClass base_class; +}; + +GST_ELEMENT_REGISTER_DECLARE (lc3enc); + +G_END_DECLS +#endif diff --git a/subprojects/gst-plugins-bad/ext/lc3/lc3-plugin.c b/subprojects/gst-plugins-bad/ext/lc3/lc3-plugin.c new file mode 100644 index 0000000000..543f737e34 --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/lc3-plugin.c @@ -0,0 +1,41 @@ +/* GStreamer LC3 audio plugin + * Copyright (C) 2023 Asymptotic Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "gstlc3dec.h" +#include "gstlc3enc.h" + + +static gboolean +plugin_init (GstPlugin * plugin) +{ + gboolean ret = FALSE; + ret |= GST_ELEMENT_REGISTER (lc3dec, plugin); + ret |= GST_ELEMENT_REGISTER (lc3enc, plugin); + return ret; +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + lc3, + "LC3 codec for Bluetooth LE Audio", + plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN); diff --git a/subprojects/gst-plugins-bad/ext/lc3/meson.build b/subprojects/gst-plugins-bad/ext/lc3/meson.build new file mode 100644 index 0000000000..be9cdd760d --- /dev/null +++ b/subprojects/gst-plugins-bad/ext/lc3/meson.build @@ -0,0 +1,20 @@ +lc3_sources = [ + 'lc3-plugin.c', + 'gstlc3dec.c', + 'gstlc3enc.c', +] + +lc3_dep = dependency('liblc3', required:get_option ('lc3')) + +if lc3_dep.found() + gstlc3 = library('gstlc3', + lc3_sources, + c_args: gst_plugins_bad_args, + include_directories: [configinc], + dependencies: [gstaudio_dep, lc3_dep], + install: true, + install_dir: plugins_install_dir, + ) + pkgconfig.generate(gstlc3, install_dir: plugins_pkgconfig_install_dir) + plugins += [gstlc3] +endif diff --git a/subprojects/gst-plugins-bad/ext/meson.build b/subprojects/gst-plugins-bad/ext/meson.build index 169aa059cb..0ca81c79c1 100644 --- a/subprojects/gst-plugins-bad/ext/meson.build +++ b/subprojects/gst-plugins-bad/ext/meson.build @@ -28,6 +28,7 @@ subdir('iqa') subdir('isac') subdir('kate') subdir('ladspa') +subdir('lc3') subdir('ldac') subdir('libde265') subdir('lv2') diff --git a/subprojects/gst-plugins-bad/meson_options.txt b/subprojects/gst-plugins-bad/meson_options.txt index a29d89d3db..a1d9fd2f4f 100644 --- a/subprojects/gst-plugins-bad/meson_options.txt +++ b/subprojects/gst-plugins-bad/meson_options.txt @@ -125,6 +125,7 @@ option('iqa', type : 'feature', value : 'auto', description : 'Image quality ass option('kate', type : 'feature', value : 'auto', description : 'Kate subtitle parser, tagger, and codec plugin') option('kms', type : 'feature', value : 'auto', description : 'KMS video sink plugin') option('ladspa', type : 'feature', value : 'auto', description : 'LADSPA plugin bridge') +option('lc3', type : 'feature', value : 'auto', description : 'LC3 (Bluetooth) LE audio codec plugin') option('ldac', type : 'feature', value : 'auto', description : 'LDAC bluetooth audio codec plugin') option('libde265', type : 'feature', value : 'auto', description : 'HEVC/H.265 video decoder plugin') option('openaptx', type : 'feature', value : 'auto', description : 'Open Source implementation of Audio Processing Technology codec (aptX) plugin') diff --git a/subprojects/liblc3.wrap b/subprojects/liblc3.wrap new file mode 100644 index 0000000000..3520a18c47 --- /dev/null +++ b/subprojects/liblc3.wrap @@ -0,0 +1,6 @@ +[wrap-git] +directory=liblc3 +url=https://github.com/google/liblc3.git +depth=1 +revision=v1.0.3 +