From defeac3fab7301f5fdcd719d86cfa6e3afdeb5fc Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:45:43 +0000 Subject: [PATCH] Prepare audio system and UI for surround --- libs/JSystem/src/JAudio2/JASChannel.cpp | 4 -- libs/JSystem/src/JAudio2/JASDriverIF.cpp | 12 ++++ src/Z2AudioLib/Z2Audience.cpp | 16 ++++- src/dusk/audio/DuskAudioSystem.cpp | 85 ++++++++++++++++++------ src/dusk/audio/DuskAudioSystem.h | 2 + src/dusk/audio/DuskDsp.cpp | 6 +- src/dusk/audio/DuskDsp.hpp | 12 +++- src/dusk/config.cpp | 3 +- src/dusk/settings.cpp | 4 +- src/dusk/settings.h | 15 ++++- src/dusk/ui/settings.cpp | 43 ++++++++++-- src/m_Do/m_Do_main.cpp | 1 - 12 files changed, 159 insertions(+), 44 deletions(-) diff --git a/libs/JSystem/src/JAudio2/JASChannel.cpp b/libs/JSystem/src/JAudio2/JASChannel.cpp index a99c13ce63..a709edc3f9 100644 --- a/libs/JSystem/src/JAudio2/JASChannel.cpp +++ b/libs/JSystem/src/JAudio2/JASChannel.cpp @@ -173,11 +173,7 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo f32 pan = 0.5f; f32 dolby = 0.0f; -#if TARGET_PC - u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode(); -#else u32 effectiveOutputMode = JASDriver::getOutputMode(); -#endif switch (effectiveOutputMode) { case JAS_OUTPUT_MONO: break; diff --git a/libs/JSystem/src/JAudio2/JASDriverIF.cpp b/libs/JSystem/src/JAudio2/JASDriverIF.cpp index 87a88adca0..a08e3339ea 100644 --- a/libs/JSystem/src/JAudio2/JASDriverIF.cpp +++ b/libs/JSystem/src/JAudio2/JASDriverIF.cpp @@ -3,6 +3,7 @@ #include "JSystem/JAudio2/JASDriverIF.h" #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASDSPInterface.h" +#include "dusk/settings.h" #include void JASDriver::setDSPLevel(f32 param_0) { @@ -30,7 +31,18 @@ void JASDriver::setOutputMode(u32 mode) { } u32 JASDriver::getOutputMode() { +#ifdef TARGET_PC + switch (dusk::getSettings().audio.outputMode) { + case dusk::AudioOutputMode::StereoSpeakers: + return JAS_OUTPUT_STEREO; + case dusk::AudioOutputMode::StereoHeadphones: + case dusk::AudioOutputMode::Surround6ch: + case dusk::AudioOutputMode::Surround8ch: + return JAS_OUTPUT_SURROUND; + } +#else return JASDriver::JAS_SYSTEM_OUTPUT_MODE; +#endif } void JASDriver::waitSubFrame() { diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp index dda1a4b2d0..e5f9bfebf4 100644 --- a/src/Z2AudioLib/Z2Audience.cpp +++ b/src/Z2AudioLib/Z2Audience.cpp @@ -745,16 +745,26 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) { f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) { f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ(); #if TARGET_PC - if (dusk::audio::EnableHrtf) { + const auto mode = dusk::getSettings().audio.outputMode.getValue(); + if (mode >= dusk::AudioOutputMode::StereoHeadphones) { // Normalize the direction so result is purely front/back orientation, // independent of how far away the sound is - f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z; + f32 lenSq = param_0.x * param_0.x + param_0.z * param_0.z; + if (mode == dusk::AudioOutputMode::StereoHeadphones) { + // original HRTF math + lenSq += param_0.y * param_0.y; + } if (lenSq < 0.0001f) { return 0.5f; } f32 zNorm = param_0.z / sqrtf(lenSq); f32 t = (zNorm + 1.0f) * 0.5f; - return 0.5f - 0.5f * cosf(t * static_cast(M_PI)); + if (mode == dusk::AudioOutputMode::StereoHeadphones) { + // original HRTF math + return 0.5f - 0.5f * cosf(t * static_cast(M_PI)); + } else { + return t; + } } #endif if (fVar1 > mSetting.field_0x48) { diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index 3fda98a436..b1c36b419c 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -19,7 +19,7 @@ using namespace dusk::audio; static OutputSubframe OutBuffer; -static std::array OutInterleaveBuffer; +static std::array OutInterleaveBufferFull; static SDL_AudioStream* PlaybackStream; @@ -42,21 +42,55 @@ static int RenderNewAudioFrame(); /** * Render an audio subframe and output it to SDL3. */ -static void RenderAudioSubframe(); +static int RenderAudioSubframe(); -static void InitSDL3Output() { - SDL_Init(SDL_INIT_AUDIO); +static size_t GetChannelCountForOutputMode(dusk::AudioOutputMode config) { + switch (config) { + default: + case dusk::AudioOutputMode::StereoSpeakers: + case dusk::AudioOutputMode::StereoHeadphones: + return 2; + case dusk::AudioOutputMode::Surround6ch: + return 6; + case dusk::AudioOutputMode::Surround8ch: + return 8; + } +} - constexpr SDL_AudioSpec spec = { +static bool InitSDL3Output() { + const auto speakerConfig = dusk::getSettings().audio.outputMode.getValue(); + const auto desiredChannelCount = GetChannelCountForOutputMode(speakerConfig); + const bool hrtf = speakerConfig == dusk::AudioOutputMode::StereoHeadphones; + + if (PlaybackStream && desiredChannelCount == OutChannelCount) { + JASCriticalSection section; + EnableHrtf = hrtf; + return false; + } + + if (PlaybackStream) { + SDL_PauseAudioStreamDevice(PlaybackStream); + SDL_DestroyAudioStream(PlaybackStream); + } else { + SDL_Init(SDL_INIT_AUDIO); + } + + const SDL_AudioSpec spec = { SDL_AUDIO_F32, - 2, + static_cast(desiredChannelCount), SampleRate, }; - PlaybackStream = SDL_OpenAudioDeviceStream( - SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, - &spec, - &GetNewAudio, - nullptr); + SDL_AudioStream* newStream = + SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, &GetNewAudio, nullptr); + + { + JASCriticalSection section; + EnableHrtf = hrtf; + OutChannelCount = desiredChannelCount; + PlaybackStream = newStream; + } + + return true; } void dusk::audio::Initialize() { @@ -71,6 +105,12 @@ void dusk::audio::Initialize() { SDL_ResumeAudioStreamDevice(PlaybackStream); } +void dusk::audio::Reinitialize() { + if (InitSDL3Output()) { + SDL_ResumeAudioStreamDevice(PlaybackStream); + } +} + void dusk::audio::SetMasterVolume(const f32 value) { JASCriticalSection section; @@ -112,53 +152,58 @@ int RenderNewAudioFrame() { ZoneScoped; JASCriticalSection section; const u32 countSubframes = JASDriver::getSubFrames(); + int bytesWritten = 0; JASAudioThread::setDSPSyncCount(countSubframes); for (u32 i = 0; i < countSubframes; i++) { - RenderAudioSubframe(); + bytesWritten += RenderAudioSubframe(); JASAudioThread::snIntCount -= 1; } - return static_cast(countSubframes) * sizeof(OutputSubframe); + return bytesWritten; } static void InterleaveOutputData(const OutputSubframe& data, std::span target) { - assert(target.size() >= data.channels[0].size() * OutputSubframe::NUM_CHANNELS); + assert(target.size() >= data.channels[0].size() * OutChannelCount); size_t outPos = 0; for (size_t inPos = 0; inPos < data.channels[0].size(); inPos++) { - for (size_t channelIdx = 0; channelIdx < OutputSubframe::NUM_CHANNELS; channelIdx++) { + for (size_t channelIdx = 0; channelIdx < OutChannelCount; channelIdx++) { target[outPos++] = data.channels[channelIdx][inPos]; } } } -void RenderAudioSubframe() { +int RenderAudioSubframe() { ZoneScoped; OutBuffer = {}; JASDriver::updateDSP(); DspRender(OutBuffer); + std::span OutInterleaveBuffer{OutInterleaveBufferFull.data(), static_cast(DSP_SUBFRAME_SIZE * OutChannelCount)}; InterleaveOutputData(OutBuffer, OutInterleaveBuffer); if (JASDriver::extMixCallback != nullptr && JASDriver::sMixMode == MIX_MODE_INTERLEAVE) { - static_assert(OutputSubframe::NUM_CHANNELS == 2); // This code only works with Stereo so far. // NOTE: In the real game, this gets called on the entire audio frame, rather than the subframe. // That's probably more efficient, but I didn't wanna change the code to calculate the // entire audio buffers at once. // This is only used for the movie player, and it seems to work fine with the smaller calls. const auto mixData = JASDriver::extMixCallback(DSP_SUBFRAME_SIZE); if (mixData) { - for (int i = 0; i < OutInterleaveBuffer.size(); i++) { - OutInterleaveBuffer[i] += static_cast(mixData[i]) / static_cast(0x7FFF); + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto oi = i * OutChannelCount; + OutInterleaveBuffer[oi] += static_cast(mixData[i * 2]) / 32767.0f; + OutInterleaveBuffer[oi + 1] += static_cast(mixData[i * 2 + 1]) / 32767.0f; } } } - SDL_PutAudioStreamData(PlaybackStream, &OutInterleaveBuffer, sizeof(OutInterleaveBuffer)); + auto bytesToWrite = OutInterleaveBuffer.size_bytes(); + SDL_PutAudioStreamData(PlaybackStream, OutInterleaveBuffer.data(), bytesToWrite); + return bytesToWrite; } u32 dusk::audio::GetResetCount(int channelIdx) { diff --git a/src/dusk/audio/DuskAudioSystem.h b/src/dusk/audio/DuskAudioSystem.h index 780187f9d2..efec50658b 100644 --- a/src/dusk/audio/DuskAudioSystem.h +++ b/src/dusk/audio/DuskAudioSystem.h @@ -19,6 +19,8 @@ namespace dusk::audio { */ void Initialize(); + void Reinitialize(); + void SetEnableReverb(bool value); void SetMasterVolume(f32 value); diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 3ea0c51623..824289679a 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -50,7 +50,7 @@ bool dusk::audio::EnableReverb = true; bool dusk::audio::DumpAudio = false; bool dusk::audio::EnableHrtf = false; f32 dusk::audio::HrtfGain = 0.5f; - +u8 dusk::audio::OutChannelCount = 0; // 3dB at 5kHz. static constexpr f32 HRTF_LP_K = 0.75f; @@ -512,10 +512,10 @@ static void ApplyPanning( switch (outChannel.mBusConnect) { case 0x0D00: - ch = OutputChannel::LEFT; + ch = OutputChannel::FRONT_LEFT; break; case 0x0D60: - ch = OutputChannel::RIGHT; + ch = OutputChannel::FRONT_RIGHT; break; default: break; diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index a2504ab586..47900233a5 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -12,8 +12,15 @@ namespace dusk::audio { constexpr int SampleRate = 32000; enum class OutputChannel : u8 { - LEFT, - RIGHT, + // same as SDL channel layout for 7.1 + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LFE, + REAR_LEFT, + REAR_RIGHT, + SURROUND_LEFT, + SURROUND_RIGHT, OutputChannel_MAX }; @@ -126,4 +133,5 @@ namespace dusk::audio { extern bool DumpAudio; extern bool EnableHrtf; extern f32 HrtfGain; + extern u8 OutChannelCount; } diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 5a66380ced..51c8a7a16c 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -316,6 +316,7 @@ template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; +template class ConfigImpl; template <> void ConfigImpl::loadFromJson( @@ -639,4 +640,4 @@ void shutdown() { s_activeChangeNotifications.clear(); } -} // namespace dusk::config \ No newline at end of file +} // namespace dusk::config diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index a1a0f9ce54..d43530ad73 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -25,13 +25,13 @@ UserSettings g_userSettings = { }, .audio = { + .outputMode {"audio.outputMode", AudioOutputMode::StereoSpeakers}, .masterVolume {"audio.masterVolume", 60}, .mainMusicVolume {"audio.mainMusicVolume", 100}, .subMusicVolume {"audio.subMusicVolume", 100}, .soundEffectsVolume {"audio.soundEffectsVolume", 100}, .fanfareVolume {"audio.fanfareVolume", 100}, .enableReverb {"audio.enableReverb", true}, - .enableHrtf {"audio.enableHrtf", false}, .menuSounds {"audio.menuSounds", true}, }, @@ -256,13 +256,13 @@ void registerSettings() { [](const int&, const int&) { dusk::ui::apply_scale(); }); // Audio + Register(g_userSettings.audio.outputMode); Register(g_userSettings.audio.masterVolume); Register(g_userSettings.audio.mainMusicVolume); Register(g_userSettings.audio.subMusicVolume); Register(g_userSettings.audio.soundEffectsVolume); Register(g_userSettings.audio.fanfareVolume); Register(g_userSettings.audio.enableReverb); - Register(g_userSettings.audio.enableHrtf); Register(g_userSettings.audio.menuSounds); // Game diff --git a/src/dusk/settings.h b/src/dusk/settings.h index 83c2bfc2fa..0b4ce76838 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -68,6 +68,13 @@ enum class MagicArmorMode : u8 { COSMETIC = 4, }; +enum class AudioOutputMode : u8 { + StereoSpeakers = 0, + StereoHeadphones = 1, // spatial audio + Surround6ch = 2, // discrete 5.1 + Surround8ch = 3, // discrete 7.1 +}; + namespace config { template <> struct ConfigEnumRange { @@ -123,6 +130,12 @@ struct ConfigEnumRange { static constexpr auto max = MagicArmorMode::COSMETIC; }; +template <> +struct ConfigEnumRange { + static constexpr auto min = AudioOutputMode::StereoSpeakers; + static constexpr auto max = AudioOutputMode::Surround8ch; +}; + template <> struct ConfigValueTraits { static constexpr bool enabled = true; @@ -150,13 +163,13 @@ struct UserSettings { struct { // Audio + ConfigVar outputMode; ConfigVar masterVolume; ConfigVar mainMusicVolume; ConfigVar subMusicVolume; ConfigVar soundEffectsVolume; ConfigVar fanfareVolume; ConfigVar enableReverb; - ConfigVar enableHrtf; ConfigVar menuSounds; } audio; diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index a6f549048f..123ab4f4bb 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -72,6 +72,13 @@ constexpr std::array kInterpolationModes = { "Unlimited", }; +constexpr std::array kAudioOutputModeNames = { + "Stereo (Speakers)", + "Stereo (Headphones)", + "5.1 Surround", + "7.1 Surround", +}; + constexpr std::array kTouchTargetingLabels = { "Hybrid", "Hold", @@ -1044,6 +1051,35 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { auto& leftPane = add_child(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Uncontrolled); + leftPane.add_section("Output"); + leftPane.register_control( + leftPane.add_select_button({ + .key = "Output Mode", + .getValue = [] { + const auto idx = static_cast(getSettings().audio.outputMode.getValue()); + return Rml::String{kAudioOutputModeNames[idx]}; + }, + .isModified = [] { + const auto& setting = getSettings().audio.outputMode; + return setting.getValue() != setting.getDefaultValue(); + }, + }), rightPane, [](Pane& pane) { + for (int i = 0; i < static_cast(kAudioOutputModeNames.size()); ++i) { + pane.add_button({ + .text = kAudioOutputModeNames[i], + .isSelected = [i] { + const auto& setting = getSettings().audio.outputMode; + return setting.getValue() == static_cast(i); + }, + }).on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().audio.outputMode.setValue(static_cast(i)); + config::save(); + audio::Reinitialize(); + }); + } + }); + // TODO: Individual sliders for Main Music, Sub Music, Sound Effects, and Fanfare. leftPane.add_section("Volume"); leftPane.register_control( @@ -1076,13 +1112,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .helpText = "Enables the reverb effect in game audio.", .onChange = [](bool value) { audio::SetEnableReverb(value); }, }); - config_bool_select(leftPane, rightPane, getSettings().audio.enableHrtf, - { - .key = "Enable Spatial Sound", - .helpText = - "Emulate surround sound via HRTF. Recommended only for use with headphones!", - .onChange = [](bool value) { audio::EnableHrtf = value; }, - }); config_bool_select(leftPane, rightPane, getSettings().audio.menuSounds, { .key = "Dusklight Menu Sounds", diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index dcc6787f4b..af0a489770 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -753,7 +753,6 @@ int game_main(int argc, char* argv[]) { dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f)); dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb); - dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf; // Run ImGui UI loop if Aurora couldn't initialize a backend if (auroraInfo.backend == BACKEND_NULL) {