diff mbox series

[FFmpeg-devel] avformat/lc3: Add file format for LC3/LC3plus transport

Message ID 20240408222733.1422670-1-asoulier@google.com
State New
Headers show
Series [FFmpeg-devel] avformat/lc3: Add file format for LC3/LC3plus transport | expand

Checks

Context Check Description
yinshiyou/make_loongarch64 success Make finished
yinshiyou/make_fate_loongarch64 success Make fate finished
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Antoine Soulier April 8, 2024, 10:27 p.m. UTC
From: Antoine SOULIER <asoulier@google.com>

A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
test purpose. This is the format implemented here.
---
 Changelog                |   1 +
 doc/muxers.texi          |   6 ++
 libavformat/Makefile     |   2 +
 libavformat/allformats.c |   2 +
 libavformat/lc3dec.c     | 173 +++++++++++++++++++++++++++++++++++++++
 libavformat/lc3enc.c     | 114 ++++++++++++++++++++++++++
 6 files changed, 298 insertions(+)
 create mode 100644 libavformat/lc3dec.c
 create mode 100644 libavformat/lc3enc.c

Comments

Stefano Sabatini April 10, 2024, 9:11 a.m. UTC | #1
On date Monday 2024-04-08 22:27:33 +0000, ffmpeg-devel Mailing List wrote:
> From: Antoine SOULIER <asoulier@google.com>
> 
> A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
> test purpose. This is the format implemented here.
> ---
>  Changelog                |   1 +
>  doc/muxers.texi          |   6 ++
>  libavformat/Makefile     |   2 +
>  libavformat/allformats.c |   2 +
>  libavformat/lc3dec.c     | 173 +++++++++++++++++++++++++++++++++++++++
>  libavformat/lc3enc.c     | 114 ++++++++++++++++++++++++++
>  6 files changed, 298 insertions(+)
>  create mode 100644 libavformat/lc3dec.c
>  create mode 100644 libavformat/lc3enc.c
> 
> diff --git a/Changelog b/Changelog
> index 18e83b99a1..02ed7831ec 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -4,6 +4,7 @@ releases are sorted from youngest to oldest.
>  version <next>:
>  - Raw Captions with Time (RCWT) closed caption demuxer
>  - LC3/LC3plus decoding/encoding using external library liblc3
> +- LC3/LC3plus demuxer and muxer
>  
>  
>  version 7.0:
> diff --git a/doc/muxers.texi b/doc/muxers.texi
> index d8a1f83309..ed4144f6d1 100644
> --- a/doc/muxers.texi
> +++ b/doc/muxers.texi
> @@ -2700,6 +2700,12 @@ computer-generated compositions.
>  
>  This muxer accepts a single audio stream containing PCM data.
>  
> +@section lc3
> +Bluetooth SIG Low Complexity Communication Codec audio (LC3), or
> +ETSI TS 103 634 Low Complexity Communication Codec plus (LC3plus).
> +
> +This muxer accepts a single @code{lc3} audio stream.
> +
>  @section matroska
>  
>  Matroska container muxer.
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index 9981799cc9..027d0cdae5 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -332,6 +332,8 @@ OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
>  OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
>  OBJS-$(CONFIG_LAF_DEMUXER)               += lafdec.o
>  OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
> +OBJS-$(CONFIG_LC3_DEMUXER)               += lc3dec.o
> +OBJS-$(CONFIG_LC3_MUXER)                 += lc3enc.o
>  OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
>  OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
>  OBJS-$(CONFIG_LUODAT_DEMUXER)            += luodatdec.o
> diff --git a/libavformat/allformats.c b/libavformat/allformats.c
> index ae925dcf60..305fa46532 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -252,6 +252,8 @@ extern const FFInputFormat  ff_kvag_demuxer;
>  extern const FFOutputFormat ff_kvag_muxer;
>  extern const FFInputFormat  ff_laf_demuxer;
>  extern const FFOutputFormat ff_latm_muxer;
> +extern const FFInputFormat  ff_lc3_demuxer;
> +extern const FFOutputFormat ff_lc3_muxer;
>  extern const FFInputFormat  ff_lmlm4_demuxer;
>  extern const FFInputFormat  ff_loas_demuxer;
>  extern const FFInputFormat  ff_luodat_demuxer;
> diff --git a/libavformat/lc3dec.c b/libavformat/lc3dec.c
> new file mode 100644
> index 0000000000..371e9242d5
> --- /dev/null
> +++ b/libavformat/lc3dec.c
> @@ -0,0 +1,173 @@
> +/*
> + * LC3 demuxer
> + * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
> + *
> + * This file is part of FFmpeg.
> + *
> + * FFmpeg is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public
> + * License as published by the Free Software Foundation; either
> + * version 2.1 of the License, or (at your option) any later version.
> + *
> + * FFmpeg 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
> + * Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> + * License along with FFmpeg; if not, write to the Free Software
> + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
> + */
> +
> +/**
> + * @file
> + * Based on the file format specified by :
> + *
> + * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
> + *   https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
> + *   3.2.8.2 Reference LC3 Codec Bitstream Format
> + *
> + * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
> + *   https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
> + *   LC3plus conformance script package
> + */
> +
> +#include "libavcodec/packet.h"
> +#include "libavutil/intreadwrite.h"
> +
> +#include "avformat.h"
> +#include "avio.h"
> +#include "demux.h"
> +#include "internal.h"
> +
> +typedef struct LC3DemuxContext {
> +    int frame_samples;
> +    int64_t end_dts;
> +} LC3DemuxContext;
> +
> +static int lc3_read_probe(const AVProbeData *p)
> +{
> +    int frame_us, srate_hz;
> +
> +    if (p->buf_size < 12)
> +        return 0;
> +
> +    if (AV_RB16(p->buf + 0) != 0x1ccc ||
> +        AV_RL16(p->buf + 2) <  9 * sizeof(uint16_t))
> +        return 0;
> +
> +    srate_hz = AV_RL16(p->buf + 4) * 100;
> +    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
> +        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000)
> +        return 0;
> +
> +    frame_us = AV_RL16(p->buf + 10) * 10;
> +    if (frame_us != 2500 && frame_us !=  5000 &&
> +        frame_us != 7500 && frame_us != 10000)
> +        return 0;
> +
> +    return AVPROBE_SCORE_MAX;
> +}
> +
> +static int lc3_read_header(AVFormatContext *s)
> +{
> +    LC3DemuxContext *lc3 = s->priv_data;
> +    AVStream *st = NULL;
> +    uint16_t tag, hdr_size;
> +    uint32_t length;
> +    int srate_hz, frame_us, channels, bit_rate;
> +    int ep_mode, hr_mode;
> +    int num_extra_params;
> +    int delay, ret;
> +
> +    tag = avio_rb16(s->pb);
> +    hdr_size = avio_rl16(s->pb);
> +
> +    if (tag != 0x1ccc || hdr_size < 9 * sizeof(uint16_t))
> +        return AVERROR_INVALIDDATA;
> +
> +    num_extra_params = hdr_size / sizeof(uint16_t) - 9;
> +
> +    srate_hz = avio_rl16(s->pb) * 100;
> +    bit_rate = avio_rl16(s->pb) * 100;
> +    channels = avio_rl16(s->pb);
> +    frame_us = avio_rl16(s->pb) * 10;
> +    ep_mode  = avio_rl16(s->pb) != 0;
> +    length   = avio_rl32(s->pb);
> +    hr_mode  = num_extra_params >= 1 && avio_rl16(s->pb);
> +

> +    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
> +        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000) {
> +        av_log(s, AV_LOG_ERROR, "Incompatible LC3 sample rate: %d Hz.\n",
> +               srate_hz);
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    if (frame_us != 2500 && frame_us !=  5000 &&
> +        frame_us != 7500 && frame_us != 10000) {
> +        av_log(s, AV_LOG_ERROR, "Incompatible LC3 frame duration: %.1f ms.\n",
> +               frame_us / 1000.f);
> +        return AVERROR_INVALIDDATA;
> +    }

This looks like the previous patch, is that intended?

(Also probably it's best to update the thread in place of sending a
new email, toghether with a short notice about the changes, so we
don't lose the context).

Thanks.
Antoine Soulier April 10, 2024, 5:41 p.m. UTC | #2
Sorry for that, I missed the rebasing.
I have followed your recommendations, and put the muxer/demuxer in the same
file.
Here is the patch:

From f85989288a99130eb3583d5ae9c5bf441e961ed4 Mon Sep 17 00:00:00 2001
From: Antoine SOULIER <asoulier@google.com>
Date: Thu, 4 Apr 2024 22:38:03 +0000
Subject: [PATCH] avformat/lc3: Add file format for LC3/LC3plus transport

A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
test purpose. This is the format implemented here.
---
 Changelog                |   1 +
 doc/muxers.texi          |   8 ++
 libavformat/Makefile     |   2 +
 libavformat/allformats.c |   2 +
 libavformat/lc3.c        | 248 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 261 insertions(+)
 create mode 100644 libavformat/lc3.c

diff --git a/Changelog b/Changelog
index b7a1af4083..5c8f505211 100644
--- a/Changelog
+++ b/Changelog
@@ -5,6 +5,7 @@ version <next>:
 - Raw Captions with Time (RCWT) closed caption demuxer
 - LC3/LC3plus decoding/encoding using external library liblc3
 - ffmpeg CLI filtergraph chaining
+- LC3/LC3plus demuxer and muxer


 version 7.0:
diff --git a/doc/muxers.texi b/doc/muxers.texi
index 4b30970b78..032f48410e 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -2703,6 +2703,14 @@ This muxer accepts a single audio stream containing
PCM data.
 @section ivf
 On2 IVF muxer.

+@section lc3
+Bluetooth SIG Low Complexity Communication Codec audio (LC3), or
+ETSI TS 103 634 Low Complexity Communication Codec plus (LC3plus).
+
+This muxer accepts a single @code{lc3} audio stream.
+
+@section matroska
+
 IVF was developed by On2 Technologies (formerly known as Duck
 Corporation), to store internally developed codecs.

diff --git a/libavformat/Makefile b/libavformat/Makefile
index 9981799cc9..8efe26b6df 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -332,6 +332,8 @@ OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
 OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
 OBJS-$(CONFIG_LAF_DEMUXER)               += lafdec.o
 OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
+OBJS-$(CONFIG_LC3_DEMUXER)               += lc3.o
+OBJS-$(CONFIG_LC3_MUXER)                 += lc3.o
 OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
 OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
 OBJS-$(CONFIG_LUODAT_DEMUXER)            += luodatdec.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index ae925dcf60..305fa46532 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -252,6 +252,8 @@ extern const FFInputFormat  ff_kvag_demuxer;
 extern const FFOutputFormat ff_kvag_muxer;
 extern const FFInputFormat  ff_laf_demuxer;
 extern const FFOutputFormat ff_latm_muxer;
+extern const FFInputFormat  ff_lc3_demuxer;
+extern const FFOutputFormat ff_lc3_muxer;
 extern const FFInputFormat  ff_lmlm4_demuxer;
 extern const FFInputFormat  ff_loas_demuxer;
 extern const FFInputFormat  ff_luodat_demuxer;
diff --git a/libavformat/lc3.c b/libavformat/lc3.c
new file mode 100644
index 0000000000..8b1ec62589
--- /dev/null
+++ b/libavformat/lc3.c
@@ -0,0 +1,248 @@
+/*
+ * LC3 demuxer
+ * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA
+ */
+
+/**
+ * @file
+ * Based on the file format specified by :
+ *
+ * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
+ *
https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
+ *   3.2.8.2 Reference LC3 Codec Bitstream Format
+ *
+ * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
+ *
https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
+ *   LC3plus conformance script package
+ */
+
+#include "config_components.h"
+
+#include "libavcodec/packet.h"
+#include "libavutil/intreadwrite.h"
+
+#include "avformat.h"
+#include "avio.h"
+#include "demux.h"
+#include "internal.h"
+#include "mux.h"
+
+typedef struct LC3DemuxContext {
+    int frame_samples;
+    int64_t end_dts;
+} LC3DemuxContext;
+
+static int check_frame_length(int srate_hz, int frame_us)
+{
+    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
+        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000)
+        return -1;
+
+    if (frame_us != 2500 && frame_us !=  5000 &&
+        frame_us != 7500 && frame_us != 10000)
+        return -1;
+
+    return 0;
+}
+
+static int lc3_read_probe(const AVProbeData *p)
+{
+    int frame_us, srate_hz;
+
+    if (p->buf_size < 12)
+        return 0;
+
+    if (AV_RB16(p->buf + 0) != 0x1ccc ||
+        AV_RL16(p->buf + 2) <  9 * sizeof(uint16_t))
+        return 0;
+
+    srate_hz = AV_RL16(p->buf + 4) * 100;
+    frame_us = AV_RL16(p->buf + 10) * 10;
+    if (check_frame_length(srate_hz, frame_us) < 0)
+        return 0;
+
+    return AVPROBE_SCORE_MAX;
+}
+
+static int lc3_read_header(AVFormatContext *s)
+{
+    LC3DemuxContext *lc3 = s->priv_data;
+    AVStream *st = NULL;
+    uint16_t tag, hdr_size;
+    uint32_t length;
+    int srate_hz, frame_us, channels, bit_rate;
+    int ep_mode, hr_mode;
+    int num_extra_params;
+    int delay, ret;
+
+    tag = avio_rb16(s->pb);
+    hdr_size = avio_rl16(s->pb);
+
+    if (tag != 0x1ccc || hdr_size < 9 * sizeof(uint16_t))
+        return AVERROR_INVALIDDATA;
+
+    num_extra_params = hdr_size / sizeof(uint16_t) - 9;
+
+    srate_hz = avio_rl16(s->pb) * 100;
+    bit_rate = avio_rl16(s->pb) * 100;
+    channels = avio_rl16(s->pb);
+    frame_us = avio_rl16(s->pb) * 10;
+    ep_mode  = avio_rl16(s->pb) != 0;
+    length   = avio_rl32(s->pb);
+    hr_mode  = num_extra_params >= 1 && avio_rl16(s->pb);
+
+    if (check_frame_length(srate_hz, frame_us) < 0) {
+        av_log(s, AV_LOG_ERROR, "Invalid LC3 sample rate: %d Hz, "
+                                "frame duration: %.1f ms.\n",
+               srate_hz, frame_us / 1000.f);
+        return AVERROR_INVALIDDATA;
+    }
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    avpriv_set_pts_info(st, 64, 1, srate_hz);
+    avpriv_update_cur_dts(s, st, 0);
+    st->duration = length;
+
+    st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
+    st->codecpar->codec_id = AV_CODEC_ID_LC3;
+    st->codecpar->sample_rate = srate_hz;
+    st->codecpar->bit_rate = bit_rate;
+    st->codecpar->ch_layout.nb_channels = channels;
+
+    if ((ret = ff_alloc_extradata(st->codecpar, 6)) < 0)
+        return ret;
+
+    AV_WL16(st->codecpar->extradata + 0, frame_us / 10);
+    AV_WL16(st->codecpar->extradata + 2, ep_mode);
+    AV_WL16(st->codecpar->extradata + 4, hr_mode);
+
+    lc3->frame_samples = av_rescale(frame_us, srate_hz, 1000*1000);
+
+    delay = av_rescale(frame_us == 7500 ? 4000 : 2500, srate_hz,
1000*1000);
+    lc3->end_dts = length ? length + delay : -1;
+
+    return 0;
+}
+
+static int lc3_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    LC3DemuxContext *lc3 = s->priv_data;
+    AVStream *st = s->streams[0];
+    AVIOContext *pb = s->pb;
+    int64_t pos = avio_tell(pb);
+    int64_t remaining_samples;
+    int ret;
+
+    ret = av_get_packet(s->pb, pkt, avio_rl16(pb));
+    if (ret < 0)
+        return ret;
+
+    pkt->pos = pos;
+
+    remaining_samples = lc3->end_dts < 0 ? lc3->frame_samples :
+                        FFMAX(lc3->end_dts - ffstream(st)->cur_dts, 0);
+    pkt->duration = FFMIN(lc3->frame_samples, remaining_samples);
+
+    return 0;
+}
+
+static av_cold int lc3_muxer_init(AVFormatContext *s)
+{
+    if (s->nb_streams != 1) {
+        av_log(s, AV_LOG_ERROR, "This muxer only supports a single
stream.\n");
+        return AVERROR(EINVAL);
+    }
+
+    return 0;
+}
+
+static int lc3_write_header(AVFormatContext *s)
+{
+    AVStream *st = s->streams[0];
+    int channels = st->codecpar->ch_layout.nb_channels;
+    int srate_hz = st->codecpar->sample_rate;
+    int bit_rate = st->codecpar->bit_rate;
+    int frame_us, ep_mode, hr_mode;
+    uint32_t nb_samples = av_rescale_q(
+        st->duration, st->time_base, (AVRational){ 1, srate_hz });
+
+    if (st->codecpar->extradata_size < 6)
+        return AVERROR_INVALIDDATA;
+
+    frame_us = AV_RL16(st->codecpar->extradata + 0) * 10;
+    ep_mode = AV_RL16(st->codecpar->extradata + 2) != 0;
+    hr_mode = AV_RL16(st->codecpar->extradata + 4) != 0;
+
+    if (check_frame_length(srate_hz, frame_us) < 0) {
+        av_log(s, AV_LOG_ERROR, "Invalid LC3 sample rate: %d Hz, "
+                                "frame duration: %.1f ms.\n",
+               srate_hz, frame_us / 1000.f);
+        return AVERROR_INVALIDDATA;
+    }
+
+    avio_wb16(s->pb, 0x1ccc);
+    avio_wl16(s->pb, (9 + hr_mode) * sizeof(uint16_t));
+    avio_wl16(s->pb, srate_hz / 100);
+    avio_wl16(s->pb, bit_rate / 100);
+    avio_wl16(s->pb, channels);
+    avio_wl16(s->pb, frame_us / 10);
+    avio_wl16(s->pb, ep_mode);
+    avio_wl32(s->pb, nb_samples);
+    if (hr_mode)
+        avio_wl16(s->pb, hr_mode);
+
+    return 0;
+}
+
+static int lc3_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    avio_wl16(s->pb, pkt->size);
+    avio_write(s->pb, pkt->data, pkt->size);
+    return 0;
+}
+
+#if CONFIG_LC3_DEMUXER
+const FFInputFormat ff_lc3_demuxer = {
+    .p.name         = "lc3",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity
Communication Codec)"),
+    .p.extensions   = "lc3",
+    .p.flags        = AVFMT_GENERIC_INDEX,
+    .priv_data_size = sizeof(LC3DemuxContext),
+    .read_probe     = lc3_read_probe,
+    .read_header    = lc3_read_header,
+    .read_packet    = lc3_read_packet,
+};
+#endif
+
+#if CONFIG_LC3_MUXER
+const FFOutputFormat ff_lc3_muxer = {
+    .p.name        = "lc3",
+    .p.long_name   = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity
Communication Codec)"),
+    .p.extensions  = "lc3",
+    .p.audio_codec = AV_CODEC_ID_LC3,
+    .p.video_codec = AV_CODEC_ID_NONE,
+    .p.flags       = AVFMT_NOTIMESTAMPS,
+    .init          = lc3_muxer_init,
+    .write_header  = lc3_write_header,
+    .write_packet  = lc3_write_packet,
+};
+#endif
Andreas Rheinhardt April 10, 2024, 6:26 p.m. UTC | #3
Antoine Soulier via ffmpeg-devel:
> Sorry for that, I missed the rebasing.
> I have followed your recommendations, and put the muxer/demuxer in the same
> file.
> Here is the patch:
> 

