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"
/>
Surround
+
+
@@ -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",