volume: support arbitrarily-large positive gains

The current limit is `x10`, which allows just `+20 dB` of gain.

While it may seem sufficient, this came up as a problem
in a real-world, non-specially-engineered situation,
in strawberry's EBU R 128 loudness normalization.
(https://github.com/strawberrymusicplayer/strawberry/pull/1216)

There is an audio track (that was not intentionally engineered that way),
that has integrated loudness of `-38 LUFS`,
and if we want to normalize it's loudness to e.g. `-16 LUFS`,
which is a very reasonable thing to do,
we need to apply gain of `+22 dB`,
which is larger than `+20 dB`, and we fail...

I think it should allow at least `+96 dB` of gain,
and therefore should be at `10^(96/20) ~= 63096`.

But, i don't see why we need to put any specific restriction
on that parameter in the first place, other than the fact
that the fixed-point multiplication scheme does not support volume
larger than 15x-ish.

So let's just implement a floating-point fall-back path
that does not involve fixed-point multiplication
and lift the restriction altogether?

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/5063>
This commit is contained in:
Roman Lebedev 2023-07-19 03:11:52 +03:00 committed by GStreamer Marge Bot
parent b575f6c683
commit 8b1500d7ff
4 changed files with 1270 additions and 26 deletions

View file

@ -15709,7 +15709,7 @@
"construct-only": false, "construct-only": false,
"controllable": true, "controllable": true,
"default": "1", "default": "1",
"max": "10", "max": "1.79769e+308",
"min": "0", "min": "0",
"mutable": "null", "mutable": "null",
"readable": true, "readable": true,

View file

@ -58,8 +58,7 @@
#include "gstvolume.h" #include "gstvolume.h"
/* some defines for audio processing */ /* some defines for audio processing */
/* the volume factor is a range from 0.0 to (arbitrary) VOLUME_MAX_DOUBLE = 10.0 /* we map VOLUME_UNITY_INT* to volume = 1.0
* we map 1.0 to VOLUME_UNITY_INT*
*/ */
#define VOLUME_UNITY_INT8 8 /* internal int for unity 2^(8-5) */ #define VOLUME_UNITY_INT8 8 /* internal int for unity 2^(8-5) */
#define VOLUME_UNITY_INT8_BIT_SHIFT 3 /* number of bits to shift for unity */ #define VOLUME_UNITY_INT8_BIT_SHIFT 3 /* number of bits to shift for unity */
@ -69,7 +68,6 @@
#define VOLUME_UNITY_INT24_BIT_SHIFT 19 /* number of bits to shift for unity */ #define VOLUME_UNITY_INT24_BIT_SHIFT 19 /* number of bits to shift for unity */
#define VOLUME_UNITY_INT32 134217728 /* internal int for unity 2^(32-5) */ #define VOLUME_UNITY_INT32 134217728 /* internal int for unity 2^(32-5) */
#define VOLUME_UNITY_INT32_BIT_SHIFT 27 #define VOLUME_UNITY_INT32_BIT_SHIFT 27
#define VOLUME_MAX_DOUBLE 10.0
#define VOLUME_MAX_INT8 G_MAXINT8 #define VOLUME_MAX_INT8 G_MAXINT8
#define VOLUME_MIN_INT8 G_MININT8 #define VOLUME_MIN_INT8 G_MININT8
#define VOLUME_MAX_INT16 G_MAXINT16 #define VOLUME_MAX_INT16 G_MAXINT16
@ -78,6 +76,8 @@
#define VOLUME_MIN_INT24 -8388608 #define VOLUME_MIN_INT24 -8388608
#define VOLUME_MAX_INT32 G_MAXINT32 #define VOLUME_MAX_INT32 G_MAXINT32
#define VOLUME_MIN_INT32 G_MININT32 #define VOLUME_MIN_INT32 G_MININT32
#define VOLUME_MAX_INT64 G_MAXINT64
#define VOLUME_MIN_INT64 G_MININT64
#define GST_CAT_DEFAULT gst_volume_debug #define GST_CAT_DEFAULT gst_volume_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
@ -140,24 +140,32 @@ static void volume_process_int32 (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int32_clamp (GstVolume * self, gpointer bytes, static void volume_process_int32_clamp (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int32_via_double_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes);
static void volume_process_controlled_int32_clamp (GstVolume * self, static void volume_process_controlled_int32_clamp (GstVolume * self,
gpointer bytes, gdouble * volume, guint channels, guint n_bytes); gpointer bytes, gdouble * volume, guint channels, guint n_bytes);
static void volume_process_int24 (GstVolume * self, gpointer bytes, static void volume_process_int24 (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int24_clamp (GstVolume * self, gpointer bytes, static void volume_process_int24_clamp (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int24_via_float_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes);
static void volume_process_controlled_int24_clamp (GstVolume * self, static void volume_process_controlled_int24_clamp (GstVolume * self,
gpointer bytes, gdouble * volume, guint channels, guint n_bytes); gpointer bytes, gdouble * volume, guint channels, guint n_bytes);
static void volume_process_int16 (GstVolume * self, gpointer bytes, static void volume_process_int16 (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int16_clamp (GstVolume * self, gpointer bytes, static void volume_process_int16_clamp (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int16_via_float_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes);
static void volume_process_controlled_int16_clamp (GstVolume * self, static void volume_process_controlled_int16_clamp (GstVolume * self,
gpointer bytes, gdouble * volume, guint channels, guint n_bytes); gpointer bytes, gdouble * volume, guint channels, guint n_bytes);
static void volume_process_int8 (GstVolume * self, gpointer bytes, static void volume_process_int8 (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int8_clamp (GstVolume * self, gpointer bytes, static void volume_process_int8_clamp (GstVolume * self, gpointer bytes,
guint n_bytes); guint n_bytes);
static void volume_process_int8_via_float_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes);
static void volume_process_controlled_int8_clamp (GstVolume * self, static void volume_process_controlled_int8_clamp (GstVolume * self,
gpointer bytes, gdouble * volume, guint channels, guint n_bytes); gpointer bytes, gdouble * volume, guint channels, guint n_bytes);
@ -181,7 +189,12 @@ volume_choose_func (GstVolume * self, const GstAudioInfo * info)
case GST_AUDIO_FORMAT_S32: case GST_AUDIO_FORMAT_S32:
/* only clamp if the gain is greater than 1.0 */ /* only clamp if the gain is greater than 1.0 */
if (self->current_vol_i32 > VOLUME_UNITY_INT32) { if (self->current_vol_i32 > VOLUME_UNITY_INT32) {
/* Fixed-point multiplication only supports small subset of volumes */
if (self->current_vol_i32 > VOLUME_MAX_INT32) {
self->process = volume_process_int32_via_double_with_clamp;
} else {
self->process = volume_process_int32_clamp; self->process = volume_process_int32_clamp;
}
} else { } else {
self->process = volume_process_int32; self->process = volume_process_int32;
} }
@ -190,7 +203,12 @@ volume_choose_func (GstVolume * self, const GstAudioInfo * info)
case GST_AUDIO_FORMAT_S24: case GST_AUDIO_FORMAT_S24:
/* only clamp if the gain is greater than 1.0 */ /* only clamp if the gain is greater than 1.0 */
if (self->current_vol_i24 > VOLUME_UNITY_INT24) { if (self->current_vol_i24 > VOLUME_UNITY_INT24) {
/* Fixed-point multiplication only supports small subset of volumes */
if (self->current_vol_i24 > VOLUME_MAX_INT24) {
self->process = volume_process_int24_via_float_with_clamp;
} else {
self->process = volume_process_int24_clamp; self->process = volume_process_int24_clamp;
}
} else { } else {
self->process = volume_process_int24; self->process = volume_process_int24;
} }
@ -199,7 +217,12 @@ volume_choose_func (GstVolume * self, const GstAudioInfo * info)
case GST_AUDIO_FORMAT_S16: case GST_AUDIO_FORMAT_S16:
/* only clamp if the gain is greater than 1.0 */ /* only clamp if the gain is greater than 1.0 */
if (self->current_vol_i16 > VOLUME_UNITY_INT16) { if (self->current_vol_i16 > VOLUME_UNITY_INT16) {
/* Fixed-point multiplication only supports small subset of volumes */
if (self->current_vol_i16 > VOLUME_MAX_INT16) {
self->process = volume_process_int16_via_float_with_clamp;
} else {
self->process = volume_process_int16_clamp; self->process = volume_process_int16_clamp;
}
} else { } else {
self->process = volume_process_int16; self->process = volume_process_int16;
} }
@ -208,7 +231,12 @@ volume_choose_func (GstVolume * self, const GstAudioInfo * info)
case GST_AUDIO_FORMAT_S8: case GST_AUDIO_FORMAT_S8:
/* only clamp if the gain is greater than 1.0 */ /* only clamp if the gain is greater than 1.0 */
if (self->current_vol_i8 > VOLUME_UNITY_INT8) { if (self->current_vol_i8 > VOLUME_UNITY_INT8) {
/* Fixed-point multiplication only supports small subset of volumes */
if (self->current_vol_i8 > VOLUME_MAX_INT8) {
self->process = volume_process_int8_via_float_with_clamp;
} else {
self->process = volume_process_int8_clamp; self->process = volume_process_int8_clamp;
}
} else { } else {
self->process = volume_process_int8; self->process = volume_process_int8;
} }
@ -252,16 +280,49 @@ volume_update_volume (GstVolume * self, const GstAudioInfo * info,
self->current_mute = FALSE; self->current_mute = FALSE;
self->current_volume = volume; self->current_volume = volume;
self->current_vol_i8 = gdouble current_fp_vol_i8 = (gdouble) volume * (gdouble) VOLUME_UNITY_INT8;
(gint) ((gdouble) volume * (gdouble) VOLUME_UNITY_INT8); gdouble current_fp_vol_i16 =
self->current_vol_i16 = (gdouble) volume * (gdouble) VOLUME_UNITY_INT16;
(gint) ((gdouble) volume * (gdouble) VOLUME_UNITY_INT16); gdouble current_fp_vol_i24 =
self->current_vol_i24 = (gdouble) volume * (gdouble) VOLUME_UNITY_INT24;
(gint) ((gdouble) volume * (gdouble) VOLUME_UNITY_INT24); gdouble current_fp_vol_i32 =
self->current_vol_i32 = (gdouble) volume * (gdouble) VOLUME_UNITY_INT32;
(gint) ((gdouble) volume * (gdouble) VOLUME_UNITY_INT32);
/* Perform "saturating" FP->int conversion.
* We want to be able to tell when current_fp_vol_i32 is *larger* than
* VOLUME_MAX_INT32, *and* don't have UB on FP->int cast overflow,
* `2 * VOLUME_MAX_INT32` achieves both of these points.
*/
self->current_vol_i8 = (glong) (CLAMP (current_fp_vol_i8, (gdouble) 0,
(gdouble) 2 * VOLUME_MAX_INT32));
self->current_vol_i16 = (glong) (CLAMP (current_fp_vol_i16, (gdouble) 0,
(gdouble) 2 * VOLUME_MAX_INT32));
self->current_vol_i24 = (glong) (CLAMP (current_fp_vol_i24, (gdouble) 0,
(gdouble) 2 * VOLUME_MAX_INT32));
self->current_vol_i32 = (glong) (CLAMP (current_fp_vol_i32, (gdouble) 0,
(gdouble) 2 * VOLUME_MAX_INT32));
switch (GST_AUDIO_INFO_FORMAT (info)) {
case GST_AUDIO_FORMAT_F32:
case GST_AUDIO_FORMAT_F64:
passthrough = (self->current_volume == 1.0);
break;
case GST_AUDIO_FORMAT_S8:
passthrough = (self->current_vol_i8 == VOLUME_UNITY_INT8);
break;
case GST_AUDIO_FORMAT_S16:
passthrough = (self->current_vol_i16 == VOLUME_UNITY_INT16); passthrough = (self->current_vol_i16 == VOLUME_UNITY_INT16);
break;
case GST_AUDIO_FORMAT_S24:
passthrough = (self->current_vol_i24 == VOLUME_UNITY_INT24);
break;
case GST_AUDIO_FORMAT_S32:
passthrough = (self->current_vol_i32 == VOLUME_UNITY_INT32);
break;
default:
passthrough = FALSE;
break;
}
} }
/* If a controller is used, never use passthrough mode /* If a controller is used, never use passthrough mode
@ -319,9 +380,13 @@ gst_volume_class_init (GstVolumeClass * klass)
DEFAULT_PROP_MUTE, DEFAULT_PROP_MUTE,
G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS)); G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS));
/* The volume factor is a range from 0.0 to G_MAXDOUBLE.
* The choice of G_MAXDOUBLE is somewhat arbitrary,
* but it should be *very* inclusive, e.g. gain of +48 dB is very reasonable.
*/
g_object_class_install_property (gobject_class, PROP_VOLUME, g_object_class_install_property (gobject_class, PROP_VOLUME,
g_param_spec_double ("volume", "Volume", "volume factor, 1.0=100%", g_param_spec_double ("volume", "Volume", "volume factor, 1.0=100%",
0.0, VOLUME_MAX_DOUBLE, DEFAULT_PROP_VOLUME, 0.0, G_MAXDOUBLE, DEFAULT_PROP_VOLUME,
G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS)); G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS));
gst_element_class_set_static_metadata (element_class, "Volume", gst_element_class_set_static_metadata (element_class, "Volume",
@ -437,6 +502,26 @@ volume_process_int32_clamp (GstVolume * self, gpointer bytes, guint n_bytes)
volume_orc_process_int32_clamp (data, self->current_vol_i32, num_samples); volume_orc_process_int32_clamp (data, self->current_vol_i32, num_samples);
} }
// TODO: Add ORC implementation for this
static void
volume_process_int32_via_double_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes)
{
gint32 *data = (gint32 *) bytes;
guint i;
guint num_samples = n_bytes / sizeof (gint32);
/* Cast between the 32-bit integer and 32-bit floating point is lossy,
* but not between 64-bit floating point, so use it instead. */
/* WARNING: (gint32)((gfloat)VOLUME_MAX_INT32)) is UB! */
gdouble vol = self->current_volume;
gdouble val;
for (i = 0; i < num_samples; i++) {
val = *data * vol;
*data++ = (gint32) CLAMP (val, VOLUME_MIN_INT32, VOLUME_MAX_INT32);
}
}
static void static void
volume_process_controlled_int32_clamp (GstVolume * self, gpointer bytes, volume_process_controlled_int32_clamp (GstVolume * self, gpointer bytes,
gdouble * volume, guint channels, guint n_bytes) gdouble * volume, guint channels, guint n_bytes)
@ -460,6 +545,7 @@ volume_process_controlled_int32_clamp (GstVolume * self, gpointer bytes,
} }
#if (G_BYTE_ORDER == G_LITTLE_ENDIAN) #if (G_BYTE_ORDER == G_LITTLE_ENDIAN)
/* NOTE: does not sign-extend, result is an 24-bit unsigned integer! */
#define get_unaligned_i24(_x) ( (((guint8*)_x)[0]) | ((((guint8*)_x)[1]) << 8) | ((((gint8*)_x)[2]) << 16) ) #define get_unaligned_i24(_x) ( (((guint8*)_x)[0]) | ((((guint8*)_x)[1]) << 8) | ((((gint8*)_x)[2]) << 16) )
#define write_unaligned_u24(_x,samp) \ #define write_unaligned_u24(_x,samp) \
@ -470,6 +556,7 @@ G_STMT_START { \
} G_STMT_END } G_STMT_END
#else /* BIG ENDIAN */ #else /* BIG ENDIAN */
/* NOTE: does not sign-extend, result is an 24-bit unsigned integer! */
#define get_unaligned_i24(_x) ( (((guint8*)_x)[2]) | ((((guint8*)_x)[1]) << 8) | ((((gint8*)_x)[0]) << 16) ) #define get_unaligned_i24(_x) ( (((guint8*)_x)[2]) | ((((guint8*)_x)[1]) << 8) | ((((gint8*)_x)[0]) << 16) )
#define write_unaligned_u24(_x,samp) \ #define write_unaligned_u24(_x,samp) \
G_STMT_START { \ G_STMT_START { \
@ -479,6 +566,9 @@ G_STMT_START { \
} G_STMT_END } G_STMT_END
#endif #endif
#define sign_extend_i24(_x) \
(((gint32)(((guint32)(_x)) << (32 - 24))) >> (32 - 24))
static void static void
volume_process_int24 (GstVolume * self, gpointer bytes, guint n_bytes) volume_process_int24 (GstVolume * self, gpointer bytes, guint n_bytes)
{ {
@ -525,6 +615,30 @@ volume_process_int24_clamp (GstVolume * self, gpointer bytes, guint n_bytes)
} }
} }
static void
volume_process_int24_via_float_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes)
{
gint8 *data = (gint8 *) bytes; /* treat the data as a byte stream */
guint i, num_samples;
guint32 samp;
gfloat vol = self->current_volume;
gfloat val;
num_samples = n_bytes / (sizeof (gint8) * 3);
for (i = 0; i < num_samples; i++) {
samp = get_unaligned_i24 (data); /* NOT SIGN-EXTENDED!!! */
val = (gfloat) (sign_extend_i24 (samp));
val *= vol;
/* NOTE: we *MUST* first cast FP to int, and only then to unsigned. */
samp = (guint32) ((gint32) CLAMP (val, VOLUME_MIN_INT24, VOLUME_MAX_INT24));
/* write the value back into the stream */
write_unaligned_u24 (data, samp);
}
}
static void static void
volume_process_controlled_int24_clamp (GstVolume * self, gpointer bytes, volume_process_controlled_int24_clamp (GstVolume * self, gpointer bytes,
gdouble * volume, guint channels, guint n_bytes) gdouble * volume, guint channels, guint n_bytes)
@ -568,6 +682,23 @@ volume_process_int16_clamp (GstVolume * self, gpointer bytes, guint n_bytes)
volume_orc_process_int16_clamp (data, self->current_vol_i16, num_samples); volume_orc_process_int16_clamp (data, self->current_vol_i16, num_samples);
} }
// TODO: Add ORC implementation for this
static void
volume_process_int16_via_float_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes)
{
gint16 *data = (gint16 *) bytes;
guint i;
guint num_samples = n_bytes / sizeof (gint16);
gfloat vol = self->current_volume;
gfloat val;
for (i = 0; i < num_samples; i++) {
val = *data * vol;
*data++ = (gint16) CLAMP (val, VOLUME_MIN_INT16, VOLUME_MAX_INT16);
}
}
static void static void
volume_process_controlled_int16_clamp (GstVolume * self, gpointer bytes, volume_process_controlled_int16_clamp (GstVolume * self, gpointer bytes,
gdouble * volume, guint channels, guint n_bytes) gdouble * volume, guint channels, guint n_bytes)
@ -616,6 +747,24 @@ volume_process_int8_clamp (GstVolume * self, gpointer bytes, guint n_bytes)
volume_orc_process_int8_clamp (data, self->current_vol_i8, num_samples); volume_orc_process_int8_clamp (data, self->current_vol_i8, num_samples);
} }
// TODO: Add ORC implementation for this
static void
volume_process_int8_via_float_with_clamp (GstVolume * self,
gpointer bytes, guint n_bytes)
{
gint8 *data = (gint8 *) bytes;
guint i;
guint num_samples = n_bytes / sizeof (gint8);
gfloat vol = self->current_volume;
gfloat val;
for (i = 0; i < num_samples; i++) {
val = *data;
val *= vol;
*data++ = (gint8) CLAMP (val, VOLUME_MIN_INT8, VOLUME_MAX_INT8);
}
}
static void static void
volume_process_controlled_int8_clamp (GstVolume * self, gpointer bytes, volume_process_controlled_int8_clamp (GstVolume * self, gpointer bytes,
gdouble * volume, guint channels, guint n_bytes) gdouble * volume, guint channels, guint n_bytes)

View file

@ -52,10 +52,10 @@ struct _GstVolume {
gboolean current_mute; gboolean current_mute;
gdouble current_volume; gdouble current_volume;
gint current_vol_i32; gint64 current_vol_i32;
gint current_vol_i24; /* the _i(nt) values get synchronized with the */ gint64 current_vol_i24; /* the _i(nt) values get synchronized with the */
gint current_vol_i16; /* the _i(nt) values get synchronized with the */ gint64 current_vol_i16; /* the _i(nt) values get synchronized with the */
gint current_vol_i8; /* the _i(nt) values get synchronized with the */ gint64 current_vol_i8; /* the _i(nt) values get synchronized with the */
GList *tracklist; GList *tracklist;
gboolean negotiated; gboolean negotiated;

File diff suppressed because it is too large Load diff