Send your patch either with git send-email or attach it to your mail; do
not copy the file produced by git format-patch into your mail.

> From f85989288a99130eb3583d5ae9c5bf441e961ed4 Mon Sep 17 00:00:00 2001
> From: Antoine SOULIER <asoulier@google.com>
> Date: Thu, 4 Apr 2024 22:38:03 +0000
> Subject: [PATCH] avformat/lc3: Add file format for LC3/LC3plus transport
> 
> A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
> test purpose. This is the format implemented here.
> ---
>  Changelog                |   1 +
>  doc/muxers.texi          |   8 ++
>  libavformat/Makefile     |   2 +
>  libavformat/allformats.c |   2 +
>  libavformat/lc3.c        | 248 +++++++++++++++++++++++++++++++++++++++
>  5 files changed, 261 insertions(+)
>  create mode 100644 libavformat/lc3.c
> 
> diff --git a/Changelog b/Changelog
> index b7a1af4083..5c8f505211 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -5,6 +5,7 @@ version <next>:
>  - Raw Captions with Time (RCWT) closed caption demuxer
>  - LC3/LC3plus decoding/encoding using external library liblc3
>  - ffmpeg CLI filtergraph chaining
> +- LC3/LC3plus demuxer and muxer
> 
> 
>  version 7.0:
> diff --git a/doc/muxers.texi b/doc/muxers.texi
> index 4b30970b78..032f48410e 100644
> --- a/doc/muxers.texi
> +++ b/doc/muxers.texi
> @@ -2703,6 +2703,14 @@ This muxer accepts a single audio stream containing
> PCM data.
>  @section ivf
>  On2 IVF muxer.
> 
> +@section lc3
> +Bluetooth SIG Low Complexity Communication Codec audio (LC3), or
> +ETSI TS 103 634 Low Complexity Communication Codec plus (LC3plus).
> +
> +This muxer accepts a single @code{lc3} audio stream.
> +
> +@section matroska

