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/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index 7f18fa426b..6790684e7b 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -876,7 +876,7 @@ void dMenu_Option_c::vib_init() { void dMenu_Option_c::vib_move() { bool upTrigger = mpStick->checkUpTrigger(); - bool downTrigger = mpStick->checkDownTrigger(); + IF_NOT_DUSK(bool downTrigger =) mpStick->checkDownTrigger(); bool leftTrigger = checkLeftTrigger(); bool rightTrigger = checkRightTrigger(); @@ -891,10 +891,14 @@ void dMenu_Option_c::vib_move() { field_0x3ef = PROC_ATTEN_e; #endif Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); - } else if (downTrigger) { + } +#ifndef TARGET_PC + else if (downTrigger) { field_0x3ef = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); - } else if (leftTrigger) { + } +#endif + else if (leftTrigger) { if (isRumbleSupported()) { if (field_0x3ea == 0) { field_0x3ea = 1; @@ -1369,8 +1373,7 @@ void dMenu_Option_c::calibration_close2_move() { void dMenu_Option_c::menuVisible() { for (int i = 0; i < 6; i++) { - if (i < OPTION_SELECT(PROC_CHANGE_MOVE_e)) - { + if (i < OPTION_SELECT(DUSK_IF_ELSE(PROC_SOUND_e, PROC_CHANGE_MOVE_e))) { menuShow(i); } else { menuHide(i); @@ -2478,7 +2481,7 @@ bool dMenu_Option_c::isRumbleSupported() { #if TARGET_PC bool dMenu_Option_c::pointerConfirmSelect() { dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) { + for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) { if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { dusk::menu_pointer::set_hover_target(i); return false; @@ -2502,7 +2505,7 @@ bool dMenu_Option_c::pointerConfirmSelect() { bool dMenu_Option_c::dpdMenuMove() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) { + for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) { if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { continue; } @@ -2582,20 +2585,6 @@ bool dMenu_Option_c::dpdMenuMove() { -1.0f, 0); } return true; - case PROC_SOUND_e: - if (field_0x3e9 == 0) { - field_0x3e9 = 2; - } else { - field_0x3e9--; - } - field_0x3da = 5; - mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]); - setSoundMode(dMo_soundMode[field_0x3e9]); - field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); - field_0x3f5 = OPTION_SELECT(PROC_SOUND_e); - Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, - -1.0f, 0); - return true; } } #endif diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index 13275ed830..90ab355228 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -20,7 +20,7 @@ using namespace dusk::audio; static OutputSubframe OutBuffer; -static std::array OutInterleaveBuffer; +static std::array OutInterleaveBufferFull; static SDL_AudioStream* PlaybackStream; @@ -43,21 +43,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() { @@ -72,6 +106,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; @@ -113,53 +153,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 01b07be90c..2e3c14cad7 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -16,6 +16,7 @@ #include #include #include +#include using namespace dusk::audio; @@ -50,7 +51,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; @@ -101,28 +102,6 @@ static u32 ConvertSamplesToDataLength(const JASDsp::TChannel& channel, u32 sampl return (samples / channel.mSamplesPerBlock) * BlockBytes(channel); } -/** - * Render the audio data contributed by a single DSP channel. Reads & decodes new input samples. - */ -static void RenderChannel( - JASDsp::TChannel& channel, - ChannelAuxData& channelAux, - OutputSubframe& subframe); - -static void RenderOutputChannel( - const JASDsp::TChannel& sourceChannel, - ChannelAuxData& aux, - OutputChannel outputChannel, - const std::span inputSamples, - OutputSubframe& fullOutputSubframe); - -/** - * Converts a pitch value on a DSP channel to a sample rate. - */ -constexpr static int PitchToSampleRate(u16 value) { - return static_cast(static_cast(SampleRate) * value / 4096); -} - /** * Reset state for a DSP channel between independent playbacks. */ @@ -164,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, @@ -203,16 +188,14 @@ static void GenerateEvolvingHarmonic() { } } - static void RenderOscChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, - OutputSubframe& subframe) { + DspSubframe& buf) { if (channel.mResetFlag) ResetChannel(channel, channelAux); const u32 pitch = channel.mPitch; - DspSubframe buf = {}; const auto oscType = static_cast(channel.mBytesPerBlock); switch (oscType) { @@ -270,142 +253,6 @@ static void RenderOscChannel( DuskLog.error("RenderOscChannel: unimplemented oscillator type {}", channel.mBytesPerBlock); break; } - - auto samples = std::span(buf).subspan(0, DSP_SUBFRAME_SIZE); - RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, samples, subframe); - RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, samples, subframe); -} - - -void dusk::audio::DspRender(OutputSubframe& subframe) { - ZoneScoped; - if (DumpAudio != sDumpWasActive) { - sDumpWasActive = DumpAudio; - if (DumpAudio) { - OpenChannelDumpFiles(); - } else { - CloseChannelDumpFiles(); - } - } - - GenerateEvolvingHarmonic(); - - std::span channels(JASDsp::CH_BUF, DSP_CHANNELS); - - DspSubframe reverbInputL = {}; - DspSubframe reverbInputR = {}; - bool anyReverbInput = false; - - DspSubframe surroundBus = {}; - bool anySurroundInput = false; - - for (int i = 0; i < channels.size(); i++) { - auto& channel = channels[i]; - auto& channelAux = ChannelAux[i]; - - if (!channel.mIsActive) { - continue; - } - else if (channel.mPauseFlag) { - // Not really sure what the practical difference between pause and - // deactivation is. Either avoids clearing state or allows the DSP to avoid popping? - continue; - } - else if (channel.mForcedStop) { - channel.mIsFinished = true; - continue; - } - - OutputSubframe channelSubframe = {}; - if (channel.mWaveAramAddress == 0) { - RenderOscChannel(channel, channelAux, channelSubframe); - } else { - ValidateChannel(channel); - RenderChannel(channel, channelAux, channelSubframe); - } - - if (EnableReverb) { - // scale the input to the reverb rather than using wet/dry on the output. - // this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix, - // so any tail always decays at the correct level regardless of mAutoMixerFxMix changes - // prevents transients when the next sound starts playing with a different reverb level - // 600.0f was pulled out of my ass and just sounds good enough for console - f32 inputGain = (channel.mAutoMixerFxMix >> 8) / 600.0f; - if (inputGain > 0) { - anyReverbInput = true; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - reverbInputL[j] += channelSubframe.channels[0][j] * inputGain; - reverbInputR[j] += channelSubframe.channels[1][j] * inputGain; - } - } - } - - if (EnableHrtf && channel.mAutoMixerBeenSet) { - f32 dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.0f; - if (dolby > 0.0f) { - anySurroundInput = true; - f32 extract = dolby * HRTF_EXTRACT_MAX; - f32 frontScale = 1.0f - extract; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - f32 mono = (channelSubframe.channels[0][j] + channelSubframe.channels[1][j]) * 0.5f; - surroundBus[j] += mono * extract; - channelSubframe.channels[0][j] *= frontScale; - channelSubframe.channels[1][j] *= frontScale; - } - } - } - - if (DumpAudio && sChannelDumpFiles[i]) { - f32 interleaved[DSP_SUBFRAME_SIZE * 2]; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - interleaved[j * 2 + 0] = channelSubframe.channels[0][j]; - interleaved[j * 2 + 1] = channelSubframe.channels[1][j]; - } - fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); - } - - for (int o = 0; o < subframe.channels.size(); o++) { - MixSubframe(subframe.channels[o], channelSubframe.channels[o]); - } - } - - 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 - ); - ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON; - } - - if (EnableHrtf && anySurroundInput) { - // Two-pole LPF: -12 dB/oct above 3 kHz - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j]; - sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1; - surroundBus[j] = sHrtfLp2; - } - - // Mix into L and R - // L gets the filtered signal directly; R gets it allpass for mild decorrelation - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - f32 s = surroundBus[j]; - - subframe.channels[0][j] += s * HrtfGain; - - f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1; - sHrtfApIn1 = s; - sHrtfApOut1 = r; - subframe.channels[1][j] += r * HrtfGain; - } - } - - for (auto& channel : subframe.channels) { - ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); - } - PrevMasterVolume = MasterVolume; } /** @@ -539,135 +386,19 @@ static void FillDecodeBuf(JASDsp::TChannel& channel, ChannelAuxData& aux, int ne } /** - * Get the expected BusConnect value needed to define the given output channel in a DSP channel. - */ -constexpr u16 GetBusConnect(const OutputChannel channel) { - switch (channel) { - // TODO: This is a guess for now. - case OutputChannel::LEFT: - return 0x0D00; - case OutputChannel::RIGHT: - return 0x0D60; - default: - CRASH("Invalid output channel!"); - } -} - -/** - * For a DSP channel the JASDsp::OutputChannelConfig value targeting the given output channel. - * Returns null if the DSP channel does not output to this output channel. - */ -static const JASDsp::OutputChannelConfig* GetOutputConfig( - const JASDsp::TChannel& sourceChannel, - OutputChannel channel) { - - auto busConnect = GetBusConnect(channel); - for (const auto& mOutputChannel : sourceChannel.mOutputChannels) { - auto config = &mOutputChannel; - if (config->mBusConnect == busConnect) { - return config; - } - } - - return nullptr; -} - -struct VolumeValue { - f32 Target; - f32 Init; -}; - -/** - * Get the volume that the given DSP channel should render to the given output channel at. - */ -static VolumeValue GetVolumeForOutputChannel( - const JASDsp::TChannel& sourceChannel, - OutputChannel outputChannel) { - - u16 volume; - u16 initVolume; - f32 panValue = 1; - if (sourceChannel.mAutoMixerBeenSet) { - volume = sourceChannel.mAutoMixerVolume; - initVolume = sourceChannel.mAutoMixerInitVolume; - - auto autoMixerPan = static_cast(sourceChannel.mAutoMixerPanDolby >> 8) / 127; - - switch (outputChannel) { - case OutputChannel::LEFT: - panValue = 1 - autoMixerPan; - break; - case OutputChannel::RIGHT: - panValue = autoMixerPan; - break; - default: - CRASH("Unhandled output channel: OutputChannel"); - } - - } else { - auto config = GetOutputConfig(sourceChannel, outputChannel); - if (config == nullptr) { - return {0, 0}; - } - - volume = config->mTargetVolume; - initVolume = config->mCurrentVolume; - } - - // TODO: interpolate to avoid popping. - f32 targetRatio = VolumeFromU16(volume); - targetRatio *= panValue; - - f32 initRatio = VolumeFromU16(initVolume); - initRatio *= panValue; - - return {targetRatio, initRatio}; -} - -/** - * Given decoded & resampled input samples, render a DSP channel to a given output channel. - */ -static void RenderOutputChannel( - const JASDsp::TChannel& sourceChannel, - ChannelAuxData& aux, - OutputChannel outputChannel, - const std::span inputSamples, - OutputSubframe& fullOutputSubframe) { - - auto& outputSubframe = fullOutputSubframe[outputChannel]; - assert(inputSamples.size() <= outputSubframe.size()); - - auto volume = GetVolumeForOutputChannel(sourceChannel, outputChannel); - - f32 targetVolume = volume.Target; - auto& prevVolume = aux.PrevVolume(outputChannel); - if (std::isnan(prevVolume)) { - // Initialize previous volume to new volume on first render. - prevVolume = volume.Init; - } - - if (prevVolume == 0 && targetVolume == 0) { - return; - } - - ApplyVolume(outputSubframe, inputSamples, prevVolume, targetVolume); - prevVolume = targetVolume; -} - -/** - * Fetch, decode, resample, output + * Render the audio data contributed by a single DSP channel. Reads & decodes new input samples. */ static void RenderChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, - OutputSubframe& subframe) { + DspSubframe& buf) { if (channel.mResetFlag) { ResetChannel(channel, channelAux); } // how many input samples we step per output sample, aka the resampling ratio - f32 step = (f32)PitchToSampleRate(channel.mPitch) / SampleRate; + auto step = static_cast(channel.mPitch) / 4096.0f; // how many input samples to resample to DSP_SUBFRAME_SIZE output samples int needed = static_cast(channelAux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2; @@ -679,7 +410,6 @@ static void RenderChannel( channel.mIsFinished = true; } - DspSubframe audioLoadBuffer = {}; f32 pos = channelAux.resamplePos; s16 prev = channelAux.resamplePrev; s16 next = channelAux.decodeBufCount > 0 ? channelAux.decodeBuf[0] : prev; @@ -687,7 +417,7 @@ static void RenderChannel( // linear resampling and f32 conversion for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { - audioLoadBuffer[i] = (prev + pos * (next - prev)) / 32768.0f; + buf[i] = (prev + pos * (next - prev)) / 32768.0f; pos += step; while (pos >= 1.0f) { pos -= 1.0f; @@ -705,7 +435,7 @@ static void RenderChannel( // IIR part 1, low-pass: out[n] = (in[n] - in[n-1]) * (coeff/128) + out[n-1] if (s16 coeff = channel.iir_filter_params[4]; coeff != 0) { - for (f32& sample : audioLoadBuffer) { + for (f32& sample : buf) { f32 out = std::clamp( (sample - channelAux.prev_lp_in) * ((f32)coeff / 128.0f) + channelAux.prev_lp_out, -1.0f, 1.0f ); @@ -717,7 +447,7 @@ static void RenderChannel( // IIR part 2, biquad: out[n] = (b1*in[n-1] + b2*in[n-2] + a1*out[n-1] + a2*out[n-2]) / 32768 if ((channel.mFilterMode & 0x20) != 0) { - for (f32& sample : audioLoadBuffer) { + for (f32& sample : buf) { f32 out = std::clamp(( channel.iir_filter_params[0] * channelAux.biq_in1 + // b1 channel.iir_filter_params[1] * channelAux.biq_in2 + // b2 @@ -740,28 +470,18 @@ static void RenderChannel( } channelAux.decodeBufCount = std::max(0, remainingDecodeBuf); - - auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, DSP_SUBFRAME_SIZE); - - static_assert(OutputSubframe::NUM_CHANNELS == 2, "Keep RenderChannel in sync!"); - - RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, hasReadSamples, subframe); - RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, hasReadSamples, subframe); } -void dusk::audio::DspInit() { - SharedReverb.setwet(1.0f); - SharedReverb.setdry(0.0f); - SharedReverb.setroomsize(0.5f); - SharedReverb.setdamp(0.7f); - SharedReverb.setwidth(1.0f); - SharedReverb.setmode(0.0f); - SharedReverb.mute(); -} +struct VolumeValue { + f32 Target; + f32 Init; +}; -void dusk::audio::ApplyVolume( +using VolumeArray = std::array; + +static void ApplyVolume( std::span dst, - const std::span src, + std::span src, const f32 startVolume, const f32 endVolume) { assert(dst.size() >= src.size()); @@ -777,3 +497,391 @@ void dusk::audio::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) +{ + VolumeArray volumes = {}; + + if (voice.mAutoMixerBeenSet) { + if (OutChannelCount > 2) { + CalcSurroundChannelVolumes(voice, volumes); + } else { + CalcStereoChannelVolumes(voice, volumes); + } + } else { + for (const auto& outChannel : voice.mOutputChannels) { + std::optional ch; + + switch (outChannel.mBusConnect) { + case 0x0D00: + ch = OutputChannel::FRONT_LEFT; + break; + case 0x0D60: + ch = OutputChannel::FRONT_RIGHT; + break; + default: + break; + } + + if (ch) { + auto& v = volumes[static_cast(*ch)]; + v.Target = VolumeFromU16(outChannel.mTargetVolume); + v.Init = VolumeFromU16(outChannel.mCurrentVolume); + } + } + } + + 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; + auto& prevVolume = aux.PrevVolume(ch); + + if (std::isnan(prevVolume)) { + // Initialize previous volume to new volume on first render. + prevVolume = volume.Init; + } + + if (prevVolume == 0 && targetVolume == 0) { + continue; + } + + ApplyVolume(output[ch], input, prevVolume, targetVolume); + prevVolume = targetVolume; + } +} + +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); + SharedReverb.setroomsize(0.5f); + SharedReverb.setdamp(0.7f); + SharedReverb.setwidth(1.0f); + SharedReverb.setmode(0.0f); + SharedReverb.mute(); +} + +void dusk::audio::DspRender(OutputSubframe& subframe) { + ZoneScoped; + if (DumpAudio != sDumpWasActive) { + sDumpWasActive = DumpAudio; + if (DumpAudio) { + OpenChannelDumpFiles(); + } else { + CloseChannelDumpFiles(); + } + } + + GenerateEvolvingHarmonic(); + + std::span voices(JASDsp::CH_BUF, DSP_CHANNELS); + + DspSubframe reverbInputL = {}; + DspSubframe reverbInputR = {}; + bool anyReverbInput = false; + + DspSubframe surroundBus = {}; + bool anySurroundInput = false; + + for (int i = 0; i < voices.size(); i++) { + auto& voice = voices[i]; + auto& aux = ChannelAux[i]; + + if (!voice.mIsActive) { + continue; + } + else if (voice.mPauseFlag) { + // Not really sure what the practical difference between pause and + // deactivation is. Either avoids clearing state or allows the DSP to avoid popping? + continue; + } + else if (voice.mForcedStop) { + voice.mIsFinished = true; + continue; + } + + DspSubframe monoBuf = {}; + if (voice.mWaveAramAddress == 0) { + RenderOscChannel(voice, aux, monoBuf); + } else { + ValidateChannel(voice); + RenderChannel(voice, aux, monoBuf); + } + + OutputSubframe buf = {}; + ApplyPanning(voice, aux, monoBuf, buf); + + if (EnableReverb) { + // scale the input to the reverb rather than using wet/dry on the output. + // this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix, + // so any tail always decays at the correct level regardless of mAutoMixerFxMix changes + // prevents transients when the next sound starts playing with a different reverb level + // 600.0f was pulled out of my ass and just sounds good enough for console + f32 inputGain = (voice.mAutoMixerFxMix >> 8) / 600.0f; + if (inputGain > 0) { + anyReverbInput = true; + 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); + } + } + } + + if (EnableHrtf && voice.mAutoMixerBeenSet) { + f32 dolby = (voice.mAutoMixerPanDolby & 0xFF) / 127.0f; + if (dolby > 0.0f) { + anySurroundInput = true; + f32 extract = dolby * HRTF_EXTRACT_MAX; + f32 frontScale = 1.0f - extract; + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + f32 mono = (buf.channels[0][j] + buf.channels[1][j]) * 0.5f; + surroundBus[j] += mono * extract; + buf.channels[0][j] *= frontScale; + buf.channels[1][j] *= frontScale; + } + } + } + + if (DumpAudio && sChannelDumpFiles[i]) { + f32 interleaved[DSP_SUBFRAME_SIZE * 2]; + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + interleaved[j * 2 + 0] = buf.channels[0][j]; + interleaved[j * 2 + 1] = buf.channels[1][j]; + } + fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); + } + + 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 = 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; + } + + if (EnableHrtf && anySurroundInput) { + // Two-pole LPF: -12 dB/oct above 3 kHz + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j]; + sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1; + surroundBus[j] = sHrtfLp2; + } + + // Mix into L and R + // L gets the filtered signal directly; R gets it allpass for mild decorrelation + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + f32 s = surroundBus[j]; + + subframe.channels[0][j] += s * HrtfGain; + + f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1; + sHrtfApIn1 = s; + sHrtfApOut1 = r; + subframe.channels[1][j] += r * HrtfGain; + } + } + + for (int i = 0; i < OutChannelCount; i++) { + auto& channel = subframe.channels[i]; + ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); + } + PrevMasterVolume = MasterVolume; +} diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 8d7b74a1d6..bef422b956 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -8,14 +8,20 @@ #include #include -#include 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 }; @@ -122,16 +128,11 @@ namespace dusk::audio { return channel.mBytesPerBlock; } - /** - * Apply a volume level to audio data. - * Interpolates across the two provided volume levels to avoid clicking. - */ - void ApplyVolume(std::span dst, std::span src, f32 startVolume, f32 endVolume); - extern f32 MasterVolume; extern f32 PrevMasterVolume; extern bool EnableReverb; 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 b9bbe88b70..1749e964d7 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 527e6c32c8..c7fc22ccc1 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -69,6 +69,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", @@ -1041,6 +1048,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( @@ -1073,13 +1109,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 3b5606eaae..b44f5e41b8 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -731,7 +731,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) {