From 93c8bcc2103998c91f48b6336e0a8f3ffbfd8d81 Mon Sep 17 00:00:00 2001 From: madeline Date: Fri, 1 May 2026 13:04:08 -0700 Subject: [PATCH] hrtf --- include/dusk/settings.h | 1 + .../include/JSystem/JAudio2/JAISeqMgr.h | 3 + libs/JSystem/src/JAudio2/JASChannel.cpp | 10 ++- src/Z2AudioLib/Z2Audience.cpp | 19 ++++- src/dusk/audio/DuskDsp.cpp | 54 +++++++++++++ src/dusk/audio/DuskDsp.hpp | 2 + src/dusk/imgui/ImGuiAudio.cpp | 75 ++++++++++++++++++- src/dusk/imgui/ImGuiMenuGame.cpp | 6 ++ src/dusk/settings.cpp | 2 + src/m_Do/m_Do_main.cpp | 2 + 10 files changed, 171 insertions(+), 3 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index e426e301fa..e6fb0bd32b 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -55,6 +55,7 @@ struct UserSettings { ConfigVar soundEffectsVolume; ConfigVar fanfareVolume; ConfigVar enableReverb; + ConfigVar enableHrtf; } audio; // Game settings diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h b/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h index 87c85f316f..6f6d29ca98 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h @@ -59,6 +59,9 @@ public: bool isActive() const { return mSeqList.getNumLinks() != 0; } int getNumActiveSeqs() const { return mSeqList.getNumLinks(); } void pause(bool paused) { mActivity.field_0x0.flags.flag2 = paused; } + #if TARGET_PC + JSUList* getSeqList() { return &mSeqList; } + #endif private: /* 0x08 */ JAIAudience* mAudience; diff --git a/libs/JSystem/src/JAudio2/JASChannel.cpp b/libs/JSystem/src/JAudio2/JASChannel.cpp index 61fe80a098..758167cc5c 100644 --- a/libs/JSystem/src/JAudio2/JASChannel.cpp +++ b/libs/JSystem/src/JAudio2/JASChannel.cpp @@ -1,6 +1,9 @@ #include "JSystem/JSystem.h" // IWYU pragma: keep #include "JSystem/JAudio2/JASChannel.h" +#if TARGET_PC +#include "dusk/audio/DuskDsp.hpp" +#endif #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASCalc.h" #include "JSystem/JAudio2/JASDriverIF.h" @@ -170,7 +173,12 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo f32 pan = 0.5f; f32 dolby = 0.0f; - switch (JASDriver::getOutputMode()) { +#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; case JAS_OUTPUT_STEREO: diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp index 5bb91eef31..93b3f701c6 100644 --- a/src/Z2AudioLib/Z2Audience.cpp +++ b/src/Z2AudioLib/Z2Audience.cpp @@ -1,5 +1,9 @@ #include "Z2AudioLib/Z2Audience.h" #include "Z2AudioLib/Z2SoundInfo.h" +#if TARGET_PC +#include "dusk/audio/DuskDsp.hpp" +#include +#endif #include "Z2AudioLib/Z2Calc.h" #include "Z2AudioLib/Z2Param.h" #include "JSystem/JAudio2/JAISound.h" @@ -734,9 +738,22 @@ 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) { + // 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; + 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)); + } +#endif if (fVar1 > mSetting.field_0x48) { return 1.0f; - } + } if (fVar1 < mSetting.field_0x44) { return 0.0f; diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index f576a5d0f1..697371702f 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -48,6 +48,20 @@ f32 dusk::audio::MasterVolume = 1.0f; f32 dusk::audio::PrevMasterVolume = 1.0f; bool dusk::audio::EnableReverb = true; bool dusk::audio::DumpAudio = false; +bool dusk::audio::EnableHrtf = false; +f32 dusk::audio::HrtfGain = 0.5f; + + +// 3dB at 5kHz. +static constexpr f32 HRTF_LP_K = 0.75f; +static constexpr f32 HRTF_ALLPASS_G = 0.3f; +// Front never drops below (1 - HRTF_EXTRACT_MAX). +static constexpr f32 HRTF_EXTRACT_MAX = 0.6f; + +static f32 sHrtfLp1 = 0.0f; +static f32 sHrtfLp2 = 0.0f; +static f32 sHrtfApIn1 = 0.0f; +static f32 sHrtfApOut1 = 0.0f; /** * Validate that a DSP channel's format is actually something we know how to play. @@ -283,6 +297,9 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { 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]; @@ -324,6 +341,21 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { } } + 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++) { @@ -349,6 +381,28 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { 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); } diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 8000e627a1..cfcbfe3f46 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -133,4 +133,6 @@ namespace dusk::audio { extern f32 PrevMasterVolume; extern bool EnableReverb; extern bool DumpAudio; + extern bool EnableHrtf; + extern f32 HrtfGain; } diff --git a/src/dusk/imgui/ImGuiAudio.cpp b/src/dusk/imgui/ImGuiAudio.cpp index 9abe4c5261..6bddc6f07c 100644 --- a/src/dusk/imgui/ImGuiAudio.cpp +++ b/src/dusk/imgui/ImGuiAudio.cpp @@ -1,5 +1,7 @@ #include "ImGuiConsole.hpp" #include "ImGuiMenuTools.hpp" +#include +#include "JSystem/JAudio2/JAISeq.h" #include "JSystem/JAudio2/JAISeMgr.h" #include "JSystem/JAudio2/JAISeqMgr.h" #include "JSystem/JAudio2/JAIStreamMgr.h" @@ -15,6 +17,24 @@ static std::array lastResetCounts = {}; static bool sortUpdateCount = true; +static void DrawDirectionGauge(float pan, float dolby) { + constexpr float R = 20.0f; + constexpr float SIZE = R * 2.0f + 4.0f; + + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImGui::Dummy(ImVec2(SIZE, SIZE)); + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 c = ImVec2(origin.x + SIZE * 0.5f, origin.y + SIZE * 0.5f); + + dl->AddCircle(c, R, IM_COL32(90, 90, 90, 255), 32); + + float dx = (pan - 0.5f) * 2.0f; + float dy = dolby * 2.0f - 1.0f; + float len = sqrtf(dx * dx + dy * dy); + if (len > 1.0f) { dx /= len; dy /= len; } + dl->AddLine(c, ImVec2(c.x + dx * R, c.y + dy * R), IM_COL32(255, 200, 50, 255), 1.5f); +} + static void DisplayDspChannel(int i) { using namespace dusk::audio; @@ -52,8 +72,10 @@ static void DisplayDspChannel(int i) { auto fxMix = (channel.mAutoMixerFxMix >> 8) / 127.5f; auto volume = VolumeFromU16(channel.mAutoMixerVolume); auto pitch = channel.mPitch / 4096.0f; + DrawDirectionGauge(pan, dolby); + ImGui::SameLine(); ImGui::Text( - "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f, pitch %f)", + "pan: %.2f dolby: %.2f\nfx: %.2f vol: %.2f pitch: %.2f", pan, dolby, fxMix, volume, pitch); } else { ImGui::Text( @@ -183,6 +205,10 @@ static void ShowAllJAISes() { if (ImGui::Button("Pause All")) { category->pause(true); } + ImGui::SameLine(); + if (ImGui::Button("Resume All")) { + category->pause(false); + } for (auto seLink = category->getSeList()->getFirst(); seLink != nullptr; seLink = seLink->getNext()) { const auto se = seLink->getObject(); @@ -196,6 +222,33 @@ static void ShowAllJAISes() { } +static void ShowSeqTracks(JAISeq& seq) { + JASTrack& root = seq.inner_.outputTrack; + + for (int group = 0; group < 2; group++) { + JASTrack* groupTrack = root.getChild(group); + if (groupTrack == nullptr) { + continue; + } + + for (int j = 0; j < JASTrack::MAX_CHILDREN; j++) { + JASTrack* track = groupTrack->getChild(j); + if (track == nullptr) { + continue; + } + + int trackIdx = group * 16 + j; + char label[64]; + snprintf(label, sizeof(label), "Track %d (bank %hu, prog %hu)##%p", + trackIdx, track->getBankNumber(), track->getProgNumber(), track); + bool muted = track->mFlags.mute; + if (ImGui::Checkbox(label, &muted)) { + track->mute(muted); + } + } + } +} + static void ShowAllJAISeqs() { auto& mgr = *JAISeqMgr::getInstance(); @@ -206,6 +259,26 @@ static void ShowAllJAISeqs() { if (ImGui::Button("Unpause")) { mgr.pause(false); } + + ImGui::Text("Active sequences: %d", mgr.getNumActiveSeqs()); + + auto* seqList = mgr.getSeqList(); + for (auto* link = seqList->getFirst(); link != nullptr; link = link->getNext()) { + JAISeq* seq = link->getObject(); + if (seq == nullptr) { + continue; + } + + char buf[32]; + snprintf(buf, sizeof(buf), "%p", seq); + + if (ImGui::BeginChild(buf, ImVec2(), ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) { + ImGui::Text("Seq [%p]", seq); + ShowSeqTracks(*seq); + } + + ImGui::EndChild(); + } } void dusk::ImGuiMenuTools::ShowAudioDebug() { diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index deced34b8e..cf72e17a9a 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -397,6 +397,12 @@ namespace dusk { dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); } + if (config::ImGuiCheckbox("Spatial Sound", getSettings().audio.enableHrtf)) { + dusk::audio::EnableHrtf = getSettings().audio.enableHrtf; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Emulate surround sound via HRTF (for headphone use only)!"); + } ImGui::SeparatorText("Tweaks"); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 2eac6d5a19..6198e789d1 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -17,6 +17,7 @@ UserSettings g_userSettings = { .soundEffectsVolume {"audio.soundEffectsVolume", 100}, .fanfareVolume {"audio.fanfareVolume", 100}, .enableReverb {"audio.enableReverb", true}, + .enableHrtf {"audio.enableHrtf", false}, }, .game = { @@ -133,6 +134,7 @@ void registerSettings() { Register(g_userSettings.audio.soundEffectsVolume); Register(g_userSettings.audio.fanfareVolume); Register(g_userSettings.audio.enableReverb); + Register(g_userSettings.audio.enableHrtf); // Game Register(g_userSettings.game.language); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index d25debb38e..3211fc8873 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -68,6 +68,7 @@ #include "cxxopts.hpp" #include "d/actor/d_a_movie_player.h" #include "dusk/audio/DuskAudioSystem.h" +#include "dusk/audio/DuskDsp.hpp" #include "dusk/config.hpp" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" @@ -577,6 +578,7 @@ int game_main(int argc, char* argv[]) { dusk::audio::SetMasterVolume(dusk::getSettings().audio.masterVolume / 100.0f); dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb); + dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf; std::string dvd_path; bool dvd_opened = false;