Nonsense. You are adding this in the middle of the description of the
IVF muxer.

> +
>  IVF was developed by On2 Technologies (formerly known as Duck
>  Corporation), to store internally developed codecs.
> 
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index 9981799cc9..8efe26b6df 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -332,6 +332,8 @@ OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
>  OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
>  OBJS-$(CONFIG_LAF_DEMUXER)               += lafdec.o
>  OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
> +OBJS-$(CONFIG_LC3_DEMUXER)               += lc3.o
> +OBJS-$(CONFIG_LC3_MUXER)                 += lc3.o
>  OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
>  OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
>  OBJS-$(CONFIG_LUODAT_DEMUXER)            += luodatdec.o
> diff --git a/libavformat/allformats.c b/libavformat/allformats.c
> index ae925dcf60..305fa46532 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -252,6 +252,8 @@ extern const FFInputFormat  ff_kvag_demuxer;
>  extern const FFOutputFormat ff_kvag_muxer;
>  extern const FFInputFormat  ff_laf_demuxer;
>  extern const FFOutputFormat ff_latm_muxer;
> +extern const FFInputFormat  ff_lc3_demuxer;
> +extern const FFOutputFormat ff_lc3_muxer;
>  extern const FFInputFormat  ff_lmlm4_demuxer;
>  extern const FFInputFormat  ff_loas_demuxer;
>  extern const FFInputFormat  ff_luodat_demuxer;
> diff --git a/libavformat/lc3.c b/libavformat/lc3.c
> new file mode 100644
> index 0000000000..8b1ec62589
> --- /dev/null
> +++ b/libavformat/lc3.c
> @@ -0,0 +1,248 @@
> +/*
> + * LC3 demuxer
> + * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
> + *
> + * This file is part of FFmpeg.
> + *
> + * FFmpeg is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public
> + * License as published by the Free Software Foundation; either
> + * version 2.1 of the License, or (at your option) any later version.
> + *
> + * FFmpeg 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
> + * Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> + * License along with FFmpeg; if not, write to the Free Software
> + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> 02110-1301 USA
> + */
> +
> +/**
> + * @file
> + * Based on the file format specified by :
> + *
> + * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
> + *
> https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
> + *   3.2.8.2 Reference LC3 Codec Bitstream Format
> + *
> + * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
> + *
> https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
> + *   LC3plus conformance script package
> + */
> +
> +#include "config_components.h"
> +
> +#include "libavcodec/packet.h"
> +#include "libavutil/intreadwrite.h"
> +
> +#include "avformat.h"
> +#include "avio.h"
> +#include "demux.h"
> +#include "internal.h"
> +#include "mux.h"
> +
> +typedef struct LC3DemuxContext {
> +    int frame_samples;
> +    int64_t end_dts;
> +} LC3DemuxContext;
> +
> +static int check_frame_length(int srate_hz, int frame_us)
> +{
> +    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
> +        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000)
> +        return -1;
> +
> +    if (frame_us != 2500 && frame_us !=  5000 &&
> +        frame_us != 7500 && frame_us != 10000)
> +        return -1;
> +
> +    return 0;
> +}
> +
> +static int lc3_read_probe(const AVProbeData *p)
> +{
> +    int frame_us, srate_hz;
> +
> +    if (p->buf_size < 12)
> +        return 0;
> +
> +    if (AV_RB16(p->buf + 0) != 0x1ccc ||
> +        AV_RL16(p->buf + 2) <  9 * sizeof(uint16_t))
> +        return 0;
> +
> +    srate_hz = AV_RL16(p->buf + 4) * 100;
> +    frame_us = AV_RL16(p->buf + 10) * 10;
> +    if (check_frame_length(srate_hz, frame_us) < 0)
> +        return 0;
> +
> +    return AVPROBE_SCORE_MAX;
> +}
> +
> +static int lc3_read_header(AVFormatContext *s)
> +{
> +    LC3DemuxContext *lc3 = s->priv_data;
> +    AVStream *st = NULL;
> +    uint16_t tag, hdr_size;
> +    uint32_t length;
> +    int srate_hz, frame_us, channels, bit_rate;
> +    int ep_mode, hr_mode;
> +    int num_extra_params;
> +    int delay, ret;
> +
> +    tag = avio_rb16(s->pb);
> +    hdr_size = avio_rl16(s->pb);
> +
> +    if (tag != 0x1ccc || hdr_size < 9 * sizeof(uint16_t))
> +        return AVERROR_INVALIDDATA;
> +
> +    num_extra_params = hdr_size / sizeof(uint16_t) - 9;
> +
> +    srate_hz = avio_rl16(s->pb) * 100;
> +    bit_rate = avio_rl16(s->pb) * 100;
> +    channels = avio_rl16(s->pb);
> +    frame_us = avio_rl16(s->pb) * 10;
> +    ep_mode  = avio_rl16(s->pb) != 0;
> +    length   = avio_rl32(s->pb);
> +    hr_mode  = num_extra_params >= 1 && avio_rl16(s->pb);
> +
> +    if (check_frame_length(srate_hz, frame_us) < 0) {
> +        av_log(s, AV_LOG_ERROR, "Invalid LC3 sample rate: %d Hz, "
> +                                "frame duration: %.1f ms.\n",
> +               srate_hz, frame_us / 1000.f);
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    st = avformat_new_stream(s, NULL);
> +    if (!st)
> +        return AVERROR(ENOMEM);
> +
> +    avpriv_set_pts_info(st, 64, 1, srate_hz);
> +    avpriv_update_cur_dts(s, st, 0);
> +    st->duration = length;
> +
> +    st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
> +    st->codecpar->codec_id = AV_CODEC_ID_LC3;
> +    st->codecpar->sample_rate = srate_hz;
> +    st->codecpar->bit_rate = bit_rate;
> +    st->codecpar->ch_layout.nb_channels = channels;
> +
> +    if ((ret = ff_alloc_extradata(st->codecpar, 6)) < 0)
> +        return ret;
> +
> +    AV_WL16(st->codecpar->extradata + 0, frame_us / 10);
> +    AV_WL16(st->codecpar->extradata + 2, ep_mode);
> +    AV_WL16(st->codecpar->extradata + 4, hr_mode);
> +
> +    lc3->frame_samples = av_rescale(frame_us, srate_hz, 1000*1000);
> +
> +    delay = av_rescale(frame_us == 7500 ? 4000 : 2500, srate_hz,
> 1000*1000);
> +    lc3->end_dts = length ? length + delay : -1;
> +
> +    return 0;
> +}
> +
> +static int lc3_read_packet(AVFormatContext *s, AVPacket *pkt)
> +{
> +    LC3DemuxContext *lc3 = s->priv_data;
> +    AVStream *st = s->streams[0];
> +    AVIOContext *pb = s->pb;
> +    int64_t pos = avio_tell(pb);
> +    int64_t remaining_samples;
> +    int ret;
> +
> +    ret = av_get_packet(s->pb, pkt, avio_rl16(pb));
> +    if (ret < 0)
> +        return ret;
> +
> +    pkt->pos = pos;
> +
> +    remaining_samples = lc3->end_dts < 0 ? lc3->frame_samples :
> +                        FFMAX(lc3->end_dts - ffstream(st)->cur_dts, 0);
> +    pkt->duration = FFMIN(lc3->frame_samples, remaining_samples);
> +
> +    return 0;
> +}
> +
> +static av_cold int lc3_muxer_init(AVFormatContext *s)
> +{
> +    if (s->nb_streams != 1) {
> +        av_log(s, AV_LOG_ERROR, "This muxer only supports a single
> stream.\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    return 0;
> +}
> +
> +static int lc3_write_header(AVFormatContext *s)
> +{
> +    AVStream *st = s->streams[0];
> +    int channels = st->codecpar->ch_layout.nb_channels;
> +    int srate_hz = st->codecpar->sample_rate;
> +    int bit_rate = st->codecpar->bit_rate;
> +    int frame_us, ep_mode, hr_mode;
> +    uint32_t nb_samples = av_rescale_q(
> +        st->duration, st->time_base, (AVRational){ 1, srate_hz });
> +
> +    if (st->codecpar->extradata_size < 6)
> +        return AVERROR_INVALIDDATA;
> +
> +    frame_us = AV_RL16(st->codecpar->extradata + 0) * 10;
> +    ep_mode = AV_RL16(st->codecpar->extradata + 2) != 0;
> +    hr_mode = AV_RL16(st->codecpar->extradata + 4) != 0;
> +
> +    if (check_frame_length(srate_hz, frame_us) < 0) {
> +        av_log(s, AV_LOG_ERROR, "Invalid LC3 sample rate: %d Hz, "
> +                                "frame duration: %.1f ms.\n",
> +               srate_hz, frame_us / 1000.f);
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    avio_wb16(s->pb, 0x1ccc);
> +    avio_wl16(s->pb, (9 + hr_mode) * sizeof(uint16_t));
> +    avio_wl16(s->pb, srate_hz / 100);
> +    avio_wl16(s->pb, bit_rate / 100);
> +    avio_wl16(s->pb, channels);
> +    avio_wl16(s->pb, frame_us / 10);
> +    avio_wl16(s->pb, ep_mode);
> +    avio_wl32(s->pb, nb_samples);
> +    if (hr_mode)
> +        avio_wl16(s->pb, hr_mode);
> +
> +    return 0;
> +}
> +
> +static int lc3_write_packet(AVFormatContext *s, AVPacket *pkt)
> +{
> +    avio_wl16(s->pb, pkt->size);
> +    avio_write(s->pb, pkt->data, pkt->size);
> +    return 0;
> +}
> +
> +#if CONFIG_LC3_DEMUXER
> +const FFInputFormat ff_lc3_demuxer = {
> +    .p.name         = "lc3",
> +    .p.long_name    = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity
> Communication Codec)"),
> +    .p.extensions   = "lc3",
> +    .p.flags        = AVFMT_GENERIC_INDEX,
> +    .priv_data_size = sizeof(LC3DemuxContext),
> +    .read_probe     = lc3_read_probe,
> +    .read_header    = lc3_read_header,
> +    .read_packet    = lc3_read_packet,
> +};
> +#endif
> +
> +#if CONFIG_LC3_MUXER
> +const FFOutputFormat ff_lc3_muxer = {
> +    .p.name        = "lc3",
> +    .p.long_name   = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity
> Communication Codec)"),
> +    .p.extensions  = "lc3",
> +    .p.audio_codec = AV_CODEC_ID_LC3,
> +    .p.video_codec = AV_CODEC_ID_NONE,
> +    .p.flags       = AVFMT_NOTIMESTAMPS,
> +    .init          = lc3_muxer_init,
> +    .write_header  = lc3_write_header,
> +    .write_packet  = lc3_write_packet,
> +};
> +#endif

You only put the muxer and demuxer inside #if guards. If only one of
these two is enabled, the other's functions will not be used and lead to
compiler warnings. This can be fixed by putting all the stuff for only
the muxer/demuxer inside the #if (see argo_cvg.c for an example).

- Andreas
Stefano Sabatini April 10, 2024, 9:22 p.m. UTC | #4
On date Wednesday 2024-04-10 20:26:01 +0200, Andreas Rheinhardt wrote:
> Antoine Soulier via ffmpeg-devel:
[...]
> > +#if CONFIG_LC3_DEMUXER
> > +const FFInputFormat ff_lc3_demuxer = {
> > +    .p.name         = "lc3",
> > +    .p.long_name    = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity
> > Communication Codec)"),
> > +    .p.extensions   = "lc3",
> > +    .p.flags        = AVFMT_GENERIC_INDEX,
> > +    .priv_data_size = sizeof(LC3DemuxContext),
> > +    .read_probe     = lc3_read_probe,
> > +    .read_header    = lc3_read_header,
> > +    .read_packet    = lc3_read_packet,
> > +};
> > +#endif
> > +
> > +#if CONFIG_LC3_MUXER
> > +const FFOutputFormat ff_lc3_muxer = {
> > +    .p.name        = "lc3",
> > +    .p.long_name   = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity
> > Communication Codec)"),
> > +    .p.extensions  = "lc3",
> > +    .p.audio_codec = AV_CODEC_ID_LC3,
> > +    .p.video_codec = AV_CODEC_ID_NONE,
> > +    .p.flags       = AVFMT_NOTIMESTAMPS,
> > +    .init          = lc3_muxer_init,
> > +    .write_header  = lc3_write_header,
> > +    .write_packet  = lc3_write_packet,
> > +};
> > +#endif
> 

