diff --git a/assets/config_menu/sound.rml b/assets/config_menu/sound.rml index d95f8a0..169d3c0 100644 --- a/assets/config_menu/sound.rml +++ b/assets/config_menu/sound.rml @@ -117,11 +117,40 @@ data-checked="audio_mode" value="Surround" id="audio_surround" - style="nav-up: #lhb_on" + style="nav-up: #lhb_on; nav-down: #enhanced_surround_on" /> + +
+ +
+ + + + + +
+
@@ -137,6 +166,9 @@

Sets the audio output mode. Stereo outputs standard stereo audio. Mono outputs mono audio. Headphones optimizes audio for headphone listening. Surround enables 5.1 surround sound output using matrix decoding.

+

+ When enabled with Surround mode, adds pan-based rear channel separation. Sounds panned left go to the rear-left speaker, sounds panned right go to the rear-right speaker, creating improved spatial separation. +

diff --git a/include/zelda_config.h b/include/zelda_config.h index 4b1e727..1e64f8e 100644 --- a/include/zelda_config.h +++ b/include/zelda_config.h @@ -103,6 +103,10 @@ namespace zelda64 { {zelda64::AudioMode::Surround, "Surround"} }); + // Enhanced surround - adds pan-based rear channel separation + bool get_enhanced_surround_enabled(); + void set_enhanced_surround_enabled(bool enabled); + AudioMode get_audio_mode(); void set_audio_mode(AudioMode mode); diff --git a/patches/sound.h b/patches/sound.h index e194559..50d1dc2 100644 --- a/patches/sound.h +++ b/patches/sound.h @@ -10,5 +10,6 @@ DECLARE_FUNC(u32, recomp_get_low_health_beeps_enabled); // Audio channel settings: 0 = Stereo, 1 = 5.1 Matrix, 2 = 5.1 Raw DECLARE_FUNC(void, recomp_set_audio_channels, s32 channels); DECLARE_FUNC(s32, recomp_get_audio_channels); +DECLARE_FUNC(s32, recomp_get_enhanced_surround_enabled); #endif diff --git a/patches/sound_patches.c b/patches/sound_patches.c index b33ace2..9cc9bee 100644 --- a/patches/sound_patches.c +++ b/patches/sound_patches.c @@ -408,3 +408,96 @@ RECOMP_PATCH void Audio_SetFileSelectSettings(s8 audioSetting) { SEQCMD_SET_SOUND_MODE(soundMode); } + +// ============================================================================ +// Enhanced Surround Sound - Pan-based RL/RR steering +// ============================================================================ + +// DMEM address constants for audio synthesis (from synthesis.c) +#define DMEM_SURROUND_TEMP 0x4B0 +#define DMEM_HAAS_TEMP 0x5B0 +#define DMEM_LEFT_CH 0x930 +#define DMEM_RIGHT_CH 0xAD0 +#define DMEM_WET_LEFT_CH 0xC70 +#define DMEM_WET_RIGHT_CH 0xE10 + +// Forward declarations +void AudioSynth_DMemMove(Acmd* cmd, s32 dmemIn, s32 dmemOut, size_t size); +extern f32 gDefaultPanVolume[]; + +// @recomp Patched to add enhanced surround with pan-based RL/RR channel steering +// When enhanced surround is enabled, sounds panned left go more to Rear Left, +// and sounds panned right go more to Rear Right, creating better spatial separation. +RECOMP_PATCH Acmd* AudioSynth_ApplySurroundEffect(Acmd* cmd, NoteSampleState* sampleState, NoteSynthesisState* synthState, + s32 numSamplesPerUpdate, s32 haasDmem, s32 flags) { + s32 wetGain; + u16 dryGain; + s64 dmem = DMEM_SURROUND_TEMP; + f32 decayGain; + + AudioSynth_DMemMove(cmd++, haasDmem, DMEM_HAAS_TEMP, numSamplesPerUpdate * SAMPLE_SIZE); + dryGain = synthState->surroundEffectGain; + + if (flags == A_INIT) { + aClearBuffer(cmd++, dmem, sizeof(synthState->synthesisBuffers->surroundEffectState)); + synthState->surroundEffectGain = 0; + } else { + aLoadBuffer(cmd++, synthState->synthesisBuffers->surroundEffectState, dmem, + sizeof(synthState->synthesisBuffers->surroundEffectState)); + + // @recomp Check if enhanced surround is enabled for pan-based RL/RR steering + if (recomp_get_enhanced_surround_enabled()) { + // === Matrix surround encoding: steer surround to RL or RR based on pan === + // Calculate pan position: 0.0 = full left, 0.5 = center, 1.0 = full right + f32 sumVol = sampleState->targetVolLeft + sampleState->targetVolRight; + f32 panPosition = 0.5f; // default: center (mono surround) + if (sumVol > 0.0f) { + panPosition = (f32)sampleState->targetVolRight / sumVol; + } + + // The L/R balance determines RL vs RR steering: + // - L dominant (leftGain > rightGain): surround goes more to Rear Left + // - R dominant (rightGain > leftGain): surround goes more to Rear Right + // - Equal: mono surround to both + s16 leftGain = (s16)(dryGain * (1.0f - panPosition)); + s16 rightGain = (s16)(dryGain * panPosition); + + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, leftGain, dmem, DMEM_LEFT_CH); + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, (rightGain ^ 0xFFFF), dmem, DMEM_RIGHT_CH); + + wetGain = (dryGain * synthState->curReverbVol) >> 7; + s16 wetLeftGain = (s16)(wetGain * (1.0f - panPosition)); + s16 wetRightGain = (s16)(wetGain * panPosition); + + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, wetLeftGain, dmem, DMEM_WET_LEFT_CH); + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, (wetRightGain ^ 0xFFFF), dmem, DMEM_WET_RIGHT_CH); + // === End matrix surround encoding === + } else { + // Original behavior: mono surround to both channels + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, dryGain, dmem, DMEM_LEFT_CH); + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, (dryGain ^ 0xFFFF), dmem, DMEM_RIGHT_CH); + + wetGain = (dryGain * synthState->curReverbVol) >> 7; + + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, wetGain, dmem, DMEM_WET_LEFT_CH); + aMix(cmd++, (numSamplesPerUpdate * (s32)SAMPLE_SIZE) >> 4, (wetGain ^ 0xFFFF), dmem, DMEM_WET_RIGHT_CH); + } + } + + aSaveBuffer(cmd++, DMEM_SURROUND_TEMP + (numSamplesPerUpdate * SAMPLE_SIZE), + synthState->synthesisBuffers->surroundEffectState, + sizeof(synthState->synthesisBuffers->surroundEffectState)); + + decayGain = (sampleState->targetVolLeft + sampleState->targetVolRight) * (1.0f / 0x2000); + + if (decayGain > 1.0f) { + decayGain = 1.0f; + } + + decayGain = decayGain * gDefaultPanVolume[127 - sampleState->surroundEffectIndex]; + synthState->surroundEffectGain = ((decayGain * 0x7FFF) + synthState->surroundEffectGain) / 2; + + AudioSynth_DMemMove(cmd++, DMEM_HAAS_TEMP, haasDmem, numSamplesPerUpdate * SAMPLE_SIZE); + + return cmd; +} diff --git a/patches/syms.ld b/patches/syms.ld index e076507..a1de75f 100644 --- a/patches/syms.ld +++ b/patches/syms.ld @@ -55,3 +55,4 @@ recomp_get_actor_data = 0x8F0000D0; recomp_get_actor_spawn_index = 0x8F0000D4; recomp_set_audio_channels = 0x8F0000D8; recomp_get_audio_channels = 0x8F0000DC; +recomp_get_enhanced_surround_enabled = 0x8F0000E0; diff --git a/src/game/config.cpp b/src/game/config.cpp index 063aa6b..ac9143c 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -461,6 +461,7 @@ bool save_sound_config(const std::filesystem::path& path) { config_json["bgm_volume"] = zelda64::get_bgm_volume(); config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled(); config_json["audio_mode"] = zelda64::get_audio_mode(); + config_json["enhanced_surround"] = zelda64::get_enhanced_surround_enabled(); return save_json_with_backups(path, config_json); } @@ -476,6 +477,7 @@ bool load_sound_config(const std::filesystem::path& path) { call_if_key_exists(zelda64::set_bgm_volume, config_json, "bgm_volume"); call_if_key_exists(zelda64::set_low_health_beeps_enabled, config_json, "low_health_beeps"); call_if_key_exists(zelda64::set_audio_mode, config_json, "audio_mode"); + call_if_key_exists(zelda64::set_enhanced_surround_enabled, config_json, "enhanced_surround"); return true; } diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp index 4567789..9980bec 100644 --- a/src/game/recomp_api.cpp +++ b/src/game/recomp_api.cpp @@ -196,3 +196,7 @@ extern "C" void recomp_set_audio_channels(uint8_t* rdram, recomp_context* ctx) { extern "C" void recomp_get_audio_channels(uint8_t* rdram, recomp_context* ctx) { _return(ctx, static_cast(get_audio_channels())); } + +extern "C" void recomp_get_enhanced_surround_enabled(uint8_t* rdram, recomp_context* ctx) { + _return(ctx, zelda64::get_enhanced_surround_enabled() ? 1 : 0); +} diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index 5ea4c0e..8d729ba 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -366,11 +366,13 @@ struct SoundOptionsContext { std::atomic bgm_volume; std::atomic low_health_beeps_enabled; // RmlUi doesn't seem to like "true"/"false" strings for setting variants so an int is used here instead. zelda64::AudioMode audio_mode; // Audio output mode (Stereo, Mono, Headphones, Surround) + std::atomic enhanced_surround_enabled; // Pan-based rear channel separation void reset() { bgm_volume = 100; main_volume = 100; low_health_beeps_enabled = (int)true; audio_mode = zelda64::AudioMode::Stereo; + enhanced_surround_enabled = (int)false; } SoundOptionsContext() { reset(); @@ -428,7 +430,7 @@ void zelda64::set_audio_mode(zelda64::AudioMode mode) { if (sound_options_model_handle) { sound_options_model_handle.DirtyVariable("audio_mode"); } - // Update audio backend - only Surround mode uses 5.1 matrix decoding + // Update audio backend - Surround mode uses 5.1 matrix decoding set_audio_channels(mode == zelda64::AudioMode::Surround ? audioMatrix51 : audioStereo); } @@ -436,6 +438,17 @@ zelda64::AudioMode zelda64::get_audio_mode() { return sound_options_context.audio_mode; } +void zelda64::set_enhanced_surround_enabled(bool enabled) { + sound_options_context.enhanced_surround_enabled.store((int)enabled); + if (sound_options_model_handle) { + sound_options_model_handle.DirtyVariable("enhanced_surround_enabled"); + } +} + +bool zelda64::get_enhanced_surround_enabled() { + return (bool)sound_options_context.enhanced_surround_enabled.load(); +} + struct DebugContext { Rml::DataModelHandle model_handle; std::vector area_names; @@ -959,6 +972,7 @@ public: bind_atomic(constructor, sound_options_model_handle, "main_volume", &sound_options_context.main_volume); bind_atomic(constructor, sound_options_model_handle, "bgm_volume", &sound_options_context.bgm_volume); bind_atomic(constructor, sound_options_model_handle, "low_health_beeps_enabled", &sound_options_context.low_health_beeps_enabled); + bind_atomic(constructor, sound_options_model_handle, "enhanced_surround_enabled", &sound_options_context.enhanced_surround_enabled); // Custom binding for audio mode that calls the setter to update audio channels constructor.BindFunc("audio_mode",