[FFmpeg-devel] [PATCH 279/281] channel_layout: add support for Ambisonic
James Almer
jamrial at gmail.com
Thu Jan 13 04:09:11 EET 2022
From: Vittorio Giovara <vittorio.giovara at gmail.com>
Signed-off-by: James Almer <jamrial at gmail.com>
---
libavutil/channel_layout.c | 166 ++++++++++++++++++++++++++++++-
libavutil/channel_layout.h | 52 +++++++++-
libavutil/tests/channel_layout.c | 20 ++++
tests/ref/fate/channel_layout | 13 +++
4 files changed, 247 insertions(+), 4 deletions(-)
diff --git a/libavutil/channel_layout.c b/libavutil/channel_layout.c
index 68b40cc37c..3863e50e91 100644
--- a/libavutil/channel_layout.c
+++ b/libavutil/channel_layout.c
@@ -31,6 +31,9 @@
#include "bprint.h"
#include "common.h"
+#define CHAN_IS_AMBI(x) ((x) >= AV_CHAN_AMBISONIC_BASE &&\
+ (x) <= AV_CHAN_AMBISONIC_END)
+
struct channel_name {
const char *name;
const char *description;
@@ -81,7 +84,10 @@ void av_channel_name_bprint(AVBPrint *bp, enum AVChannel channel_id)
{
av_bprint_clear(bp);
- if ((unsigned)channel_id < FF_ARRAY_ELEMS(channel_names))
+ if (channel_id >= AV_CHAN_AMBISONIC_BASE &&
+ channel_id <= AV_CHAN_AMBISONIC_END)
+ av_bprintf(bp, "AMBI%d", channel_id - AV_CHAN_AMBISONIC_BASE);
+ else if ((unsigned)channel_id < FF_ARRAY_ELEMS(channel_names))
av_bprintf(bp, "%s", channel_names[channel_id].name);
else
av_bprintf(bp, "USR%d", channel_id);
@@ -104,7 +110,10 @@ void av_channel_description_bprint(AVBPrint *bp, enum AVChannel channel_id)
{
av_bprint_clear(bp);
- if ((unsigned)channel_id < FF_ARRAY_ELEMS(channel_names))
+ if (channel_id >= AV_CHAN_AMBISONIC_BASE &&
+ channel_id <= AV_CHAN_AMBISONIC_END)
+ av_bprintf(bp, "ambisonic ACN %d", channel_id - AV_CHAN_AMBISONIC_BASE);
+ else if ((unsigned)channel_id < FF_ARRAY_ELEMS(channel_names))
av_bprintf(bp, "%s", channel_names[channel_id].description);
else
av_bprintf(bp, "user %d", channel_id);
@@ -128,6 +137,14 @@ enum AVChannel av_channel_from_string(const char *str)
int i;
char *endptr = (char *)str;
enum AVChannel id = AV_CHAN_NONE;
+
+ if (!strncmp(str, "AMBI", 4)) {
+ i = strtol(str + 4, NULL, 0);
+ if (i < 0 || i > AV_CHAN_AMBISONIC_END - AV_CHAN_AMBISONIC_BASE)
+ return AV_CHAN_NONE;
+ return AV_CHAN_AMBISONIC_BASE + i;
+ }
+
for (i = 0; i < FF_ARRAY_ELEMS(channel_names); i++) {
if (channel_names[i].name && !strcmp(str, channel_names[i].name))
return i;
@@ -395,6 +412,60 @@ int av_channel_layout_from_string(AVChannelLayout *channel_layout,
}
}
+ /* ambisonic */
+ if (!strncmp(str, "ambisonic ", 10)) {
+ const char *p = str + 10;
+ char *endptr;
+ AVChannelLayout extra = {0};
+ int order;
+
+ order = strtol(p, &endptr, 0);
+ if (order < 0 || order + 1 > INT_MAX / (order + 1) ||
+ (*endptr && *endptr != '+'))
+ return AVERROR(EINVAL);
+
+ channel_layout->order = AV_CHANNEL_ORDER_AMBISONIC;
+ channel_layout->nb_channels = (order + 1) * (order + 1);
+
+ if (*endptr) {
+ int ret = av_channel_layout_from_string(&extra, endptr + 1);
+ if (ret < 0)
+ return ret;
+ if (extra.nb_channels >= INT_MAX - channel_layout->nb_channels) {
+ av_channel_layout_uninit(&extra);
+ return AVERROR(EINVAL);
+ }
+
+ if (extra.order == AV_CHANNEL_ORDER_NATIVE) {
+ channel_layout->u.mask = extra.u.mask;
+ } else {
+ channel_layout->order = AV_CHANNEL_ORDER_CUSTOM;
+ channel_layout->u.map =
+ av_calloc(channel_layout->nb_channels + extra.nb_channels,
+ sizeof(*channel_layout->u.map));
+ if (!channel_layout->u.map) {
+ av_channel_layout_uninit(&extra);
+ return AVERROR(ENOMEM);
+ }
+
+ for (i = 0; i < channel_layout->nb_channels; i++)
+ channel_layout->u.map[i].id = AV_CHAN_AMBISONIC_BASE + i;
+ for (i = 0; i < extra.nb_channels; i++) {
+ enum AVChannel ch = av_channel_layout_channel_from_index(&extra, i);
+ if (CHAN_IS_AMBI(ch)) {
+ av_channel_layout_uninit(&extra);
+ return AVERROR(EINVAL);
+ }
+ channel_layout->u.map[channel_layout->nb_channels + i].id = ch;
+ }
+ }
+ channel_layout->nb_channels += extra.nb_channels;
+ av_channel_layout_uninit(&extra);
+ }
+
+ return 0;
+ }
+
/* channel names */
while (*dup) {
char *chname = av_get_token(&dup, "+");
@@ -526,6 +597,77 @@ int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src)
return 0;
}
+/**
+ * If the custom layout is n-th order standard-order ambisonic, with optional
+ * extra non-diegetic channels at the end, write its string description in bp.
+ * Return a negative error code on error.
+ */
+static int try_describe_ambisonic(AVBPrint *bp, const AVChannelLayout *channel_layout)
+{
+ int i, highest_ambi, order;
+
+ highest_ambi = -1;
+ if (channel_layout->order == AV_CHANNEL_ORDER_AMBISONIC)
+ highest_ambi = channel_layout->nb_channels - av_popcount64(channel_layout->u.mask) - 1;
+ else {
+ const AVChannelCustom *map = channel_layout->u.map;
+ for (i = 0; i < channel_layout->nb_channels; i++) {
+ int is_ambi = CHAN_IS_AMBI(map[i].id);
+
+ /* ambisonic following non-ambisonic */
+ if (i > 0 && is_ambi && !CHAN_IS_AMBI(map[i - 1].id))
+ return 0;
+
+ /* non-default ordering */
+ if (is_ambi && map[i].id - AV_CHAN_AMBISONIC_BASE != i)
+ return 0;
+
+ if (CHAN_IS_AMBI(map[i].id))
+ highest_ambi = i;
+ }
+ }
+ /* no ambisonic channels*/
+ if (highest_ambi < 0)
+ return 0;
+
+ order = floor(sqrt(highest_ambi));
+ /* incomplete order - some harmonics are missing */
+ if ((order + 1) * (order + 1) != highest_ambi + 1)
+ return 0;
+
+ av_bprintf(bp, "ambisonic %d", order);
+
+ /* extra channels present */
+ if (highest_ambi < channel_layout->nb_channels - 1) {
+ AVChannelLayout extra = { 0 };
+ char buf[128];
+
+ if (channel_layout->order == AV_CHANNEL_ORDER_AMBISONIC) {
+ extra.order = AV_CHANNEL_ORDER_NATIVE;
+ extra.nb_channels = av_popcount64(channel_layout->u.mask);
+ extra.u.mask = channel_layout->u.mask;
+ } else {
+ const AVChannelCustom *map = channel_layout->u.map;
+
+ extra.order = AV_CHANNEL_ORDER_CUSTOM;
+ extra.nb_channels = channel_layout->nb_channels - highest_ambi - 1;
+ extra.u.map = av_calloc(extra.nb_channels, sizeof(*extra.u.map));
+ if (!extra.u.map)
+ return AVERROR(ENOMEM);
+
+ memcpy(extra.u.map, &map[highest_ambi + 1],
+ sizeof(*extra.u.map) * extra.nb_channels);
+ }
+
+ av_channel_layout_describe(&extra, buf, sizeof(buf));
+ av_channel_layout_uninit(&extra);
+
+ av_bprintf(bp, "+%s", buf);
+ }
+
+ return 0;
+}
+
int av_channel_layout_describe_bprint(const AVChannelLayout *channel_layout,
AVBPrint *bp)
{
@@ -542,6 +684,11 @@ int av_channel_layout_describe_bprint(const AVChannelLayout *channel_layout,
}
// fall-through
case AV_CHANNEL_ORDER_CUSTOM:
+ if (channel_layout->order == AV_CHANNEL_ORDER_CUSTOM) {
+ int res = try_describe_ambisonic(bp, channel_layout);
+ if (res < 0 || bp->len)
+ return res;
+ }
for (i = 0; i < channel_layout->nb_channels; i++) {
const char *ch_name = NULL;
enum AVChannel ch = AV_CHAN_NONE;
@@ -567,6 +714,8 @@ int av_channel_layout_describe_bprint(const AVChannelLayout *channel_layout,
case AV_CHANNEL_ORDER_UNSPEC:
av_bprintf(bp, "%d channels", channel_layout->nb_channels);
return 0;
+ case AV_CHANNEL_ORDER_AMBISONIC:
+ return try_describe_ambisonic(bp, channel_layout);
default:
return AVERROR(EINVAL);
}
@@ -601,6 +750,8 @@ av_channel_layout_channel_from_index(const AVChannelLayout *channel_layout,
switch (channel_layout->order) {
case AV_CHANNEL_ORDER_CUSTOM:
return channel_layout->u.map[idx].id;
+ case AV_CHANNEL_ORDER_AMBISONIC:
+ return AV_CHAN_AMBISONIC_BASE + idx;
case AV_CHANNEL_ORDER_NATIVE:
for (i = 0; i < 64; i++) {
if ((1ULL << i) & channel_layout->u.mask && !idx--)
@@ -651,6 +802,11 @@ int av_channel_layout_index_from_channel(const AVChannelLayout *channel_layout,
if (channel_layout->u.map[i].id == channel)
return i;
return AVERROR(EINVAL);
+ case AV_CHANNEL_ORDER_AMBISONIC:
+ if (!CHAN_IS_AMBI(channel) ||
+ channel - AV_CHAN_AMBISONIC_BASE >= channel_layout->nb_channels)
+ return AVERROR(EINVAL);
+ return channel - AV_CHAN_AMBISONIC_BASE;
case AV_CHANNEL_ORDER_NATIVE: {
uint64_t mask = channel_layout->u.mask;
if (!(mask & (1ULL << channel)))
@@ -701,6 +857,9 @@ int av_channel_layout_check(const AVChannelLayout *channel_layout)
return 0;
}
return 1;
+ case AV_CHANNEL_ORDER_AMBISONIC:
+ /* If non-diegetic channels are present, ensure they are taken into account */
+ return av_popcount64(channel_layout->u.mask) < channel_layout->nb_channels;
case AV_CHANNEL_ORDER_UNSPEC:
return 1;
default:
@@ -725,7 +884,8 @@ int av_channel_layout_compare(const AVChannelLayout *chl, const AVChannelLayout
return 0;
/* can compare masks directly */
- if (chl->order != AV_CHANNEL_ORDER_CUSTOM &&
+ if ((chl->order == AV_CHANNEL_ORDER_NATIVE ||
+ chl->order == AV_CHANNEL_ORDER_AMBISONIC) &&
chl->order == chl1->order)
return chl->u.mask != chl1->u.mask;
diff --git a/libavutil/channel_layout.h b/libavutil/channel_layout.h
index 6356a9a38a..b7e709a022 100644
--- a/libavutil/channel_layout.h
+++ b/libavutil/channel_layout.h
@@ -79,6 +79,23 @@ enum AVChannel {
/** Channel contains data, but its position is unknown. */
AV_CHAN_UNKWNOWN = 128,
+
+ /**
+ * Range of channels between AV_CHAN_AMBISONIC_BASE and
+ * AV_CHAN_AMBISONIC_END represent Ambisonic components using the ACN system.
+ *
+ * Given a channel id <i> between AV_CHAN_AMBISONIC_BASE and
+ * AV_CHAN_AMBISONIC_END (inclusive), the ACN index of the channel <n> is
+ * <n> = <i> - AV_CHAN_AMBISONIC_BASE.
+ *
+ * @note these values are only used for AV_CHANNEL_ORDER_CUSTOM channel
+ * orderings, the AV_CHANNEL_ORDER_AMBISONIC ordering orders the channels
+ * implicitly by their position in the stream.
+ */
+ AV_CHAN_AMBISONIC_BASE = 0x400,
+ // leave space for 1024 ids, which correspond to maximum order-32 harmonics,
+ // which should be enough for the foreseeable use cases
+ AV_CHAN_AMBISONIC_END = 0x7ff,
};
enum AVChannelOrder {
@@ -100,6 +117,29 @@ enum AVChannelOrder {
* channels at arbitrary positions.
*/
AV_CHANNEL_ORDER_CUSTOM,
+ /**
+ * The audio is represented as the decomposition of the sound field into
+ * spherical harmonics. Each channel corresponds to a single expansion
+ * component. Channels are ordered according to ACN (Ambisonic Channel
+ * Number).
+ *
+ * The channel with the index n in the stream contains the spherical
+ * harmonic of degree l and order m given by
+ * @code{.unparsed}
+ * l = floor(sqrt(n)),
+ * m = n - l * (l + 1).
+ * @endcode
+ *
+ * Conversely given a spherical harmonic of degree l and order m, the
+ * corresponding channel index n is given by
+ * @code{.unparsed}
+ * n = l * (l + 1) + m.
+ * @endcode
+ *
+ * Normalization is assumed to be SN3D (Schmidt Semi-Normalization)
+ * as defined in AmbiX format $ 2.1.
+ */
+ AV_CHANNEL_ORDER_AMBISONIC,
};
@@ -266,7 +306,8 @@ typedef struct AVChannelLayout {
*/
union {
/**
- * This member must be used for AV_CHANNEL_ORDER_NATIVE.
+ * This member must be used for AV_CHANNEL_ORDER_NATIVE, and may be used
+ * for AV_CHANNEL_ORDER_AMBISONIC to signal non-diegetic channels.
* It is a bitmask, where the position of each set bit means that the
* AVChannel with the corresponding value is present.
*
@@ -288,6 +329,11 @@ typedef struct AVChannelLayout {
* I.e. when map[i].id is equal to AV_CHAN_FOO, then AV_CH_FOO is the
* i-th channel in the audio data.
*
+ * When map[i].id is in the range between AV_CHAN_AMBISONIC_BASE and
+ * AV_CHAN_AMBISONIC_END (inclusive), the channel contains an ambisonic
+ * component with ACN index (as defined above)
+ * n = map[i].id - AV_CHAN_AMBISONIC_BASE.
+ *
* map[i].name may be filled with a 0-terminated string, in which case
* it will be used for the purpose of identifying the channel with the
* convenience functions below. Otherise it must be zeroed.
@@ -359,6 +405,8 @@ typedef struct AVChannelLayout {
{ .order = AV_CHANNEL_ORDER_NATIVE, .nb_channels = 2, .u = { .mask = AV_CH_LAYOUT_STEREO_DOWNMIX }}
#define AV_CHANNEL_LAYOUT_22POINT2 \
{ .order = AV_CHANNEL_ORDER_NATIVE, .nb_channels = 24, .u = { .mask = AV_CH_LAYOUT_22POINT2 }}
+#define AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER \
+ { .order = AV_CHANNEL_ORDER_AMBISONIC, .nb_channels = 4, .u = { .mask = 0 }}
struct AVBPrint;
@@ -554,6 +602,8 @@ int av_channel_layout_from_mask(AVChannelLayout *channel_layout, uint64_t mask);
* - a hexadecimal value of a channel layout (eg. "0x4")
* - the number of channels with default layout (eg. "5c")
* - the number of unordered channels (eg. "4", "4C", or "4 channels")
+ * - the ambisonic order followed by optional non-diegetic channels (eg.
+ * "ambisonic 2+stereo")
*
* @param channel_layout input channel layout
* @param str string describing the channel layout
diff --git a/libavutil/tests/channel_layout.c b/libavutil/tests/channel_layout.c
index 7e6be0be17..b3318aed27 100644
--- a/libavutil/tests/channel_layout.c
+++ b/libavutil/tests/channel_layout.c
@@ -66,6 +66,10 @@ int main(void)
printf("With AV_CHAN_FRONT_RIGHT: %26s\n", bp.str);
av_channel_name_bprint(&bp, 63);
printf("With 63: %43s\n", bp.str);
+ av_channel_name_bprint(&bp, AV_CHAN_AMBISONIC_BASE);
+ printf("With AV_CHAN_AMBISONIC_BASE: %23s\n", bp.str);
+ av_channel_name_bprint(&bp, AV_CHAN_AMBISONIC_END);
+ printf("With AV_CHAN_AMBISONIC_END: %24s\n", bp.str);
printf("Testing av_channel_description\n");
av_channel_description_bprint(&bp, AV_CHAN_FRONT_LEFT);
@@ -74,11 +78,17 @@ int main(void)
printf("With AV_CHAN_FRONT_RIGHT: %26s\n", bp.str);
av_channel_description_bprint(&bp, 63);
printf("With 63: %43s\n", bp.str);
+ av_channel_description_bprint(&bp, AV_CHAN_AMBISONIC_BASE);
+ printf("With AV_CHAN_AMBISONIC_BASE: %23s\n", bp.str);
+ av_channel_description_bprint(&bp, AV_CHAN_AMBISONIC_END);
+ printf("With AV_CHAN_AMBISONIC_END: %24s\n", bp.str);
printf("\nTesting av_channel_from_string\n");
printf("With \"FL\": %41d\n", av_channel_from_string("FL"));
printf("With \"FR\": %41d\n", av_channel_from_string("FR"));
printf("With \"USR63\": %38d\n", av_channel_from_string("USR63"));
+ printf("With \"AMBI0\": %38d\n", av_channel_from_string("AMBI0"));
+ printf("With \"AMBI1023\": %35d\n", av_channel_from_string("AMBI1023"));
printf("\n==Native layouts==\n");
@@ -177,6 +187,8 @@ int main(void)
printf("\nTesting av_channel_layout_from_string\n");
CHANNEL_LAYOUT_FROM_STRING(custom, "FL+FR+FC+BL+BR+LFE");
printf("With \"FL+FR+FC+BL+BR+LFE\": %25s\n", bp.str);
+ CHANNEL_LAYOUT_FROM_STRING(custom, "ambisonic 1+FR+FL");
+ printf("With \"ambisonic 1+FR+FL\": %26s\n", bp.str);
CHANNEL_LAYOUT_FROM_STRING(custom, "FR+FL+USR63");
printf("With \"FR+FL+USR63\" layout: %25s\n", bp.str);
@@ -225,6 +237,14 @@ int main(void)
CHANNEL_LAYOUT_CHANNEL_FROM_INDEX(custom, 3);
printf("On \"FR+FL+USR63\" layout with 3: %20d\n", ret);
+ printf("\n==Ambisonic layouts==\n");
+
+ printf("\nTesting av_channel_layout_from_string\n");
+ CHANNEL_LAYOUT_FROM_STRING(custom, "ambisonic 1");
+ printf("With \"ambisonic 1\": %32s\n", bp.str);
+ CHANNEL_LAYOUT_FROM_STRING(custom, "ambisonic 2+stereo");
+ printf("With \"ambisonic 2+stereo\": %25s\n", bp.str);
+
av_channel_layout_uninit(&surround);
av_channel_layout_uninit(&custom);
av_bprint_finalize(&bp, NULL);
diff --git a/tests/ref/fate/channel_layout b/tests/ref/fate/channel_layout
index bac00086d6..4614934028 100644
--- a/tests/ref/fate/channel_layout
+++ b/tests/ref/fate/channel_layout
@@ -2,15 +2,21 @@ Testing av_channel_name
With AV_CHAN_FRONT_LEFT: FL
With AV_CHAN_FRONT_RIGHT: FR
With 63: USR63
+With AV_CHAN_AMBISONIC_BASE: AMBI0
+With AV_CHAN_AMBISONIC_END: AMBI1023
Testing av_channel_description
With AV_CHAN_FRONT_LEFT: front left
With AV_CHAN_FRONT_RIGHT: front right
With 63: user 63
+With AV_CHAN_AMBISONIC_BASE: ambisonic ACN 0
+With AV_CHAN_AMBISONIC_END: ambisonic ACN 1023
Testing av_channel_from_string
With "FL": 0
With "FR": 1
With "USR63": 63
+With "AMBI0": 1024
+With "AMBI1023": 2047
==Native layouts==
@@ -69,6 +75,7 @@ On 5.1(side) layout with "BC": -1
Testing av_channel_layout_from_string
With "FL+FR+FC+BL+BR+LFE": FL+FR+FC+BL+BR+LFE
+With "ambisonic 1+FR+FL": ambisonic 1+FR+FL
With "FR+FL+USR63" layout: FR+FL+USR63
Testing av_channel_layout_index_from_string
@@ -96,3 +103,9 @@ On "FR+FL+USR63" layout with 0: 1
On "FR+FL+USR63" layout with 1: 0
On "FR+FL+USR63" layout with 2: 63
On "FR+FL+USR63" layout with 3: -1
+
+==Ambisonic layouts==
+
+Testing av_channel_layout_from_string
+With "ambisonic 1": ambisonic 1
+With "ambisonic 2+stereo": ambisonic 2+stereo
--
2.34.1
More information about the ffmpeg-devel
mailing list