> You only put the muxer and demuxer inside #if guards. If only one of
> these two is enabled, the other's functions will not be used and lead to
> compiler warnings. This can be fixed by putting all the stuff for only
> the muxer/demuxer inside the #if (see argo_cvg.c for an example).

Note: I pointed to codec2.c, which seems to suffer from the same
issue, argo_cvg.c is indeed a better example.
Antoine Soulier April 10, 2024, 11:46 p.m. UTC | #5
Sure, I thought these warnings were disabled while looking at codec2.c
Sorry for the bad merge of the doc.
Stefano Sabatini April 12, 2024, 1:05 p.m. UTC | #6
On date Wednesday 2024-04-10 16:46:55 -0700, ffmpeg-devel Mailing List wrote:
> Sure, I thought these warnings were disabled while looking at codec2.c
> Sorry for the bad merge of the doc.

> From 975040408f32431efc3fae0a0b8c048f02159515 Mon Sep 17 00:00:00 2001
> From: Antoine SOULIER <asoulier@google.com>
> Date: Thu, 4 Apr 2024 22:38:03 +0000
> Subject: [PATCH] avformat/lc3: Add file format for LC3/LC3plus transport
> 
> A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
> test purpose. This is the format implemented here.
> ---
>  Changelog                |   1 +
>  doc/muxers.texi          |   6 +
>  libavformat/Makefile     |   2 +
>  libavformat/allformats.c |   2 +
>  libavformat/lc3.c        | 253 +++++++++++++++++++++++++++++++++++++++
>  5 files changed, 264 insertions(+)
>  create mode 100644 libavformat/lc3.c
> 
> diff --git a/Changelog b/Changelog
> index b7a1af4083..5c8f505211 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -5,6 +5,7 @@ version <next>:
>  - Raw Captions with Time (RCWT) closed caption demuxer
>  - LC3/LC3plus decoding/encoding using external library liblc3
>  - ffmpeg CLI filtergraph chaining
> +- LC3/LC3plus demuxer and muxer
>  
>  
>  version 7.0:
> diff --git a/doc/muxers.texi b/doc/muxers.texi
> index 4b30970b78..4c14323d50 100644
> --- a/doc/muxers.texi
> +++ b/doc/muxers.texi
> @@ -2725,6 +2725,12 @@ games such as "Real War", and "Real War: Rogue States".
>  
>  This muxer accepts a single @samp{adpcm_ima_ssi} audio stream.
>  
> +@section lc3
> +Bluetooth SIG Low Complexity Communication Codec audio (LC3), or
> +ETSI TS 103 634 Low Complexity Communication Codec plus (LC3plus).
> +

