From f5ce58ca7f818bfc7100929381324b86923ae646 Mon Sep 17 00:00:00 2001 From: madeline Date: Sat, 4 Apr 2026 01:38:28 -0700 Subject: [PATCH] reverb proof of concept --- CMakeLists.txt | 8 +- include/dusk/settings.h | 2 +- libs/freeverb/CMakeLists.txt | 13 ++ libs/freeverb/allpass.cpp | 36 +++++ libs/freeverb/allpass.hpp | 48 ++++++ libs/freeverb/comb.cpp | 48 ++++++ libs/freeverb/comb.hpp | 55 +++++++ libs/freeverb/denormals.h | 15 ++ libs/freeverb/revmodel.cpp | 252 +++++++++++++++++++++++++++++++ libs/freeverb/revmodel.hpp | 87 +++++++++++ libs/freeverb/tuning.h | 60 ++++++++ src/dusk/audio/DuskDsp.cpp | 54 +++++-- src/dusk/audio/DuskDsp.hpp | 4 + src/dusk/imgui/ImGuiMenuGame.cpp | 4 +- src/dusk/settings.cpp | 1 + 15 files changed, 668 insertions(+), 19 deletions(-) create mode 100644 libs/freeverb/CMakeLists.txt create mode 100644 libs/freeverb/allpass.cpp create mode 100644 libs/freeverb/allpass.hpp create mode 100644 libs/freeverb/comb.cpp create mode 100644 libs/freeverb/comb.hpp create mode 100644 libs/freeverb/denormals.h create mode 100644 libs/freeverb/revmodel.cpp create mode 100644 libs/freeverb/revmodel.hpp create mode 100644 libs/freeverb/tuning.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 248dfcf6b1..272ff76db3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,8 @@ set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE) set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE) add_subdirectory(extern/aurora EXCLUDE_FROM_ALL) +add_subdirectory(libs/freeverb) + option(DUSK_BUILD_WARNINGS "If off, compiler warnings will be suppressed") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) @@ -108,14 +110,14 @@ target_include_directories(game_debug PUBLIC extern ${CMAKE_SOURCE_DIR}/build/${DUSK_TP_VERSION}/include build/${DUSK_TP_VERSION}/include) -target_link_libraries(game_debug PUBLIC aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::card) +target_link_libraries(game_debug PUBLIC aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::card freeverb) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) add_library(game SHARED ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${SSYSTEM_FILES} ${JSYSTEM_FILES} ${REL_FILES} ${DUSK_FILES} ${DOLPHIN_FILES} src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiAudio.cpp) -target_link_libraries(game PRIVATE game_debug cxxopts::cxxopts absl::flat_hash_map) +target_link_libraries(game PRIVATE game_debug cxxopts::cxxopts absl::flat_hash_map freeverb) if (DUSK_MOVIE_SUPPORT_REAL) if (TARGET libjpeg-turbo::turbojpeg-static) message(STATUS "dusk: Linking against libjpeg-turbo static library") @@ -132,7 +134,7 @@ target_precompile_headers(game PRIVATE "$<$:${CMAKE_SOURCE add_executable(dusk src/dusk/main.cpp) target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) target_include_directories(dusk PRIVATE include) -target_link_libraries(dusk PRIVATE game aurora::main) +target_link_libraries(dusk PRIVATE game aurora::main freeverb) add_custom_command(TARGET dusk POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/include/dusk/settings.h b/include/dusk/settings.h index daba1287d8..64fee289ba 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -20,6 +20,7 @@ struct UserSettings { float subMusicVolume; float soundEffectsVolume; float fanfareVolume; + bool enableReverb; } audio; // Game settings @@ -68,4 +69,3 @@ TransientSettings& getTransientSettings(); } #endif // DUSK_CONFIG_H - diff --git a/libs/freeverb/CMakeLists.txt b/libs/freeverb/CMakeLists.txt new file mode 100644 index 0000000000..c4f4d4ed95 --- /dev/null +++ b/libs/freeverb/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10) + +project(freeverb LANGUAGES CXX) + +add_library(freeverb + comb.cpp + allpass.cpp + revmodel.cpp +) + +target_include_directories(freeverb PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/libs/freeverb/allpass.cpp b/libs/freeverb/allpass.cpp new file mode 100644 index 0000000000..5d80eda2bd --- /dev/null +++ b/libs/freeverb/allpass.cpp @@ -0,0 +1,36 @@ +// Allpass filter implementation +// +// Written by Jezar at Dreampoint, June 2000 +// http://www.dreampoint.co.uk +// This code is public domain + +#include "allpass.hpp" + +allpass::allpass() +{ + bufidx = 0; +} + +void allpass::setbuffer(float *buf, int size) +{ + buffer = buf; + bufsize = size; +} + +void allpass::mute() +{ + for (int i=0; i=bufsize) bufidx = 0; + + return output; +} + +#endif//_allpass + +//ends \ No newline at end of file diff --git a/libs/freeverb/comb.cpp b/libs/freeverb/comb.cpp new file mode 100644 index 0000000000..c05f5069c8 --- /dev/null +++ b/libs/freeverb/comb.cpp @@ -0,0 +1,48 @@ +// Comb filter implementation +// +// Written by Jezar at Dreampoint, June 2000 +// http://www.dreampoint.co.uk +// This code is public domain + +#include "comb.hpp" + +comb::comb() +{ + filterstore = 0; + bufidx = 0; +} + +void comb::setbuffer(float *buf, int size) +{ + buffer = buf; + bufsize = size; +} + +void comb::mute() +{ + for (int i=0; i=bufsize) bufidx = 0; + + return output; +} + +#endif //_comb_ + +//ends diff --git a/libs/freeverb/denormals.h b/libs/freeverb/denormals.h new file mode 100644 index 0000000000..f871412714 --- /dev/null +++ b/libs/freeverb/denormals.h @@ -0,0 +1,15 @@ +// Macro for killing denormalled numbers +// +// Written by Jezar at Dreampoint, June 2000 +// http://www.dreampoint.co.uk +// Based on IS_DENORMAL macro by Jon Watte +// This code is public domain + +#ifndef _denormals_ +#define _denormals_ + +#define undenormalise(sample) if(((*(unsigned int*)&sample)&0x7f800000)==0) sample=0.0f + +#endif//_denormals_ + +//ends diff --git a/libs/freeverb/revmodel.cpp b/libs/freeverb/revmodel.cpp new file mode 100644 index 0000000000..feaa3bc04c --- /dev/null +++ b/libs/freeverb/revmodel.cpp @@ -0,0 +1,252 @@ +// Reverb model implementation +// +// Written by Jezar at Dreampoint, June 2000 +// http://www.dreampoint.co.uk +// This code is public domain + +#include "revmodel.hpp" + +revmodel::revmodel() +{ + // Tie the components to their buffers + combL[0].setbuffer(bufcombL1,combtuningL1); + combR[0].setbuffer(bufcombR1,combtuningR1); + combL[1].setbuffer(bufcombL2,combtuningL2); + combR[1].setbuffer(bufcombR2,combtuningR2); + combL[2].setbuffer(bufcombL3,combtuningL3); + combR[2].setbuffer(bufcombR3,combtuningR3); + combL[3].setbuffer(bufcombL4,combtuningL4); + combR[3].setbuffer(bufcombR4,combtuningR4); + combL[4].setbuffer(bufcombL5,combtuningL5); + combR[4].setbuffer(bufcombR5,combtuningR5); + combL[5].setbuffer(bufcombL6,combtuningL6); + combR[5].setbuffer(bufcombR6,combtuningR6); + combL[6].setbuffer(bufcombL7,combtuningL7); + combR[6].setbuffer(bufcombR7,combtuningR7); + combL[7].setbuffer(bufcombL8,combtuningL8); + combR[7].setbuffer(bufcombR8,combtuningR8); + allpassL[0].setbuffer(bufallpassL1,allpasstuningL1); + allpassR[0].setbuffer(bufallpassR1,allpasstuningR1); + allpassL[1].setbuffer(bufallpassL2,allpasstuningL2); + allpassR[1].setbuffer(bufallpassR2,allpasstuningR2); + allpassL[2].setbuffer(bufallpassL3,allpasstuningL3); + allpassR[2].setbuffer(bufallpassR3,allpasstuningR3); + allpassL[3].setbuffer(bufallpassL4,allpasstuningL4); + allpassR[3].setbuffer(bufallpassR4,allpasstuningR4); + + // Set default values + allpassL[0].setfeedback(0.5f); + allpassR[0].setfeedback(0.5f); + allpassL[1].setfeedback(0.5f); + allpassR[1].setfeedback(0.5f); + allpassL[2].setfeedback(0.5f); + allpassR[2].setfeedback(0.5f); + allpassL[3].setfeedback(0.5f); + allpassR[3].setfeedback(0.5f); + setwet(initialwet); + setroomsize(initialroom); + setdry(initialdry); + setdamp(initialdamp); + setwidth(initialwidth); + setmode(initialmode); + + // Buffer will be full of rubbish - so we MUST mute them + mute(); +} + +void revmodel::mute() +{ + if (getmode() >= freezemode) + return; + + for (int i=0;i 0) + { + outL = outR = 0; + input = (*inputL + *inputR) * gain; + + // Accumulate comb filters in parallel + for(int i=0; i 0) + { + outL = outR = 0; + input = (*inputL + *inputR) * gain; + + // Accumulate comb filters in parallel + for(int i=0; i= freezemode) + { + roomsize1 = 1; + damp1 = 0; + gain = muted; + } + else + { + roomsize1 = roomsize; + damp1 = damp; + gain = fixedgain; + } + + for(i=0; i= freezemode) + return 1; + else + return 0; +} + +//ends diff --git a/libs/freeverb/revmodel.hpp b/libs/freeverb/revmodel.hpp new file mode 100644 index 0000000000..10fe7c67d5 --- /dev/null +++ b/libs/freeverb/revmodel.hpp @@ -0,0 +1,87 @@ +// Reverb model declaration +// +// Written by Jezar at Dreampoint, June 2000 +// http://www.dreampoint.co.uk +// This code is public domain + +#ifndef _revmodel_ +#define _revmodel_ + +#include "comb.hpp" +#include "allpass.hpp" +#include "tuning.h" + +class revmodel +{ +public: + revmodel(); + void mute(); + void processmix(float *inputL, float *inputR, float *outputL, float *outputR, long numsamples, int skip); + void processreplace(float *inputL, float *inputR, float *outputL, float *outputR, long numsamples, int skip); + void setroomsize(float value); + float getroomsize(); + void setdamp(float value); + float getdamp(); + void setwet(float value); + float getwet(); + void setdry(float value); + float getdry(); + void setwidth(float value); + float getwidth(); + void setmode(float value); + float getmode(); +private: + void update(); +private: + float gain; + float roomsize,roomsize1; + float damp,damp1; + float wet,wet1,wet2; + float dry; + float width; + float mode; + + // The following are all declared inline + // to remove the need for dynamic allocation + // with its subsequent error-checking messiness + + // Comb filters + comb combL[numcombs]; + comb combR[numcombs]; + + // Allpass filters + allpass allpassL[numallpasses]; + allpass allpassR[numallpasses]; + + // Buffers for the combs + float bufcombL1[combtuningL1]; + float bufcombR1[combtuningR1]; + float bufcombL2[combtuningL2]; + float bufcombR2[combtuningR2]; + float bufcombL3[combtuningL3]; + float bufcombR3[combtuningR3]; + float bufcombL4[combtuningL4]; + float bufcombR4[combtuningR4]; + float bufcombL5[combtuningL5]; + float bufcombR5[combtuningR5]; + float bufcombL6[combtuningL6]; + float bufcombR6[combtuningR6]; + float bufcombL7[combtuningL7]; + float bufcombR7[combtuningR7]; + float bufcombL8[combtuningL8]; + float bufcombR8[combtuningR8]; + + // Buffers for the allpasses + float bufallpassL1[allpasstuningL1]; + float bufallpassR1[allpasstuningR1]; + float bufallpassL2[allpasstuningL2]; + float bufallpassR2[allpasstuningR2]; + float bufallpassL3[allpasstuningL3]; + float bufallpassR3[allpasstuningR3]; + float bufallpassL4[allpasstuningL4]; + float bufallpassR4[allpasstuningR4]; +}; + +#endif//_revmodel_ + +//ends \ No newline at end of file diff --git a/libs/freeverb/tuning.h b/libs/freeverb/tuning.h new file mode 100644 index 0000000000..baaa9ce004 --- /dev/null +++ b/libs/freeverb/tuning.h @@ -0,0 +1,60 @@ +// Reverb model tuning values +// +// Written by Jezar at Dreampoint, June 2000 +// http://www.dreampoint.co.uk +// This code is public domain + +#ifndef _tuning_ +#define _tuning_ + +const int numcombs = 8; +const int numallpasses = 4; +const float muted = 0; +const float fixedgain = 0.015f; +const float scalewet = 3; +const float scaledry = 2; +const float scaledamp = 0.4f; +const float scaleroom = 0.28f; +const float offsetroom = 0.7f; +const float initialroom = 0.5f; +const float initialdamp = 0.5f; +const float initialwet = 1/scalewet; +const float initialdry = 0; +const float initialwidth = 1; +const float initialmode = 0; +const float freezemode = 0.5f; +const int stereospread = 23; + +// These values assume 44.1KHz sample rate +// they will probably be OK for 48KHz sample rate +// but would need scaling for 96KHz (or other) sample rates. +// The values were obtained by listening tests. +const int combtuningL1 = 1116; +const int combtuningR1 = 1116+stereospread; +const int combtuningL2 = 1188; +const int combtuningR2 = 1188+stereospread; +const int combtuningL3 = 1277; +const int combtuningR3 = 1277+stereospread; +const int combtuningL4 = 1356; +const int combtuningR4 = 1356+stereospread; +const int combtuningL5 = 1422; +const int combtuningR5 = 1422+stereospread; +const int combtuningL6 = 1491; +const int combtuningR6 = 1491+stereospread; +const int combtuningL7 = 1557; +const int combtuningR7 = 1557+stereospread; +const int combtuningL8 = 1617; +const int combtuningR8 = 1617+stereospread; +const int allpasstuningL1 = 556; +const int allpasstuningR1 = 556+stereospread; +const int allpasstuningL2 = 441; +const int allpasstuningR2 = 441+stereospread; +const int allpasstuningL3 = 341; +const int allpasstuningR3 = 341+stereospread; +const int allpasstuningL4 = 225; +const int allpasstuningR4 = 225+stereospread; + +#endif//_tuning_ + +//ends + diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 7cf1c44cd8..fd1ae66620 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -19,6 +19,7 @@ ChannelAuxData dusk::audio::ChannelAux[DSP_CHANNELS] = {}; f32 dusk::audio::MasterVolume = 1.0f; f32 dusk::audio::PrevMasterVolume = 1.0f; +bool dusk::audio::EnableReverb = true; /** * Validate that a DSP channel's format is actually something we know how to play. @@ -112,32 +113,47 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { auto& channel = channels[i]; auto& channelAux = ChannelAux[i]; - if (!channel.mIsActive) { - continue; - } + bool skipRender = false; - if (channel.mPauseFlag) { + if (!channel.mIsActive) { + skipRender = true; + } + 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; + skipRender = true; } - - if (channel.mForcedStop) { + else if (channel.mForcedStop) { channel.mIsFinished = true; - continue; + skipRender = true; } - - if (channel.mWaveAramAddress == 0) { + else if (channel.mWaveAramAddress == 0) { // I think these are oscillator channels? Not backed by audio. // No idea how to implement these yet, so skip them. channel.mIsFinished = true; - continue; + skipRender = true; } - ValidateChannel(channel); - OutputSubframe channelSubframe = {}; - RenderChannel(channel, channelAux, channelSubframe); + + if (!skipRender) { + ValidateChannel(channel); + RenderChannel(channel, channelAux, channelSubframe); + // todo figure out an actual way to convert this accurately to dry/wet + channelAux.reverb.setwet(((channel.mAutoMixerFxMix >> 8) / 255.0f)); + channelAux.reverb.setdry(1.0f - channelAux.reverb.getwet()); + } + + if (EnableReverb) { + channelAux.reverb.processreplace( + channelSubframe.channels[0].data(), + channelSubframe.channels[1].data(), + channelSubframe.channels[0].data(), + channelSubframe.channels[1].data(), + DSP_SUBFRAME_SIZE, + 1 + ); + } for (int o = 0; o < subframe.channels.size(); o++) { MixSubframe(subframe.channels[o], channelSubframe.channels[o]); @@ -463,6 +479,16 @@ static void RenderChannel( } void dusk::audio::DspInit() { + for (int i = 0; i < DSP_CHANNELS; i++) { + auto& channelAux = ChannelAux[i]; + channelAux.reverb.setwet(0.0f); + channelAux.reverb.setdry(1.0f); + channelAux.reverb.setroomsize(0.2f); + channelAux.reverb.setdamp(0.3f); + channelAux.reverb.setwidth(1.0f); + channelAux.reverb.setmode(0.0f); + channelAux.reverb.mute(); + } } void dusk::audio::ApplyVolume( diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 35b6ccabc9..3c414375f3 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -8,6 +8,8 @@ #include "SDL3/SDL_audio.h" #include +#include "revmodel.hpp" + // ReSharper disable once CppUnusedIncludeDirective #include "global.h" @@ -29,6 +31,7 @@ namespace dusk::audio { // Used for debugging tools. u32 resetCount; + revmodel reverb; /** * Previous volume values, per output channel. @@ -118,4 +121,5 @@ namespace dusk::audio { extern f32 MasterVolume; extern f32 PrevMasterVolume; + extern bool EnableReverb; } diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 91aa0314df..7bdea47862 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -6,6 +6,7 @@ #include #include "JSystem/JUtility/JUTGamePad.h" +#include "dusk/audio/DuskDsp.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/hotkeys.h" #include "dusk/settings.h" @@ -34,7 +35,7 @@ namespace dusk { if (ImGui::BeginMenu("Audio")) { ImGui::Text("Master Volume"); ImGui::SliderFloat("##masterVolume", &getSettings().audio.masterVolume, 0.0f, 1.0f, ""); - + ImGui::Checkbox("Enable Reverb", &getSettings().audio.enableReverb); /* // TODO: implement additional settings ImGui::Text("Main Music Volume"); @@ -55,6 +56,7 @@ namespace dusk { */ audio::SetMasterVolume(getSettings().audio.masterVolume); + audio::EnableReverb = getSettings().audio.enableReverb; ImGui::EndMenu(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 9a5bb20c8c..ed2071ca30 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -17,6 +17,7 @@ UserSettings g_userSettings = { .subMusicVolume = 1.0f, .soundEffectsVolume = 1.0f, .fanfareVolume = 1.0f, + .enableReverb = true }, // Game settings