From 2ab70f86195f01d7e01b3982b9f51013b916df3a Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:03:58 +0000 Subject: [PATCH] Implement surround panner --- src/dusk/audio/DuskDsp.cpp | 236 +++++++++++++++++++++++++++++++++---- 1 file changed, 214 insertions(+), 22 deletions(-) diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 824289679a..d8293b4cd2 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "Adpcm.hpp" #include "dusk/audio/DuskAudioSystem.h" @@ -142,6 +143,12 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) { } } +static void MixOutputSubframe(OutputSubframe& dst, const OutputSubframe& src) { + for (int i = 0; i < OutChannelCount; i++) { + MixSubframe(dst.channels[i], src.channels[i]); + } +} + enum class OscType : u16 { SQUARE_WAVE_PW_50 = 0, SAW_WAVE = 1, @@ -470,6 +477,8 @@ struct VolumeValue { f32 Init; }; +using VolumeArray = std::array; + static void ApplyVolume( std::span dst, std::span src, @@ -489,23 +498,135 @@ static void ApplyVolume( } } +struct SpeakerPlacement { + OutputChannel channel; + f32 angle; +}; + +struct SpeakerPair { + OutputChannel first; + OutputChannel second; + f32 lo; + f32 span; +}; + +template +constexpr auto BuildSpeakerPairs(const std::array& config) { + std::array pairs = {}; + constexpr f32 kDegToRad = std::numbers::pi_v / 180.0f; + + for (std::size_t i = 0; i < N; i++) { + std::size_t j = (i + 1) % N; + float lo = config[i].angle; + float hi = config[j].angle; + float span = hi - lo; + if (span < 0) span += 360.0f; + + pairs[i] = {config[i].channel, config[j].channel, lo * kDegToRad, span * kDegToRad}; + } + + return pairs; +} + +constexpr auto Placement6ch = std::to_array({ + // the "rear" channels are actually surround left/right, + // changed to match SDL order + {OutputChannel::FRONT_RIGHT, -30}, + {OutputChannel::FRONT_CENTER, 0}, + {OutputChannel::FRONT_LEFT, 30}, + {OutputChannel::REAR_LEFT, 120}, + {OutputChannel::REAR_RIGHT, -120}, +}); + +constexpr auto Placement8ch = std::to_array({ + {OutputChannel::SURROUND_RIGHT, -90}, + {OutputChannel::FRONT_RIGHT, -30}, + {OutputChannel::FRONT_CENTER, 0}, + {OutputChannel::FRONT_LEFT, 30}, + {OutputChannel::SURROUND_LEFT, 90}, + {OutputChannel::REAR_LEFT, 150}, + {OutputChannel::REAR_RIGHT, -150}, +}); + +constexpr auto Pairs6ch = BuildSpeakerPairs(Placement6ch); +constexpr auto Pairs8ch = BuildSpeakerPairs(Placement8ch); + +static void CalcStereoChannelVolumes( + const JASDsp::TChannel& voice, + VolumeArray& volumes) +{ + const auto volume = VolumeFromU16(voice.mAutoMixerVolume); + const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); + + const auto right = static_cast(voice.mAutoMixerPanDolby >> 8) / 127.0f; + const auto left = 1.0f - right; + + volumes[0] = {left * volume, left * initVolume}; + volumes[1] = {right * volume, right * initVolume}; +} + +static void CalcSurroundChannelVolumes( + const JASDsp::TChannel& voice, + VolumeArray& volumes) +{ + constexpr f32 kTurn = 2.0f * std::numbers::pi_v; + + const auto omniGain = 1.0f / static_cast(OutChannelCount - 1); + const auto pan = static_cast(voice.mAutoMixerPanDolby >> 8) / 63.5f - 1.0f; + const auto dolby = static_cast(voice.mAutoMixerPanDolby & 0xFF) / 63.5f - 1.0f; + const auto focus = std::min(std::sqrt(pan * pan + dolby * dolby), 1.0f); + + f32 angle = std::atan2(-pan, -dolby); + angle = std::fmod(angle, kTurn); + if (angle < 0) angle += kTurn; + + std::array gains = {}; + gains.fill(omniGain * (1.0f - focus)); + + using Pairs = std::span; + const auto pairs = OutChannelCount == 6 ? Pairs{Pairs6ch} : Pairs{Pairs8ch}; + + for (const auto& pair : pairs) { + const auto offset = std::fmod(angle - pair.lo + kTurn, kTurn); + + if (offset <= pair.span) { + const auto first = static_cast(pair.first); + const auto second = static_cast(pair.second); + const auto t = std::clamp(offset / pair.span, 0.0f, 1.0f); + + const auto firstGain = std::cos(t * std::numbers::pi_v / 2.0f); + const auto secondGain = std::sin(t * std::numbers::pi_v / 2.0f); + + gains[first] += focus * firstGain; + gains[second] += focus * secondGain; + + break; + } + } + + const auto volume = VolumeFromU16(voice.mAutoMixerVolume); + const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); + + for (size_t i = 0; i < OutChannelCount; i++) { + volumes[i].Target = volume * gains[i]; + volumes[i].Init = initVolume * gains[i]; + } +} + static void ApplyPanning( const JASDsp::TChannel& voice, ChannelAuxData& aux, const DspSubframe& input, OutputSubframe& output) { - std::array volumes = {}; + VolumeArray volumes = {}; if (voice.mAutoMixerBeenSet) { - const auto volume = VolumeFromU16(voice.mAutoMixerVolume); - const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); - - const auto right = static_cast(voice.mAutoMixerPanDolby >> 8) / 127.0f; - const auto left = 1.0f - right; - - volumes[0] = {left * volume, left * initVolume}; - volumes[1] = {right * volume, right * initVolume}; + if (OutChannelCount > 2) { + CalcSurroundChannelVolumes(voice, volumes); + } else { + CalcStereoChannelVolumes(voice, volumes); + } } else { for (const auto& outChannel : voice.mOutputChannels) { std::optional ch; @@ -529,8 +650,12 @@ static void ApplyPanning( } } - for (size_t i = 0; i < OutputSubframe::NUM_CHANNELS; i++) { + for (size_t i = 0; i < OutChannelCount; i++) { const auto ch = static_cast(i); + if (ch == OutputChannel::LFE) { + continue; + } + const auto& volume = volumes[i]; const f32 targetVolume = volume.Target; @@ -550,6 +675,59 @@ static void ApplyPanning( } } +static void DownmixSurroundToStereo( + const OutputSubframe& input, + OutputSubframe& output) +{ + auto& left = output.channels[0]; + auto& right = output.channels[1]; + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto fc = input.channels[2][i] * 0.5f; + left[i] = input.channels[0][i] + fc + input.channels[4][i]; + right[i] = input.channels[1][i] + fc + input.channels[5][i]; + if (OutChannelCount > 6) { + left[i] += input.channels[6][i]; + right[i] += input.channels[7][i]; + left[i] /= 3.5f; + right[i] /= 3.5f; + } else { + left[i] /= 2.5f; + right[i] /= 2.5f; + } + } +} + +static void UpmixStereoToSurroundInplace(OutputSubframe& buf) { + // pseudoinverse of downmix matrix + const auto w = OutChannelCount > 6 ? 1.0f / 12.0f : 1.0f / 8.0f; + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto le = buf.channels[0][i] * (1.0f + w) + buf.channels[1][i] * -w; + const auto re = buf.channels[1][i] * (1.0f + w) + buf.channels[0][i] * -w; + const auto c = buf.channels[0][i] * 0.5f + buf.channels[1][i] * 0.5f; + /* FL */ buf.channels[0][i] = le; + /* FR */ buf.channels[1][i] = re; + /* FC */ buf.channels[2][i] = c; + /* LFE */ buf.channels[3][i] = 0.0f; + /* BL */ buf.channels[4][i] = le; + /* BR */ buf.channels[5][i] = re; + if (OutChannelCount > 6) { + /* SL */ buf.channels[6][i] = le; + /* SR */ buf.channels[7][i] = re; + } + } +} + +static void AccumulateReverbInput( + DspSubframe& dstL, DspSubframe& dstR, + const DspSubframe& srcL, const DspSubframe& srcR, + f32 gain) +{ + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + dstL[j] += srcL[j] * gain; + dstR[j] += srcR[j] * gain; + } +} + void dusk::audio::DspInit() { SharedReverb.setwet(1.0f); SharedReverb.setdry(0.0f); @@ -619,9 +797,12 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { f32 inputGain = (voice.mAutoMixerFxMix >> 8) / 600.0f; if (inputGain > 0) { anyReverbInput = true; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - reverbInputL[j] += buf.channels[0][j] * inputGain; - reverbInputR[j] += buf.channels[1][j] * inputGain; + if (OutChannelCount > 2) { + OutputSubframe downmix; + DownmixSurroundToStereo(buf, downmix); + AccumulateReverbInput(reverbInputL, reverbInputR, downmix.channels[0], downmix.channels[1], inputGain); + } else { + AccumulateReverbInput(reverbInputL, reverbInputR, buf.channels[0], buf.channels[1], inputGain); } } } @@ -650,19 +831,29 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); } - for (int o = 0; o < subframe.channels.size(); o++) { - MixSubframe(subframe.channels[o], buf.channels[o]); - } + MixOutputSubframe(subframe, buf); } if (EnableReverb && (anyReverbInput || ReverbHasTail)) { // Equivalent to -80 dBFS: rms = 1e-4, rms^2 = 1e-8, sumSq = 2 * N * 1e-8 constexpr f32 REVERB_ENERGY_EPSILON = 2.0f * DSP_SUBFRAME_SIZE * 1e-8f; - f32 wetEnergy = SharedReverb.processmix( - reverbInputL.data(), reverbInputR.data(), - subframe.channels[0].data(), subframe.channels[1].data(), - DSP_SUBFRAME_SIZE, 1, 1.0f - ); + f32 wetEnergy = 0.0f; + if (OutChannelCount > 2) { + OutputSubframe reverbOut; + wetEnergy = SharedReverb.processreplace( + reverbInputL.data(), reverbInputR.data(), + reverbOut.channels[0].data(), reverbOut.channels[1].data(), + DSP_SUBFRAME_SIZE, 1, 1.0f + ); + UpmixStereoToSurroundInplace(reverbOut); + MixOutputSubframe(subframe, reverbOut); + } else { + wetEnergy = SharedReverb.processmix( + reverbInputL.data(), reverbInputR.data(), + subframe.channels[0].data(), subframe.channels[1].data(), + DSP_SUBFRAME_SIZE, 1, 1.0f + ); + } ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON; } @@ -688,7 +879,8 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { } } - for (auto& channel : subframe.channels) { + for (int i = 0; i < OutChannelCount; i++) { + auto& channel = subframe.channels[i]; ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); } PrevMasterVolume = MasterVolume;