> +This muxer accepts a single @code{lc3} audio stream.

nit++: @samp{lc3} for consistency

> +
>  @section lrc
>  LRC lyrics file format muxer.
>  
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index 9981799cc9..8efe26b6df 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -332,6 +332,8 @@ OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
>  OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
>  OBJS-$(CONFIG_LAF_DEMUXER)               += lafdec.o
>  OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
> +OBJS-$(CONFIG_LC3_DEMUXER)               += lc3.o
> +OBJS-$(CONFIG_LC3_MUXER)                 += lc3.o
>  OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
>  OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
>  OBJS-$(CONFIG_LUODAT_DEMUXER)            += luodatdec.o
> diff --git a/libavformat/allformats.c b/libavformat/allformats.c
> index ae925dcf60..305fa46532 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -252,6 +252,8 @@ extern const FFInputFormat  ff_kvag_demuxer;
>  extern const FFOutputFormat ff_kvag_muxer;
>  extern const FFInputFormat  ff_laf_demuxer;
>  extern const FFOutputFormat ff_latm_muxer;
> +extern const FFInputFormat  ff_lc3_demuxer;
> +extern const FFOutputFormat ff_lc3_muxer;
>  extern const FFInputFormat  ff_lmlm4_demuxer;
>  extern const FFInputFormat  ff_loas_demuxer;
>  extern const FFInputFormat  ff_luodat_demuxer;
> diff --git a/libavformat/lc3.c b/libavformat/lc3.c
> new file mode 100644
> index 0000000000..e27727145b
> --- /dev/null
> +++ b/libavformat/lc3.c
> @@ -0,0 +1,253 @@
> +/*
> + * LC3 demuxer

nit: LC3 demuxer and muxer

> + * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
> + *
> + * This file is part of FFmpeg.
> + *
> + * FFmpeg is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public
> + * License as published by the Free Software Foundation; either
> + * version 2.1 of the License, or (at your option) any later version.
> + *
> + * FFmpeg 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
> + * Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> + * License along with FFmpeg; if not, write to the Free Software
> + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
> + */
> +
> +/**
> + * @file
> + * Based on the file format specified by :
> + *
> + * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
> + *   https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
> + *   3.2.8.2 Reference LC3 Codec Bitstream Format
> + *
> + * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
> + *   https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
> + *   LC3plus conformance script package
> + */
> +
> +#include "config_components.h"
> +
> +#include "libavcodec/packet.h"
> +#include "libavutil/intreadwrite.h"
> +
> +#include "avformat.h"
> +#include "avio.h"
> +#include "demux.h"
> +#include "internal.h"
> +#include "mux.h"
> +
> +static int check_frame_length(int srate_hz, int frame_us)
> +{
> +    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
> +        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000)
> +        return -1;
> +
> +    if (frame_us != 2500 && frame_us !=  5000 &&
> +        frame_us != 7500 && frame_us != 10000)
> +        return -1;
> +
> +    return 0;
> +}
> +
> +#if CONFIG_LC3_DEMUXER
> +
> +typedef struct LC3DemuxContext {
> +    int frame_samples;
> +    int64_t end_dts;
> +} LC3DemuxContext;
> +
> +static int lc3_read_probe(const AVProbeData *p)
> +{
> +    int frame_us, srate_hz;
> +
> +    if (p->buf_size < 12)
> +        return 0;
> +
> +    if (AV_RB16(p->buf + 0) != 0x1ccc ||
> +        AV_RL16(p->buf + 2) <  9 * sizeof(uint16_t))
> +        return 0;
> +
> +    srate_hz = AV_RL16(p->buf + 4) * 100;
> +    frame_us = AV_RL16(p->buf + 10) * 10;
> +    if (check_frame_length(srate_hz, frame_us) < 0)
> +        return 0;
> +
> +    return AVPROBE_SCORE_MAX;
> +}
> +
> +static int lc3_read_header(AVFormatContext *s)
> +{
> +    LC3DemuxContext *lc3 = s->priv_data;
> +    AVStream *st = NULL;
> +    uint16_t tag, hdr_size;
> +    uint32_t length;
> +    int srate_hz, frame_us, channels, bit_rate;
> +    int ep_mode, hr_mode;
> +    int num_extra_params;
> +    int delay, ret;
> +
> +    tag = avio_rb16(s->pb);
> +    hdr_size = avio_rl16(s->pb);
> +
> +    if (tag != 0x1ccc || hdr_size < 9 * sizeof(uint16_t))
> +        return AVERROR_INVALIDDATA;
> +
> +    num_extra_params = hdr_size / sizeof(uint16_t) - 9;
> +
> +    srate_hz = avio_rl16(s->pb) * 100;
> +    bit_rate = avio_rl16(s->pb) * 100;
> +    channels = avio_rl16(s->pb);
> +    frame_us = avio_rl16(s->pb) * 10;
> +    ep_mode  = avio_rl16(s->pb) != 0;
> +    length   = avio_rl32(s->pb);
> +    hr_mode  = num_extra_params >= 1 && avio_rl16(s->pb);
> +
> +    if (check_frame_length(srate_hz, frame_us) < 0) {

> +        av_log(s, AV_LOG_ERROR, "Invalid LC3 sample rate: %d Hz, "
> +                                "frame duration: %.1f ms.\n",
> +               srate_hz, frame_us / 1000.f);

you can move the log to the function, by passing a log context (this
way you can provide more context in the error message and specify what
failed between rate and duration) - or skip the log in case a NULL log
context is provided for the probe case

but I don't think this is really blocking

[...]

Looks good to me otherwise, thanks.
Antoine Soulier April 12, 2024, 11:46 p.m. UTC | #7
Thanks.

On Fri, Apr 12, 2024 at 6:05 AM Stefano Sabatini <stefasab@gmail.com> wrote:

> On date Wednesday 2024-04-10 16:46:55 -0700, ffmpeg-devel Mailing List
> wrote:
> > Sure, I thought these warnings were disabled while looking at codec2.c
> > Sorry for the bad merge of the doc.
>
> > From 975040408f32431efc3fae0a0b8c048f02159515 Mon Sep 17 00:00:00 2001
> > From: Antoine SOULIER <asoulier@google.com>
> > Date: Thu, 4 Apr 2024 22:38:03 +0000
> > Subject: [PATCH] avformat/lc3: Add file format for LC3/LC3plus transport
> >
> > A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
> > test purpose. This is the format implemented here.
> > ---
> >  Changelog                |   1 +
> >  doc/muxers.texi          |   6 +
> >  libavformat/Makefile     |   2 +
> >  libavformat/allformats.c |   2 +
> >  libavformat/lc3.c        | 253 +++++++++++++++++++++++++++++++++++++++
> >  5 files changed, 264 insertions(+)
> >  create mode 100644 libavformat/lc3.c
> >
> > diff --git a/Changelog b/Changelog
> > index b7a1af4083..5c8f505211 100644
> > --- a/Changelog
> > +++ b/Changelog
> > @@ -5,6 +5,7 @@ version <next>:
> >  - Raw Captions with Time (RCWT) closed caption demuxer
> >  - LC3/LC3plus decoding/encoding using external library liblc3
> >  - ffmpeg CLI filtergraph chaining
> > +- LC3/LC3plus demuxer and muxer
> >
> >
> >  version 7.0:
> > diff --git a/doc/muxers.texi b/doc/muxers.texi
> > index 4b30970b78..4c14323d50 100644
> > --- a/doc/muxers.texi
> > +++ b/doc/muxers.texi
> > @@ -2725,6 +2725,12 @@ games such as "Real War", and "Real War: Rogue
> States".
> >
> >  This muxer accepts a single @samp{adpcm_ima_ssi} audio stream.
> >
> > +@section lc3
> > +Bluetooth SIG Low Complexity Communication Codec audio (LC3), or
> > +ETSI TS 103 634 Low Complexity Communication Codec plus (LC3plus).
> > +
>
> > +This muxer accepts a single @code{lc3} audio stream.
>
> nit++: @samp{lc3} for consistency
>
> > +
> >  @section lrc
> >  LRC lyrics file format muxer.
> >
> > diff --git a/libavformat/Makefile b/libavformat/Makefile
> > index 9981799cc9..8efe26b6df 100644
> > --- a/libavformat/Makefile
> > +++ b/libavformat/Makefile
> > @@ -332,6 +332,8 @@ OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
> >  OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
> >  OBJS-$(CONFIG_LAF_DEMUXER)               += lafdec.o
> >  OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
> > +OBJS-$(CONFIG_LC3_DEMUXER)               += lc3.o
> > +OBJS-$(CONFIG_LC3_MUXER)                 += lc3.o
> >  OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
> >  OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
> >  OBJS-$(CONFIG_LUODAT_DEMUXER)            += luodatdec.o
> > diff --git a/libavformat/allformats.c b/libavformat/allformats.c
> > index ae925dcf60..305fa46532 100644
> > --- a/libavformat/allformats.c
> > +++ b/libavformat/allformats.c
> > @@ -252,6 +252,8 @@ extern const FFInputFormat  ff_kvag_demuxer;
> >  extern const FFOutputFormat ff_kvag_muxer;
> >  extern const FFInputFormat  ff_laf_demuxer;
> >  extern const FFOutputFormat ff_latm_muxer;
> > +extern const FFInputFormat  ff_lc3_demuxer;
> > +extern const FFOutputFormat ff_lc3_muxer;
> >  extern const FFInputFormat  ff_lmlm4_demuxer;
> >  extern const FFInputFormat  ff_loas_demuxer;
> >  extern const FFInputFormat  ff_luodat_demuxer;
> > diff --git a/libavformat/lc3.c b/libavformat/lc3.c
> > new file mode 100644
> > index 0000000000..e27727145b
> > --- /dev/null
> > +++ b/libavformat/lc3.c
> > @@ -0,0 +1,253 @@
> > +/*
> > + * LC3 demuxer
>
> nit: LC3 demuxer and muxer
>
> > + * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
> > + *
> > + * This file is part of FFmpeg.
> > + *
> > + * FFmpeg is free software; you can redistribute it and/or
> > + * modify it under the terms of the GNU Lesser General Public
> > + * License as published by the Free Software Foundation; either
> > + * version 2.1 of the License, or (at your option) any later version.
> > + *
> > + * FFmpeg 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
> > + * Lesser General Public License for more details.
> > + *
> > + * You should have received a copy of the GNU Lesser General Public
> > + * License along with FFmpeg; if not, write to the Free Software
> > + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> 02110-1301 USA
> > + */
> > +
> > +/**
> > + * @file
> > + * Based on the file format specified by :
> > + *
> > + * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
> > + *
> https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
> > + *   3.2.8.2 Reference LC3 Codec Bitstream Format
> > + *
> > + * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
> > + *
> https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
> > + *   LC3plus conformance script package
> > + */
> > +
> > +#include "config_components.h"
> > +
> > +#include "libavcodec/packet.h"
> > +#include "libavutil/intreadwrite.h"
> > +
> > +#include "avformat.h"
> > +#include "avio.h"
> > +#include "demux.h"
> > +#include "internal.h"
> > +#include "mux.h"
> > +
> > +static int check_frame_length(int srate_hz, int frame_us)
> > +{
> > +    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
> > +        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000)
> > +        return -1;
> > +
> > +    if (frame_us != 2500 && frame_us !=  5000 &&
> > +        frame_us != 7500 && frame_us != 10000)
> > +        return -1;
> > +
> > +    return 0;
> > +}
> > +
> > +#if CONFIG_LC3_DEMUXER
> > +
> > +typedef struct LC3DemuxContext {
> > +    int frame_samples;
> > +    int64_t end_dts;
> > +} LC3DemuxContext;
> > +
> > +static int lc3_read_probe(const AVProbeData *p)
> > +{
> > +    int frame_us, srate_hz;
> > +
> > +    if (p->buf_size < 12)
> > +        return 0;
> > +
> > +    if (AV_RB16(p->buf + 0) != 0x1ccc ||
> > +        AV_RL16(p->buf + 2) <  9 * sizeof(uint16_t))
> > +        return 0;
> > +
> > +    srate_hz = AV_RL16(p->buf + 4) * 100;
> > +    frame_us = AV_RL16(p->buf + 10) * 10;
> > +    if (check_frame_length(srate_hz, frame_us) < 0)
> > +        return 0;
> > +
> > +    return AVPROBE_SCORE_MAX;
> > +}
> > +
> > +static int lc3_read_header(AVFormatContext *s)
> > +{
> > +    LC3DemuxContext *lc3 = s->priv_data;
> > +    AVStream *st = NULL;
> > +    uint16_t tag, hdr_size;
> > +    uint32_t length;
> > +    int srate_hz, frame_us, channels, bit_rate;
> > +    int ep_mode, hr_mode;
> > +    int num_extra_params;
> > +    int delay, ret;
> > +
> > +    tag = avio_rb16(s->pb);
> > +    hdr_size = avio_rl16(s->pb);
> > +
> > +    if (tag != 0x1ccc || hdr_size < 9 * sizeof(uint16_t))
> > +        return AVERROR_INVALIDDATA;
> > +
> > +    num_extra_params = hdr_size / sizeof(uint16_t) - 9;
> > +
> > +    srate_hz = avio_rl16(s->pb) * 100;
> > +    bit_rate = avio_rl16(s->pb) * 100;
> > +    channels = avio_rl16(s->pb);
> > +    frame_us = avio_rl16(s->pb) * 10;
> > +    ep_mode  = avio_rl16(s->pb) != 0;
> > +    length   = avio_rl32(s->pb);
> > +    hr_mode  = num_extra_params >= 1 && avio_rl16(s->pb);
> > +
> > +    if (check_frame_length(srate_hz, frame_us) < 0) {
>
> > +        av_log(s, AV_LOG_ERROR, "Invalid LC3 sample rate: %d Hz, "
> > +                                "frame duration: %.1f ms.\n",
> > +               srate_hz, frame_us / 1000.f);
>
> you can move the log to the function, by passing a log context (this
> way you can provide more context in the error message and specify what
> failed between rate and duration) - or skip the log in case a NULL log
> context is provided for the probe case
>
> but I don't think this is really blocking
>
> [...]
>
> Looks good to me otherwise, thanks.
>
Stefano Sabatini April 13, 2024, 8:54 a.m. UTC | #8
On date Friday 2024-04-12 16:46:29 -0700, ffmpeg-devel Mailing List wrote:
> Thanks.
> 
> On Fri, Apr 12, 2024 at 6:05 AM Stefano Sabatini <stefasab@gmail.com> wrote:
[...]
> From 93c5628502a1f242043b39a18e83895d9067541e Mon Sep 17 00:00:00 2001
> From: Antoine SOULIER <asoulier@google.com>
> Date: Thu, 4 Apr 2024 22:38:03 +0000
> Subject: [PATCH] avformat/lc3: Add file format for LC3/LC3plus transport
> 
> A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
> test purpose. This is the format implemented here.
> ---
>  Changelog                |   1 +
>  doc/muxers.texi          |   6 +
>  libavformat/Makefile     |   2 +
>  libavformat/allformats.c |   2 +
>  libavformat/lc3.c        | 252 +++++++++++++++++++++++++++++++++++++++
>  5 files changed, 263 insertions(+)
>  create mode 100644 libavformat/lc3.c

Looks good to me.

Andreas, Paul, please comment. If I see no comments I'll push this in
two/three days.

Thanks.
Stefano Sabatini April 15, 2024, 4:40 p.m. UTC | #9
On date Saturday 2024-04-13 10:54:38 +0200, Stefano Sabatini wrote:
> On date Friday 2024-04-12 16:46:29 -0700, ffmpeg-devel Mailing List wrote:
> > Thanks.
> > 
> > On Fri, Apr 12, 2024 at 6:05 AM Stefano Sabatini <stefasab@gmail.com> wrote:
> [...]
> > From 93c5628502a1f242043b39a18e83895d9067541e Mon Sep 17 00:00:00 2001
> > From: Antoine SOULIER <asoulier@google.com>
> > Date: Thu, 4 Apr 2024 22:38:03 +0000
> > Subject: [PATCH] avformat/lc3: Add file format for LC3/LC3plus transport
> > 
> > A file format is described in Bluetooth SIG LC3 and ETSI TS 103 634, for
> > test purpose. This is the format implemented here.
> > ---
> >  Changelog                |   1 +
> >  doc/muxers.texi          |   6 +
> >  libavformat/Makefile     |   2 +
> >  libavformat/allformats.c |   2 +
> >  libavformat/lc3.c        | 252 +++++++++++++++++++++++++++++++++++++++
> >  5 files changed, 263 insertions(+)
> >  create mode 100644 libavformat/lc3.c
> 
> Looks good to me.
> 
> Andreas, Paul, please comment. If I see no comments I'll push this in
> two/three days.

Finally applied, thanks.
diff mbox series

Patch

diff --git a/Changelog b/Changelog
index 18e83b99a1..02ed7831ec 100644
--- a/Changelog
+++ b/Changelog
@@ -4,6 +4,7 @@  releases are sorted from youngest to oldest.
 version <next>:
 - Raw Captions with Time (RCWT) closed caption demuxer
 - LC3/LC3plus decoding/encoding using external library liblc3
+- LC3/LC3plus demuxer and muxer
 
 
 version 7.0:
diff --git a/doc/muxers.texi b/doc/muxers.texi
index d8a1f83309..ed4144f6d1 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -2700,6 +2700,12 @@  computer-generated compositions.
 
 This muxer accepts a single audio stream containing PCM data.
 
+@section lc3
+Bluetooth SIG Low Complexity Communication Codec audio (LC3), or
+ETSI TS 103 634 Low Complexity Communication Codec plus (LC3plus).
+
+This muxer accepts a single @code{lc3} audio stream.
+
 @section matroska
 
 Matroska container muxer.
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 9981799cc9..027d0cdae5 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -332,6 +332,8 @@  OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
 OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
 OBJS-$(CONFIG_LAF_DEMUXER)               += lafdec.o
 OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
+OBJS-$(CONFIG_LC3_DEMUXER)               += lc3dec.o
+OBJS-$(CONFIG_LC3_MUXER)                 += lc3enc.o
 OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
 OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
 OBJS-$(CONFIG_LUODAT_DEMUXER)            += luodatdec.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index ae925dcf60..305fa46532 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -252,6 +252,8 @@  extern const FFInputFormat  ff_kvag_demuxer;
 extern const FFOutputFormat ff_kvag_muxer;
 extern const FFInputFormat  ff_laf_demuxer;
 extern const FFOutputFormat ff_latm_muxer;
+extern const FFInputFormat  ff_lc3_demuxer;
+extern const FFOutputFormat ff_lc3_muxer;
 extern const FFInputFormat  ff_lmlm4_demuxer;
 extern const FFInputFormat  ff_loas_demuxer;
 extern const FFInputFormat  ff_luodat_demuxer;
diff --git a/libavformat/lc3dec.c b/libavformat/lc3dec.c
new file mode 100644
index 0000000000..371e9242d5
--- /dev/null
+++ b/libavformat/lc3dec.c
@@ -0,0 +1,173 @@ 
+/*
+ * LC3 demuxer
+ * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Based on the file format specified by :
+ *
+ * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
+ *   https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
+ *   3.2.8.2 Reference LC3 Codec Bitstream Format
+ *
+ * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
+ *   https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
+ *   LC3plus conformance script package
+ */
+
+#include "libavcodec/packet.h"
+#include "libavutil/intreadwrite.h"
+
+#include "avformat.h"
+#include "avio.h"
+#include "demux.h"
+#include "internal.h"
+
+typedef struct LC3DemuxContext {
+    int frame_samples;
+    int64_t end_dts;
+} LC3DemuxContext;
+
+static int lc3_read_probe(const AVProbeData *p)
+{
+    int frame_us, srate_hz;
+
+    if (p->buf_size < 12)
+        return 0;
+
+    if (AV_RB16(p->buf + 0) != 0x1ccc ||
+        AV_RL16(p->buf + 2) <  9 * sizeof(uint16_t))
+        return 0;
+
+    srate_hz = AV_RL16(p->buf + 4) * 100;
+    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
+        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000)
+        return 0;
+
+    frame_us = AV_RL16(p->buf + 10) * 10;
+    if (frame_us != 2500 && frame_us !=  5000 &&
+        frame_us != 7500 && frame_us != 10000)
+        return 0;
+
+    return AVPROBE_SCORE_MAX;
+}
+
+static int lc3_read_header(AVFormatContext *s)
+{
+    LC3DemuxContext *lc3 = s->priv_data;
+    AVStream *st = NULL;
+    uint16_t tag, hdr_size;
+    uint32_t length;
+    int srate_hz, frame_us, channels, bit_rate;
+    int ep_mode, hr_mode;
+    int num_extra_params;
+    int delay, ret;
+
+    tag = avio_rb16(s->pb);
+    hdr_size = avio_rl16(s->pb);
+
+    if (tag != 0x1ccc || hdr_size < 9 * sizeof(uint16_t))
+        return AVERROR_INVALIDDATA;
+
+    num_extra_params = hdr_size / sizeof(uint16_t) - 9;
+
+    srate_hz = avio_rl16(s->pb) * 100;
+    bit_rate = avio_rl16(s->pb) * 100;
+    channels = avio_rl16(s->pb);
+    frame_us = avio_rl16(s->pb) * 10;
+    ep_mode  = avio_rl16(s->pb) != 0;
+    length   = avio_rl32(s->pb);
+    hr_mode  = num_extra_params >= 1 && avio_rl16(s->pb);
+
+    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
+        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000) {
+        av_log(s, AV_LOG_ERROR, "Incompatible LC3 sample rate: %d Hz.\n",
+               srate_hz);
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (frame_us != 2500 && frame_us !=  5000 &&
+        frame_us != 7500 && frame_us != 10000) {
+        av_log(s, AV_LOG_ERROR, "Incompatible LC3 frame duration: %.1f ms.\n",
+               frame_us / 1000.f);
+        return AVERROR_INVALIDDATA;
+    }
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    avpriv_set_pts_info(st, 64, 1, srate_hz);
+    avpriv_update_cur_dts(s, st, 0);
+    st->duration = length;
+
+    st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
+    st->codecpar->codec_id = AV_CODEC_ID_LC3;
+    st->codecpar->sample_rate = srate_hz;
+    st->codecpar->bit_rate = bit_rate;
+    st->codecpar->ch_layout.nb_channels = channels;
+
+    if ((ret = ff_alloc_extradata(st->codecpar, 6)) < 0)
+        return ret;
+
+    AV_WL16(st->codecpar->extradata + 0, frame_us / 10);
+    AV_WL16(st->codecpar->extradata + 2, ep_mode);
+    AV_WL16(st->codecpar->extradata + 4, hr_mode);
+
+    lc3->frame_samples = av_rescale(frame_us, srate_hz, 1000*1000);
+
+    delay = av_rescale(frame_us == 7500 ? 4000 : 2500, srate_hz, 1000*1000);
+    lc3->end_dts = length ? length + delay : -1;
+
+    return 0;
+}
+
+static int lc3_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    LC3DemuxContext *lc3 = s->priv_data;
+    AVStream *st = s->streams[0];
+    AVIOContext *pb = s->pb;
+    int64_t pos = avio_tell(pb);
+    int64_t remaining_samples;
+    int ret;
+
+    ret = av_get_packet(s->pb, pkt, avio_rl16(pb));
+    if (ret < 0)
+        return ret;
+
+    pkt->pos = pos;
+
+    remaining_samples = lc3->end_dts < 0 ? lc3->frame_samples :
+                        FFMAX(lc3->end_dts - ffstream(st)->cur_dts, 0);
+    pkt->duration = FFMIN(lc3->frame_samples, remaining_samples);
+
+    return 0;
+}
+
+const FFInputFormat ff_lc3_demuxer = {
+    .p.name         = "lc3",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity Communication Codec)"),
+    .p.extensions   = "lc3",
+    .p.flags        = AVFMT_GENERIC_INDEX,
+    .priv_data_size = sizeof(LC3DemuxContext),
+    .read_probe     = lc3_read_probe,
+    .read_header    = lc3_read_header,
+    .read_packet    = lc3_read_packet,
+};
diff --git a/libavformat/lc3enc.c b/libavformat/lc3enc.c
new file mode 100644
index 0000000000..d3f823d6bb
--- /dev/null
+++ b/libavformat/lc3enc.c
@@ -0,0 +1,114 @@ 
+/*
+ * LC3 muxer
+ * Copyright (C) 2024  Antoine Soulier <asoulier@google.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Based on the file format specified by :
+ *
+ * - Bluetooth SIG - Low Complexity Communication Codec Test Suite
+ *   https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
+ *   3.2.8.2 Reference LC3 Codec Bitstream Format
+ *
+ * - ETSI TI 103 634 V1.4.1 - Low Complexity Communication Codec plus
+ *   https://www.etsi.org/deliver/etsi_ts/103600_103699/103634/01.04.01_60/ts_103634v010401p.pdf
+ *   LC3plus conformance script package
+ */
+
+#include "libavutil/intreadwrite.h"
+
+#include "avformat.h"
+#include "avio.h"
+#include "mux.h"
+#include "internal.h"
+
+static av_cold int lc3_init(AVFormatContext *s)
+{
+    if (s->nb_streams != 1) {
+        av_log(s, AV_LOG_ERROR, "This muxer only supports a single stream.\n");
+        return AVERROR(EINVAL);
+    }
+
+    return 0;
+}
+
+static int lc3_write_header(AVFormatContext *s)
+{
+    AVStream *st = s->streams[0];
+    int channels = st->codecpar->ch_layout.nb_channels;
+    int srate_hz = st->codecpar->sample_rate;
+    int bit_rate = st->codecpar->bit_rate;
+    int frame_us, ep_mode, hr_mode;
+    uint32_t nb_samples = av_rescale_q(
+        st->duration, st->time_base, (AVRational){ 1, srate_hz });
+
+    if (st->codecpar->extradata_size < 6)
+        return AVERROR_INVALIDDATA;
+
+    frame_us = AV_RL16(st->codecpar->extradata + 0) * 10;
+    ep_mode = AV_RL16(st->codecpar->extradata + 2) != 0;
+    hr_mode = AV_RL16(st->codecpar->extradata + 4) != 0;
+
+    if (srate_hz !=  8000 && srate_hz != 16000 && srate_hz != 24000 &&
+        srate_hz != 32000 && srate_hz != 48000 && srate_hz != 96000) {
+        av_log(s, AV_LOG_ERROR, "Incompatible LC3 sample rate: %d Hz.\n",
+               srate_hz);
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (frame_us != 2500 && frame_us !=  5000 &&
+        frame_us != 7500 && frame_us != 10000) {
+        av_log(s, AV_LOG_ERROR, "Incompatible LC3 frame duration: %.1f ms.\n",
+               frame_us / 1000.f);
+        return AVERROR_INVALIDDATA;
+    }
+
+    avio_wb16(s->pb, 0x1ccc);
+    avio_wl16(s->pb, (9 + hr_mode) * sizeof(uint16_t));
+    avio_wl16(s->pb, srate_hz / 100);
+    avio_wl16(s->pb, bit_rate / 100);
+    avio_wl16(s->pb, channels);
+    avio_wl16(s->pb, frame_us / 10);
+    avio_wl16(s->pb, ep_mode);
+    avio_wl32(s->pb, nb_samples);
+    if (hr_mode)
+        avio_wl16(s->pb, hr_mode);
+
+    return 0;
+}
+
+static int lc3_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    avio_wl16(s->pb, pkt->size);
+    avio_write(s->pb, pkt->data, pkt->size);
+    return 0;
+}
+
+const FFOutputFormat ff_lc3_muxer = {
+    .p.name        = "lc3",
+    .p.long_name   = NULL_IF_CONFIG_SMALL("LC3 (Low Complexity Communication Codec)"),
+    .p.extensions  = "lc3",
+    .p.audio_codec = AV_CODEC_ID_LC3,
+    .p.video_codec = AV_CODEC_ID_NONE,
+    .p.flags       = AVFMT_NOTIMESTAMPS,
+    .init          = lc3_init,
+    .write_header  = lc3_write_header,
+    .write_packet  = lc3_write_packet,
+};