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.
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
#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
SDL_ConvertStereoToMono_SSE3(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
@ -71,7 +71,7 @@ SDL_ConvertStereoToMono_SSE3(SDL_AudioCVT * cvt, SDL_AudioFormat format)
}
#endif
/* Effectively mix right and left channels into a single channel */
/* Convert from stereo to mono. Average left and right. */
static void SDLCALL
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
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");
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) {
const double front_center = (double) src[2];
dst[0] = (float) ((src[0] + front_center + src[4]) / 3.0); /* left */
dst[1] = (float) ((src[1] + front_center + src[5]) / 3.0); /* right */
const float front_center_distributed = src[2] * 0.5f;
dst[0] = (src[0] + front_center_distributed + src[4]) / 2.5f; /* left */
dst[1] = (src[1] + front_center_distributed + src[5]) / 2.5f; /* right */
}
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
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");
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) {
/* FIXME: this is a good candidate for SIMD. */
const double front_center = (double) src[2];
dst[0] = (float) ((src[0] + front_center) * 0.5); /* FL */
dst[1] = (float) ((src[1] + front_center) * 0.5); /* FR */
dst[2] = (float) ((src[4] + front_center) * 0.5); /* BL */
dst[3] = (float) ((src[5] + front_center) * 0.5); /* BR */
const float front_center_distributed = src[2] * 0.5f;
dst[0] = (src[0] + front_center_distributed) / 1.5f; /* FL */
dst[1] = (src[1] + front_center_distributed) / 1.5f; /* FR */
dst[2] = src[4] / 1.5f; /* BL */
dst[3] = src[5] / 1.5f; /* BR */
}
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
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
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");
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;
src -= 2;
lf = src[0];
rf = src[1];
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] = ce; /* !!! FIXME: wrong! This is the subwoofer. */
dst[3] = 0; /* LFE (only meant for special LFE effects) */
dst[4] = lf; /* BL */
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
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");
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;
src -= 2;
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
SDL_ResampleAudioSimple(const int chans, const double rate_incr,
float *last_sample, const float *inbuf,
@ -732,9 +869,9 @@ SDL_SupportedChannelCount(const int channels)
case 2: /* stereo */
case 4: /* quad */
case 6: /* 5.1 */
return SDL_TRUE; /* supported. */
case 8: /* 7.1 */
return SDL_TRUE; /* supported. */
case 8: /* !!! FIXME: 7.1 */
default:
break;
}
@ -857,7 +994,9 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
}
/* Channel conversion */
if (src_channels != dst_channels) {
if (src_channels < dst_channels) {
/* Upmixing */
/* Mono -> Stereo [-> ...] */
if ((src_channels == 1) && (dst_channels > 1)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertMonoToStereo) < 0) {
return -1;
@ -866,7 +1005,8 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
src_channels = 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) {
return -1;
}
@ -874,6 +1014,27 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
cvt->len_mult *= 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 (SDL_AddAudioCVTFilter(cvt, SDL_ConvertStereoToQuad) < 0) {
return -1;
@ -882,14 +1043,18 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
cvt->len_mult *= 2;
cvt->len_ratio *= 2;
}
while ((src_channels * 2) <= dst_channels) {
if (SDL_AddAudioCVTFilter(cvt, SDL_ConvertMonoToStereo) < 0) {
} else if (src_channels > dst_channels) {
/* 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;
}
cvt->len_mult *= 2;
src_channels *= 2;
cvt->len_ratio *= 2;
src_channels = 6;
cvt->len_ratio *= 0.75;
}
/* [7.1 ->] 5.1 -> Stereo [-> Mono] */
if ((src_channels == 6) && (dst_channels <= 2)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51ToStereo) < 0) {
return -1;
@ -897,19 +1062,24 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
src_channels = 2;
cvt->len_ratio /= 3;
}
/* 5.1 -> Quad */
if ((src_channels == 6) && (dst_channels == 4)) {
if (SDL_AddAudioCVTFilter(cvt, SDL_Convert51ToQuad) < 0) {
return -1;
}
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;
}
/* This assumes that 4 channel audio is in the format:
Left {front/back} + Right {front/back}
so converting to L/R stereo works properly.
*/
while (((src_channels % 2) == 0) &&
((src_channels / 2) >= dst_channels)) {
/* [... ->] Stereo -> Mono */
if ((src_channels == 2) && (dst_channels == 1)) {
SDL_AudioFilter filter = NULL;
#if HAVE_SSE3_INTRINSICS
@ -926,14 +1096,17 @@ SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
return -1;
}
src_channels /= 2;
src_channels = 1;
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). */
if (SDL_BuildAudioResampleCVT(cvt, dst_channels, src_rate, dst_rate) < 0) {
return -1; /* shouldn't happen, but just in case... */