audio: A whole bunch of improvements to audio conversion (thanks, Solra!).

"Major changes, roughly in order of appearance:

- Use float math everywhere, instead of promoting to double and casting back
all the time.
- Conserve sound energy when downmixing any channel into two other channels.
- Add a QuadToStereo filter. (The previous technique of reusing StereoToMono
never worked, since it assumed an incorrect channel layout for 4.0.)
- Add a 71to51 filter. This removes just under half of the cases the previous
code would silently break in.
- Add a QuadTo51 filter. More silent breakage fixed.
- Add a 51to71 filter, removing another almost-half of the silently broken
cases.
- Add 8 to the list of values SDL_SupportedChannelCount will accept.
- Change SDL_BuildAudioCVT's channel-related logic to handle every case, and
to actually fail if it fails instead of silently corrupting sound data and/or
crashing down the road."

(Note that SDL doesn't otherwise support 7.1 audio yet, but hopefully it will
soon and the 7.1 converters are an important piece of that.  --ryan.)

Fixes Bugzilla #3727.
This commit is contained in:
Ryan C. Gordon 2017-08-29 00:41:45 -04:00
parent 620f5342b5
commit b128e8802d
1 changed files with 213 additions and 40 deletions

View File

@ -36,7 +36,7 @@
#endif #endif
#if HAVE_SSE3_INTRINSICS #if HAVE_SSE3_INTRINSICS
/* Effectively mix right and left channels into a single channel */ /* Convert from stereo to mono. Average left and right. */
static void SDLCALL static void SDLCALL
SDL_ConvertStereoToMono_SSE3(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_ConvertStereoToMono_SSE3(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -71,7 +71,7 @@ SDL_ConvertStereoToMono_SSE3(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
#endif #endif
/* Effectively mix right and left channels into a single channel */ /* Convert from stereo to mono. Average left and right. */
static void SDLCALL static void SDLCALL
SDL_ConvertStereoToMono(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_ConvertStereoToMono(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -93,7 +93,7 @@ SDL_ConvertStereoToMono(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
/* Convert from 5.1 to stereo. Average left and right, discard subwoofer. */ /* Convert from 5.1 to stereo. Average left and right, distribute center, discard LFE. */
static void SDLCALL static void SDLCALL
SDL_Convert51ToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_Convert51ToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -104,11 +104,11 @@ SDL_Convert51ToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format)
LOG_DEBUG_CONVERT("5.1", "stereo"); LOG_DEBUG_CONVERT("5.1", "stereo");
SDL_assert(format == AUDIO_F32SYS); SDL_assert(format == AUDIO_F32SYS);
/* this assumes FL+FR+FC+subwoof+BL+BR layout. */ /* SDL's 5.1 layout: FL+FR+FC+LFE+BL+BR */
for (i = cvt->len_cvt / (sizeof (float) * 6); i; --i, src += 6, dst += 2) { for (i = cvt->len_cvt / (sizeof (float) * 6); i; --i, src += 6, dst += 2) {
const double front_center = (double) src[2]; const float front_center_distributed = src[2] * 0.5f;
dst[0] = (float) ((src[0] + front_center + src[4]) / 3.0); /* left */ dst[0] = (src[0] + front_center_distributed + src[4]) / 2.5f; /* left */
dst[1] = (float) ((src[1] + front_center + src[5]) / 3.0); /* right */ dst[1] = (src[1] + front_center_distributed + src[5]) / 2.5f; /* right */
} }
cvt->len_cvt /= 3; cvt->len_cvt /= 3;
@ -118,7 +118,60 @@ SDL_Convert51ToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
/* Convert from 5.1 to quad */ /* Convert from quad to stereo. Average left and right. */
static void SDLCALL
SDL_ConvertQuadToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
float *dst = (float *) cvt->buf;
const float *src = dst;
int i;
LOG_DEBUG_CONVERT("quad", "stereo");
SDL_assert(format == AUDIO_F32SYS);
for (i = cvt->len_cvt / (sizeof (float) * 4); i; --i, src += 4, dst += 2) {
dst[0] = (src[0] + src[2]) * 0.5f; /* left */
dst[1] = (src[1] + src[3]) * 0.5f; /* right */
}
cvt->len_cvt /= 3;
if (cvt->filters[++cvt->filter_index]) {
cvt->filters[cvt->filter_index] (cvt, format);
}
}
/* Convert from 7.1 to 5.1. Distribute sides across front and back. */
static void SDLCALL
SDL_Convert71To51(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
float *dst = (float *) cvt->buf;
const float *src = dst;
int i;
LOG_DEBUG_CONVERT("7.1", "5.1");
SDL_assert(format == AUDIO_F32SYS);
for (i = cvt->len_cvt / (sizeof (float) * 8); i; --i, src += 8, dst += 6) {
const float surround_left_distributed = src[6] * 0.5f;
const float surround_right_distributed = src[7] * 0.5f;
dst[0] = (src[0] + surround_left_distributed) / 1.5f; /* FL */
dst[1] = (src[1] + surround_right_distributed) / 1.5f; /* FR */
dst[2] = src[2] / 1.5f; /* CC */
dst[3] = src[3] / 1.5f; /* LFE */
dst[4] = (src[4] + surround_left_distributed) / 1.5f; /* BL */
dst[5] = (src[5] + surround_right_distributed) / 1.5f; /* BR */
}
cvt->len_cvt /= 8;
cvt->len_cvt *= 6;
if (cvt->filters[++cvt->filter_index]) {
cvt->filters[cvt->filter_index] (cvt, format);
}
}
/* Convert from 5.1 to quad. Distribute center across front, discard LFE. */
static void SDLCALL static void SDLCALL
SDL_Convert51ToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_Convert51ToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -129,14 +182,14 @@ SDL_Convert51ToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format)
LOG_DEBUG_CONVERT("5.1", "quad"); LOG_DEBUG_CONVERT("5.1", "quad");
SDL_assert(format == AUDIO_F32SYS); SDL_assert(format == AUDIO_F32SYS);
/* assumes quad is FL+FR+BL+BR layout and 5.1 is FL+FR+FC+subwoof+BL+BR */ /* SDL's 4.0 layout: FL+FR+BL+BR */
/* SDL's 5.1 layout: FL+FR+FC+LFE+BL+BR */
for (i = cvt->len_cvt / (sizeof (float) * 6); i; --i, src += 6, dst += 4) { for (i = cvt->len_cvt / (sizeof (float) * 6); i; --i, src += 6, dst += 4) {
/* FIXME: this is a good candidate for SIMD. */ const float front_center_distributed = src[2] * 0.5f;
const double front_center = (double) src[2]; dst[0] = (src[0] + front_center_distributed) / 1.5f; /* FL */
dst[0] = (float) ((src[0] + front_center) * 0.5); /* FL */ dst[1] = (src[1] + front_center_distributed) / 1.5f; /* FR */
dst[1] = (float) ((src[1] + front_center) * 0.5); /* FR */ dst[2] = src[4] / 1.5f; /* BL */
dst[2] = (float) ((src[4] + front_center) * 0.5); /* BL */ dst[3] = src[5] / 1.5f; /* BR */
dst[3] = (float) ((src[5] + front_center) * 0.5); /* BR */
} }
cvt->len_cvt /= 6; cvt->len_cvt /= 6;
@ -147,7 +200,7 @@ SDL_Convert51ToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
/* Duplicate a mono channel to both stereo channels */ /* Upmix mono to stereo (by duplication) */
static void SDLCALL static void SDLCALL
SDL_ConvertMonoToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_ConvertMonoToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -171,7 +224,7 @@ SDL_ConvertMonoToStereo(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
/* Duplicate a stereo channel to a pseudo-5.1 stream */ /* Upmix stereo to a pseudo-5.1 stream */
static void SDLCALL static void SDLCALL
SDL_ConvertStereoTo51(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_ConvertStereoTo51(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -183,16 +236,17 @@ SDL_ConvertStereoTo51(SDL_AudioCVT * cvt, SDL_AudioFormat format)
LOG_DEBUG_CONVERT("stereo", "5.1"); LOG_DEBUG_CONVERT("stereo", "5.1");
SDL_assert(format == AUDIO_F32SYS); SDL_assert(format == AUDIO_F32SYS);
for (i = cvt->len_cvt / 8; i; --i) { for (i = cvt->len_cvt / (sizeof(float) * 2); i; --i) {
dst -= 6; dst -= 6;
src -= 2; src -= 2;
lf = src[0]; lf = src[0];
rf = src[1]; rf = src[1];
ce = (lf + rf) * 0.5f; ce = (lf + rf) * 0.5f;
/* !!! FIXME: FL and FR may clip */
dst[0] = lf + (lf - ce); /* FL */ dst[0] = lf + (lf - ce); /* FL */
dst[1] = rf + (rf - ce); /* FR */ dst[1] = rf + (rf - ce); /* FR */
dst[2] = ce; /* FC */ dst[2] = ce; /* FC */
dst[3] = ce; /* !!! FIXME: wrong! This is the subwoofer. */ dst[3] = 0; /* LFE (only meant for special LFE effects) */
dst[4] = lf; /* BL */ dst[4] = lf; /* BL */
dst[5] = rf; /* BR */ dst[5] = rf; /* BR */
} }
@ -204,7 +258,44 @@ SDL_ConvertStereoTo51(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
/* Duplicate a stereo channel to a pseudo-4.0 stream */ /* Upmix quad to a pseudo-5.1 stream */
static void SDLCALL
SDL_ConvertQuadTo51(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
int i;
float lf, rf, lb, rb, ce;
const float *src = (const float *) (cvt->buf + cvt->len_cvt);
float *dst = (float *) (cvt->buf + cvt->len_cvt * 3 / 2);
LOG_DEBUG_CONVERT("quad", "5.1");
SDL_assert(format == AUDIO_F32SYS);
SDL_assert(cvt->len_cvt % (sizeof(float) * 4) == 0);
for (i = cvt->len_cvt / (sizeof(float) * 4); i; --i) {
dst -= 6;
src -= 4;
lf = src[0];
rf = src[1];
lb = src[2];
rb = src[3];
ce = (lf + rf) * 0.5f;
/* !!! FIXME: FL and FR may clip */
dst[0] = lf + (lf - ce); /* FL */
dst[1] = rf + (rf - ce); /* FR */
dst[2] = ce; /* FC */
dst[3] = 0; /* LFE (only meant for special LFE effects) */
dst[4] = lb; /* BL */
dst[5] = rb; /* BR */
}
cvt->len_cvt = cvt->len_cvt * 3 / 2;
if (cvt->filters[++cvt->filter_index]) {
cvt->filters[cvt->filter_index] (cvt, format);
}
}
/* Upmix stereo to a pseudo-4.0 stream (by duplication) */
static void SDLCALL static void SDLCALL
SDL_ConvertStereoToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format) SDL_ConvertStereoToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{ {
@ -216,7 +307,7 @@ SDL_ConvertStereoToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format)
LOG_DEBUG_CONVERT("stereo", "quad"); LOG_DEBUG_CONVERT("stereo", "quad");
SDL_assert(format == AUDIO_F32SYS); SDL_assert(format == AUDIO_F32SYS);
for (i = cvt->len_cvt / 8; i; --i) { for (i = cvt->len_cvt / (sizeof(float) * 2); i; --i) {
dst -= 4; dst -= 4;
src -= 2; src -= 2;
lf = src[0]; lf = src[0];
@ -233,6 +324,52 @@ SDL_ConvertStereoToQuad(SDL_AudioCVT * cvt, SDL_AudioFormat format)
} }
} }
/* Upmix 5.1 to 7.1 */
static void SDLCALL
SDL_Convert51To71(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
float lf, rf, lb, rb, ls, rs;
int i;
const float *src = (const float *) (cvt->buf + cvt->len_cvt);
float *dst = (float *) (cvt->buf + cvt->len_cvt * 4 / 3);
LOG_DEBUG_CONVERT("5.1", "7.1");
SDL_assert(format == AUDIO_F32SYS);
SDL_assert(cvt->len_cvt % (sizeof(float) * 6) == 0);
for (i = cvt->len_cvt / (sizeof(float) * 6); i; --i) {
dst -= 8;
src -= 6;
lf = src[0];
rf = src[1];
lb = src[4];
rb = src[5];
ls = (lf + lb) * 0.5f;
rs = (rf + rb) * 0.5f;
/* !!! FIXME: these four may clip */
lf += lf - ls;
rf += rf - ls;
lb += lb - ls;
rb += rb - ls;
dst[3] = src[3]; /* LFE */
dst[2] = src[2]; /* FC */
dst[7] = rs; /* SR */
dst[6] = ls; /* SL */
dst[5] = rb; /* BR */
dst[4] = lb; /* BL */
dst[1] = rf; /* FR */
dst[0] = lf; /* FL */
}
cvt->len_cvt = cvt->len_cvt * 4 / 3;
if (cvt->filters[++cvt->filter_index]) {
cvt->filters[cvt->filter_index] (cvt, format);
}
}
static int static int
SDL_ResampleAudioSimple(const int chans, const double rate_incr, SDL_ResampleAudioSimple(const int chans, const double rate_incr,
float *last_sample, const float *inbuf, float *last_sample, const float *inbuf,
@ -732,9 +869,9 @@ SDL_SupportedChannelCount(const int channels)
case 2: /* stereo */ case 2: /* stereo */
case 4: /* quad */ case 4: /* quad */
case 6: /* 5.1 */ case 6: /* 5.1 */
case 8: /* 7.1 */
return SDL_TRUE; /* supported. */ return SDL_TRUE; /* supported. */
case 8: /* !!! FIXME: 7.1 */
default: default:
break; break;
} }
@ -857,7 +994,9 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
} }
/* Channel conversion */ /* Channel conversion */
if (src_channels != dst_channels) { if (src_channels < dst_channels) {
/* Upmixing */
/* Mono -> Stereo [-> ...] */
if ((src_channels == 1) && (dst_channels > 1)) { if ((src_channels == 1) && (dst_channels > 1)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertMonoToStereo) < 0) { if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertMonoToStereo) < 0) {
return -1; return -1;
@ -866,7 +1005,8 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
src_channels = 2; src_channels = 2;
cvt->len_ratio *= 2; cvt->len_ratio *= 2;
} }
if ((src_channels == 2) && (dst_channels == 6)) { /* [Mono ->] Stereo -> 5.1 [-> 7.1] */
if ((src_channels == 2) && (dst_channels >= 6)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertStereoTo51) < 0) { if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertStereoTo51) < 0) {
return -1; return -1;
} }
@ -874,6 +1014,27 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
cvt->len_mult *= 3; cvt->len_mult *= 3;
cvt->len_ratio *= 3; cvt->len_ratio *= 3;
} }
/* Quad -> 5.1 [-> 7.1] */
if ((src_channels == 4) && (dst_channels >= 6)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertQuadTo51) < 0) {
return -1;
}
src_channels = 6;
cvt->len_mult = (cvt->len_mult * 3 + 1) / 2;
cvt->len_ratio *= 1.5;
}
/* [[Mono ->] Stereo ->] 5.1 -> 7.1 */
if ((src_channels == 6) && (dst_channels == 8)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51To71) < 0) {
return -1;
}
src_channels = 8;
cvt->len_mult = (cvt->len_mult * 4 + 2) / 3;
/* Should be numerically exact with every valid input to this
function */
cvt->len_ratio = cvt->len_ratio * 4 / 3;
}
/* [Mono ->] Stereo -> Quad */
if ((src_channels == 2) && (dst_channels == 4)) { if ((src_channels == 2) && (dst_channels == 4)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertStereoToQuad) < 0) { if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertStereoToQuad) < 0) {
return -1; return -1;
@ -882,14 +1043,18 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
cvt->len_mult *= 2; cvt->len_mult *= 2;
cvt->len_ratio *= 2; cvt->len_ratio *= 2;
} }
while ((src_channels * 2) <= dst_channels) { } else if (src_channels > dst_channels) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertMonoToStereo) < 0) { /* Downmixing */
/* 7.1 -> 5.1 [-> Stereo [-> Mono]] */
/* 7.1 -> 5.1 [-> Quad] */
if ((src_channels == 8) && (dst_channels <= 6)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_Convert71To51) < 0) {
return -1; return -1;
} }
cvt->len_mult *= 2; src_channels = 6;
src_channels *= 2; cvt->len_ratio *= 0.75;
cvt->len_ratio *= 2;
} }
/* [7.1 ->] 5.1 -> Stereo [-> Mono] */
if ((src_channels == 6) && (dst_channels <= 2)) { if ((src_channels == 6) && (dst_channels <= 2)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51ToStereo) < 0) { if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51ToStereo) < 0) {
return -1; return -1;
@ -897,19 +1062,24 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
src_channels = 2; src_channels = 2;
cvt->len_ratio /= 3; cvt->len_ratio /= 3;
} }
/* 5.1 -> Quad */
if ((src_channels == 6) && (dst_channels == 4)) { if ((src_channels == 6) && (dst_channels == 4)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51ToQuad) < 0) { if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51ToQuad) < 0) {
return -1; return -1;
} }
src_channels = 4; src_channels = 4;
cvt->len_ratio = cvt->len_ratio * 2 / 3;
}
/* Quad -> Stereo [-> Mono] */
if ((src_channels == 4) && (dst_channels <= 2)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertQuadToStereo) < 0) {
return -1;
}
src_channels = 2;
cvt->len_ratio /= 2; cvt->len_ratio /= 2;
} }
/* This assumes that 4 channel audio is in the format: /* [... ->] Stereo -> Mono */
Left {front/back} + Right {front/back} if ((src_channels == 2) && (dst_channels == 1)) {
so converting to L/R stereo works properly.
*/
while (((src_channels % 2) == 0) &&
((src_channels / 2) >= dst_channels)) {
SDL_AudioFilter filter = NULL; SDL_AudioFilter filter = NULL;
#if HAVE_SSE3_INTRINSICS #if HAVE_SSE3_INTRINSICS
@ -926,12 +1096,15 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
return -1; return -1;
} }
src_channels /= 2; src_channels = 1;
cvt->len_ratio /= 2; cvt->len_ratio /= 2;
} }
if (src_channels != dst_channels) {
/* Uh oh.. */ ;
} }
if (src_channels != dst_channels) {
/* All combinations of supported channel counts should have been
handled by now, but let's be defensive */
return SDL_SetError("Invalid channel combination");
} }
/* Do rate conversion, if necessary. Updates (cvt). */ /* Do rate conversion, if necessary. Updates (cvt). */