From bbe3bd0304421d69096c2ea21196a4a1d7a6bbd6 Mon Sep 17 00:00:00 2001 From: Beru Date: Mon, 7 Sep 2026 10:58:23 +0200 Subject: [PATCH] Audio: support 12-channel AudioOut2 output with SDL (#498) * Audio: support 12-channel AudioOut2 output via stereo downmix * Audio: document 7.1.4 downmix matrix and align s16 coefficients * audio: use shared 12-to-8 channel conversion for SDL --------- Co-authored-by: Airsoft Project Co-authored-by: nmzik --- src/libs/audio.cpp | 84 ++++++++++++++++++++---------------- src/libs/audio_internal.h | 1 + src/libs/libAudio2.cpp | 5 +++ tests/AudioOut2PortTests.cpp | 26 +++++++++++ 4 files changed, 79 insertions(+), 37 deletions(-) diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index 9a5e0e38c..e699892cc 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -105,7 +105,7 @@ private: Format format = Format::Unknown; uint64_t last_output_time = 0; int channels_num = 0; - int volume[8] = {}; + int volume[12] = {}; SDL_AudioDeviceID audio_device = 0; SDL_AudioSpec audio_spec = {}; @@ -127,7 +127,7 @@ private: static bool FormatIsFloat(Format format); static bool FormatIsStd(Format format); static uint32_t BytesPerSample(Format format); - static uint32_t FrameSize(const PortOut& port); + static uint32_t OutputChannels(const PortOut& port); static SDL_AudioFormat SdlFormat(Format format); static bool OpenSdlDevice(PortOut* port); static void CloseSdlDevice(PortOut* port); @@ -201,8 +201,14 @@ Audio::~Audio() { } bool Audio::FormatIsFloat(Format format) { - return (format == Format::FloatMono || format == Format::FloatStereo || - format == Format::Float8Ch || format == Format::Float8ChStd); + switch (format) { + case Format::FloatMono: + case Format::FloatStereo: + case Format::Float8Ch: + case Format::Float8ChStd: + case Format::Float12Ch: return true; + default: return false; + } } bool Audio::FormatIsStd(Format format) { @@ -213,8 +219,9 @@ uint32_t Audio::BytesPerSample(Format format) { return FormatIsFloat(format) ? sizeof(float) : sizeof(int16_t); } -uint32_t Audio::FrameSize(const PortOut& port) { - return BytesPerSample(port.format) * port.channels_num; +uint32_t Audio::OutputChannels(const PortOut& port) { + // SDL only takes up to 8 channels. Keep the guest buffer's channel count separate. + return std::min(port.channels_num, 8); } SDL_AudioFormat Audio::SdlFormat(Format format) { @@ -232,7 +239,7 @@ bool Audio::OpenSdlDevice(PortOut* port) { SDL_AudioSpec desired {}; desired.freq = static_cast(port->freq); desired.format = SdlFormat(port->format); - desired.channels = static_cast(port->channels_num); + desired.channels = static_cast(OutputChannels(*port)); desired.samples = static_cast(port->samples_num); desired.callback = nullptr; @@ -272,8 +279,9 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, const auto frames = port.samples_num; const auto channels = static_cast(port.channels_num); + const auto output_channels = OutputChannels(port); const auto bytes_per_sample = BytesPerSample(port.format); - const auto src_size = frames * channels * bytes_per_sample; + const bool reorder = channels >= 8 && !FormatIsStd(port.format); bool volume_changed = false; for (uint32_t ch = 0; ch < channels; ch++) { @@ -283,24 +291,35 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, } } - if (!volume_changed && !FormatIsStd(port.format)) { + if (!volume_changed && !reorder) { return data; } - buffer->resize(src_size); + buffer->resize(frames * output_channels * bytes_per_sample); - static constexpr uint32_t STD_8CH_MAP[8] = {0, 1, 2, 3, 6, 7, 4, 5}; + // SDL wants back speakers before side speakers; non-STD PCM has them reversed. + static constexpr uint32_t SDL_8CH_MAP[8] = {0, 1, 2, 3, 6, 7, 4, 5}; if (FormatIsFloat(port.format)) { auto* dst = reinterpret_cast(buffer->data()); const auto* src = static_cast(data); for (uint32_t frame = 0; frame < frames; frame++) { - for (uint32_t ch = 0; ch < channels; ch++) { - const auto src_ch = - (FormatIsStd(port.format) && channels == 8 ? STD_8CH_MAP[ch] : ch); - dst[frame * channels + ch] = src[frame * channels + src_ch] * - (static_cast(port.volume[ch]) / 32768.0f); + for (uint32_t ch = 0; ch < output_channels; ch++) { + const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch; + dst[frame * output_channels + ch] = + src[frame * channels + src_ch] * + (static_cast(port.volume[src_ch]) / 32768.0f); + } + if (channels == 12) { + // Add the four top channels to their front/back channels, turning 12 into 8. + // SDL handles the rest if the output device has fewer channels. + static constexpr uint32_t HEIGHT_DST[4] = {0, 1, 4, 5}; + for (uint32_t ch = 0; ch < 4; ch++) { + dst[frame * output_channels + HEIGHT_DST[ch]] += + src[frame * channels + 8 + ch] * + (static_cast(port.volume[8 + ch]) / 32768.0f); + } } } } else { @@ -308,17 +327,16 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, const auto* src = static_cast(data); for (uint32_t frame = 0; frame < frames; frame++) { - for (uint32_t ch = 0; ch < channels; ch++) { - const auto src_ch = - (FormatIsStd(port.format) && channels == 8 ? STD_8CH_MAP[ch] : ch); + for (uint32_t ch = 0; ch < output_channels; ch++) { + const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch; int64_t sample = - static_cast(src[frame * channels + src_ch]) * port.volume[ch] / 32768; + static_cast(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768; if (sample > std::numeric_limits::max()) { sample = std::numeric_limits::max(); } else if (sample < std::numeric_limits::min()) { sample = std::numeric_limits::min(); } - dst[frame * channels + ch] = static_cast(sample); + dst[frame * output_channels + ch] = static_cast(sample); } } } @@ -334,8 +352,10 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) { } std::vector prepared_buffer; - const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer); - const auto prepared_size = FrameSize(*port) * port->samples_num; + const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer); + const auto output_channels = OutputChannels(*port); + const auto prepared_size = + BytesPerSample(port->format) * output_channels * port->samples_num; std::vector convert_buffer; const void* queue_data = prepared_data; @@ -343,7 +363,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) { SDL_AudioCVT cvt {}; const int cvt_result = - SDL_BuildAudioCVT(&cvt, SdlFormat(port->format), static_cast(port->channels_num), + SDL_BuildAudioCVT(&cvt, SdlFormat(port->format), static_cast(output_channels), static_cast(port->freq), port->audio_spec.format, port->audio_spec.channels, port->audio_spec.freq); @@ -416,6 +436,7 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For case Format::Float8Ch: case Format::Signed16bit8ChStd: case Format::Float8ChStd: port.channels_num = 8; break; + case Format::Float12Ch: port.channels_num = 12; break; default: EXIT("unknown format"); } @@ -488,20 +509,9 @@ bool Audio::AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume) { auto bit = bitflag & 0x1u; if (bit == 1) { - int src_index = i; - if (port.format == Format::Float8ChStd || - port.format == Format::Signed16bit8ChStd) { - switch (i) { - case 4: src_index = 6; break; - case 5: src_index = 7; break; - case 6: src_index = 4; break; - case 7: src_index = 5; break; - default:; - } - } - port.volume[i] = volume[src_index]; + port.volume[i] = volume[i]; - LOGF("\t port.volume[%d] = volume[%d] (%d)\n", i, src_index, volume[src_index]); + LOGF("\t port.volume[%d] = volume[%d] (%d)\n", i, i, volume[i]); } } diff --git a/src/libs/audio_internal.h b/src/libs/audio_internal.h index 82be73c79..ad596ee5e 100644 --- a/src/libs/audio_internal.h +++ b/src/libs/audio_internal.h @@ -15,6 +15,7 @@ enum class Format { Float8Ch, Signed16bit8ChStd, Float8ChStd, + Float12Ch, }; struct OutputParam { diff --git a/src/libs/libAudio2.cpp b/src/libs/libAudio2.cpp index c0133d495..a738ae5ae 100644 --- a/src/libs/libAudio2.cpp +++ b/src/libs/libAudio2.cpp @@ -261,6 +261,11 @@ static AudioInternal::Format audioout2_data_format_to_audio_format(uint32_t data case 8: return is_std ? AudioInternal::Format::Float8ChStd : AudioInternal::Format::Float8Ch; + case 12: + if (!is_std) { + return AudioInternal::Format::Float12Ch; + } + break; default: break; } break; diff --git a/tests/AudioOut2PortTests.cpp b/tests/AudioOut2PortTests.cpp index 85deba42e..7665b634a 100644 --- a/tests/AudioOut2PortTests.cpp +++ b/tests/AudioOut2PortTests.cpp @@ -258,6 +258,31 @@ void TestSynchronousDevicePushBypassesModelledQueue() { AudioOut2::AudioOut2ContextDestroy(context); } +void TestFloat12ChannelPortOutputsPcm() { + const auto context = CreateContext(); + const auto param = MakeParam(0x0c00); + AudioOut2::AudioOut2PortHandle port = 0; + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, + "12-channel port create failed"); + Check(LiveDeviceCount() == 1, "12-channel port did not open an audio device"); + + PortState state {}; + Check(AudioOut2::AudioOut2PortGetState(port, AsState(&state)) == OK, + "12-channel port state query failed"); + Check(state.num_channels == 12, "12-channel port lost its guest channel count"); + + float pcm[512 * 12] {}; + SetPcm(port, pcm); + ResetOutputCalls(); + Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "12-channel PCM push failed"); + const auto calls = OutputCalls(); + Check(calls.size() == 1 && calls[0], "12-channel PCM did not reach the device backend"); + + AudioOut2::AudioOut2PortDestroy(port); + Check(LiveDeviceCount() == 0, "12-channel port leaked its audio device"); + AudioOut2::AudioOut2ContextDestroy(context); +} + void TestAsynchronousDevicePushKeepsQueueBounded() { const auto context = CreateContext(1); const auto param = MakeParam(); @@ -361,6 +386,7 @@ int main() { TestConcurrentCreates(); TestContextDestroyCancelsPendingCreate(); TestSynchronousDevicePushBypassesModelledQueue(); + TestFloat12ChannelPortOutputsPcm(); TestAsynchronousDevicePushKeepsQueueBounded(); TestHandleWithoutPcmDoesNotBypassQueue(); std::printf("AudioOut2PortTests: all cases passed\n");