From dacf76962e061b738fcd2ee61d44cd2fcaf7927f Mon Sep 17 00:00:00 2001
From: Tortuga Veloz
Date: Mon, 9 Feb 2026 12:01:09 +0100
Subject: [PATCH 1/8] Add surround option.
---
CMakeLists.txt | 2 +
assets/config_menu/sound.rml | 36 +++-
include/audio_channels.h | 21 +++
include/zelda_sound.h | 2 +
patches/skip_sos.c | 2 +-
patches/sound.h | 5 +
patches/sound_patches.c | 43 +++++
patches/syms.ld | 2 +
src/audio/sound_matrix_decoder.cpp | 277 +++++++++++++++++++++++++++++
src/game/config.cpp | 2 +
src/game/recomp_api.cpp | 19 ++
src/main/main.cpp | 133 ++++++++++++--
src/ui/ui_config.cpp | 31 ++++
13 files changed, 555 insertions(+), 20 deletions(-)
create mode 100644 include/audio_channels.h
create mode 100644 src/audio/sound_matrix_decoder.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1cdda09..c0b70d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -165,6 +165,8 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/game/recomp_data_api.cpp
${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp
+ ${CMAKE_SOURCE_DIR}/src/audio/sound_matrix_decoder.cpp
+
${CMAKE_SOURCE_DIR}/src/ui/ui_renderer.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_state.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_launcher.cpp
diff --git a/assets/config_menu/sound.rml b/assets/config_menu/sound.rml
index badadce..30fa2c7 100644
--- a/assets/config_menu/sound.rml
+++ b/assets/config_menu/sound.rml
@@ -52,7 +52,7 @@
data-checked="low_health_beeps_enabled"
value="1"
id="lhb_on"
- style="nav-up: #bgm_volume_input"
+ style="nav-up: #bgm_volume_input; nav-down: #surround_on;"
/>
On
@@ -64,11 +64,40 @@
data-checked="low_health_beeps_enabled"
value="0"
id="lhb_off"
- style="nav-up: #bgm_volume_input"
+ style="nav-up: #bgm_volume_input; nav-down: #surround_on;"
/>
Off
+
+
@@ -81,6 +110,9 @@
Toggles whether or not the low-health beeping sound plays.
+
+ Enables 5.1 surround sound output using matrix decoding. Requires a surround sound system or virtual surround headphones.
+
diff --git a/include/audio_channels.h b/include/audio_channels.h
new file mode 100644
index 0000000..2dbcb22
--- /dev/null
+++ b/include/audio_channels.h
@@ -0,0 +1,21 @@
+#pragma once
+
+typedef enum AudioChannelsSetting {
+ audioStereo,
+ audioMatrix51,
+ audioRaw51,
+ audioMax
+} AudioChannelsSetting;
+
+inline const char* AudioChannelsSettingName(AudioChannelsSetting setting) {
+ switch (setting) {
+ case audioStereo:
+ return "Stereo";
+ case audioMatrix51:
+ return "5.1 Matrix";
+ case audioRaw51:
+ return "5.1 Raw";
+ default:
+ return "Unknown";
+ }
+}
diff --git a/include/zelda_sound.h b/include/zelda_sound.h
index 33bec01..fb1a652 100644
--- a/include/zelda_sound.h
+++ b/include/zelda_sound.h
@@ -9,6 +9,8 @@ namespace zelda64 {
int get_bgm_volume();
void set_low_health_beeps_enabled(bool enabled);
bool get_low_health_beeps_enabled();
+ void set_surround_sound_enabled(bool enabled);
+ bool get_surround_sound_enabled();
}
#endif
diff --git a/patches/skip_sos.c b/patches/skip_sos.c
index 3b1c0fa..0352d5f 100644
--- a/patches/skip_sos.c
+++ b/patches/skip_sos.c
@@ -24,6 +24,6 @@ RECOMP_PATCH void EnTest7_Update(Actor* thisx, PlayState* play) {
// @recomp Allow skipping the Song of Soaring cutscene.
Input* input = CONTROLLER1(&play->state);
if (CHECK_BTN_ANY(input->press.button, BTN_A | BTN_B) && (OWL_WARP_CS_GET_OCARINA_MODE(&this->actor) != ENTEST7_ARRIVE)) {
- func_80AF2350(thisx, play);
+ func_80AF2350(this, play);
}
}
\ No newline at end of file
diff --git a/patches/sound.h b/patches/sound.h
index 0e05637..e194559 100644
--- a/patches/sound.h
+++ b/patches/sound.h
@@ -6,4 +6,9 @@
DECLARE_FUNC(float, recomp_get_bgm_volume);
DECLARE_FUNC(u32, recomp_get_low_health_beeps_enabled);
+// Surround sound support
+// 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);
+
#endif
diff --git a/patches/sound_patches.c b/patches/sound_patches.c
index 504f9ec..453df4c 100644
--- a/patches/sound_patches.c
+++ b/patches/sound_patches.c
@@ -5,6 +5,13 @@
void AudioSeq_ProcessSeqCmd(u32 cmd);
void AudioThread_QueueCmd(u32 opArgs, void** data);
+// Audio channel settings (must match AudioChannelsSetting enum in native code)
+typedef enum {
+ AUDIO_CHANNELS_STEREO = 0,
+ AUDIO_CHANNELS_MATRIX_51 = 1,
+ AUDIO_CHANNELS_RAW_51 = 2
+} AudioChannelsSetting;
+
// Direct audio command (skips the queueing system)
#define SEQCMD_SET_SEQPLAYER_VOLUME_NOW(seqPlayerIndex, duration, volume) \
AudioSeq_ProcessSeqCmd((SEQCMD_OP_SET_SEQPLAYER_VOLUME << 28) | ((u8)(seqPlayerIndex) << 24) | \
@@ -360,4 +367,40 @@ RECOMP_PATCH void LifeMeter_UpdateSizeAndBeep(PlayState* play) {
}
}
}
+extern s8 sSoundMode;
+// @recomp Surround sound output is now controlled by the recomp's config menu
+// (Sound -> Surround Sound 5.1), not by the in-game audio setting.
+// This function still controls the game's internal sound processing mode.
+RECOMP_PATCH void Audio_SetFileSelectSettings(s8 audioSetting) {
+ s8 soundMode;
+
+ switch (audioSetting) {
+ case SAVE_AUDIO_STEREO:
+ soundMode = SOUNDMODE_STEREO;
+ sSoundMode = SOUNDMODE_STEREO;
+ break;
+
+ case SAVE_AUDIO_MONO:
+ soundMode = SOUNDMODE_MONO;
+ sSoundMode = SOUNDMODE_MONO;
+ break;
+
+ case SAVE_AUDIO_HEADSET:
+ soundMode = SOUNDMODE_HEADSET;
+ sSoundMode = SOUNDMODE_HEADSET;
+ break;
+
+ case SAVE_AUDIO_SURROUND:
+ soundMode = SOUNDMODE_SURROUND;
+ // @recomp Use external surround mode - the actual 5.1 output is handled
+ // by the matrix decoder when enabled in the recomp's config menu
+ sSoundMode = SOUNDMODE_SURROUND_EXTERNAL;
+ break;
+
+ default:
+ break;
+ }
+
+ SEQCMD_SET_SOUND_MODE(soundMode);
+}
diff --git a/patches/syms.ld b/patches/syms.ld
index 2efa2c9..e076507 100644
--- a/patches/syms.ld
+++ b/patches/syms.ld
@@ -53,3 +53,5 @@ recomp_create_actor_data = 0x8F0000C8;
recomp_destroy_actor_data = 0x8F0000CC;
recomp_get_actor_data = 0x8F0000D0;
recomp_get_actor_spawn_index = 0x8F0000D4;
+recomp_set_audio_channels = 0x8F0000D8;
+recomp_get_audio_channels = 0x8F0000DC;
diff --git a/src/audio/sound_matrix_decoder.cpp b/src/audio/sound_matrix_decoder.cpp
new file mode 100644
index 0000000..49831f4
--- /dev/null
+++ b/src/audio/sound_matrix_decoder.cpp
@@ -0,0 +1,277 @@
+#include "sound_matrix_decoder.h"
+
+// Standard matrix decoding gains derived from psychoacoustic principles
+namespace Gains {
+constexpr float gCenter = 0.7071067811865476f * 0.5f; // -3dB (1/sqrt(2)) * 0.5
+constexpr float gFront = 0.5f; // -6dB
+constexpr float gSurroundPrimary = 0.4359f; // Primary surround contribution
+constexpr float gSurroundSecondary = 0.2449f; // Cross-feed surround contribution
+} // namespace Gains
+
+// Timing constants
+namespace Timing {
+constexpr int gSurroundDelayMs = 10; // ITU-R BS.775 recommends 10-25ms
+}
+
+SoundMatrixDecoder::SoundMatrixDecoder(int32_t sampleRate) {
+ // Compute delay length from sample rate
+ mDelayLength = (sampleRate * Timing::gSurroundDelayMs) / 1000;
+ if (mDelayLength > gMaxDelay) {
+ mDelayLength = gMaxDelay;
+ }
+
+ // Precompute base rate for all-pass sweep
+ mAllPassBaseRate = std::pow(std::pow(2.0, 4.0), 0.1 / (sampleRate / 2.0));
+
+ // Design filters for this sample rate
+ mCoefCenterHP = DesignHighPass(70.0, sampleRate); // Remove rumble from center
+ mCoefCenterLP = DesignLowPass(20000.0, sampleRate); // Anti-alias center
+ mCoefSurroundHP = DesignHighPass(100.0, sampleRate); // Surround channels high-passed
+ mCoefSubLP = DesignLowPass(120.0, sampleRate); // LFE low-pass
+
+ // Initialize phase chains with sample rate
+ mPhaseLeftMain = {};
+ mPhaseLeftCross = {};
+ mPhaseRightMain = {};
+ mPhaseRightCross = {};
+ PrepareAllPass(mPhaseLeftMain, sampleRate);
+ PrepareAllPass(mPhaseLeftCross, sampleRate);
+ PrepareAllPass(mPhaseRightMain, sampleRate);
+ PrepareAllPass(mPhaseRightCross, sampleRate);
+
+ // Reset filter states
+ ResetState();
+}
+
+void SoundMatrixDecoder::ResetState() {
+ // Clear all filter states
+ mCenterHighPass = {};
+ mCenterLowPass = {};
+ mSurrLeftMainHP = {};
+ mSurrLeftCrossHP = {};
+ mSurrRightMainHP = {};
+ mSurrRightCrossHP = {};
+ mSubLowPass = {};
+
+ // Reset delay lines
+ mDelaySurrLeft = {};
+ mDelaySurrLeft.Length = mDelayLength;
+ mDelaySurrRight = {};
+ mDelaySurrRight.Length = mDelayLength;
+}
+
+SoundMatrixDecoder::FilterCoefficients SoundMatrixDecoder::DesignLowPass(double frequency, int32_t sampleRate) {
+ FilterCoefficients coef = {};
+
+ // Clamp to safe range for bilinear transform stability
+ double maxFreq = sampleRate * 0.475;
+ if (frequency > maxFreq) {
+ frequency = maxFreq;
+ }
+
+ // Linkwitz-Riley 4th order: two cascaded Butterworth 2nd order
+ double omega = 2.0 * M_PI * frequency;
+ double omega2 = omega * omega;
+ double omega3 = omega2 * omega;
+ double omega4 = omega2 * omega2;
+
+ // Bilinear transform warping
+ double kVal = omega / std::tan(M_PI * frequency / sampleRate);
+ double k2 = kVal * kVal;
+ double k3 = k2 * kVal;
+ double k4 = k2 * k2;
+
+ double rt2 = std::sqrt(2.0);
+ double term1 = rt2 * omega3 * kVal;
+ double term2 = rt2 * omega * k3;
+ double norm = 4.0 * omega2 * k2 + 2.0 * term1 + k4 + 2.0 * term2 + omega4;
+
+ // Feedback coefficients
+ coef.B[0] = (4.0 * (omega4 + term1 - k4 - term2)) / norm;
+ coef.B[1] = (6.0 * omega4 - 8.0 * omega2 * k2 + 6.0 * k4) / norm;
+ coef.B[2] = (4.0 * (omega4 - term1 + term2 - k4)) / norm;
+ coef.B[3] = (k4 - 2.0 * term1 + omega4 - 2.0 * term2 + 4.0 * omega2 * k2) / norm;
+
+ // Feedforward coefficients (low-pass response)
+ coef.A[0] = omega4 / norm;
+ coef.A[1] = 4.0 * omega4 / norm;
+ coef.A[2] = 6.0 * omega4 / norm;
+ coef.A[3] = coef.A[1];
+ coef.A[4] = coef.A[0];
+
+ return coef;
+}
+
+SoundMatrixDecoder::FilterCoefficients SoundMatrixDecoder::DesignHighPass(double frequency, int32_t sampleRate) {
+ FilterCoefficients coef = {};
+
+ double omega = 2.0 * M_PI * frequency;
+ double omega2 = omega * omega;
+ double omega3 = omega2 * omega;
+ double omega4 = omega2 * omega2;
+
+ double kVal = omega / std::tan(M_PI * frequency / sampleRate);
+ double k2 = kVal * kVal;
+ double k3 = k2 * kVal;
+ double k4 = k2 * k2;
+
+ double rt2 = std::sqrt(2.0);
+ double term1 = rt2 * omega3 * kVal;
+ double term2 = rt2 * omega * k3;
+ double norm = 4.0 * omega2 * k2 + 2.0 * term1 + k4 + 2.0 * term2 + omega4;
+
+ coef.B[0] = (4.0 * (omega4 + term1 - k4 - term2)) / norm;
+ coef.B[1] = (6.0 * omega4 - 8.0 * omega2 * k2 + 6.0 * k4) / norm;
+ coef.B[2] = (4.0 * (omega4 - term1 + term2 - k4)) / norm;
+ coef.B[3] = (k4 - 2.0 * term1 + omega4 - 2.0 * term2 + 4.0 * omega2 * k2) / norm;
+
+ // Feedforward coefficients (high-pass response)
+ coef.A[0] = k4 / norm;
+ coef.A[1] = -4.0 * k4 / norm;
+ coef.A[2] = 6.0 * k4 / norm;
+ coef.A[3] = coef.A[1];
+ coef.A[4] = coef.A[0];
+
+ return coef;
+}
+
+float SoundMatrixDecoder::ProcessFilter(float sample, BiquadCascade& state, const FilterCoefficients& coef) {
+ double in = sample;
+ double out = coef.A[0] * in + coef.A[1] * state.X[0] + coef.A[2] * state.X[1] + coef.A[3] * state.X[2] +
+ coef.A[4] * state.X[3] - coef.B[0] * state.Y[0] - coef.B[1] * state.Y[1] - coef.B[2] * state.Y[2] -
+ coef.B[3] * state.Y[3];
+
+ // Shift history
+ state.X[3] = state.X[2];
+ state.X[2] = state.X[1];
+ state.X[1] = state.X[0];
+ state.X[0] = in;
+ state.Y[3] = state.Y[2];
+ state.Y[2] = state.Y[1];
+ state.Y[1] = state.Y[0];
+ state.Y[0] = out;
+
+ return static_cast(out);
+}
+
+void SoundMatrixDecoder::PrepareAllPass(AllPassChain& chain, int32_t sampleRate) {
+ // Sweeping all-pass parameters for decorrelation
+ constexpr double depth = 4.0;
+ constexpr double baseDelay = 100.0;
+ constexpr double sweepSpeed = 0.1;
+
+ chain.FreqMin = (M_PI * baseDelay) / sampleRate;
+ chain.Freq = chain.FreqMin;
+
+ double range = std::pow(2.0, depth);
+ chain.FreqMax = (M_PI * baseDelay * range) / sampleRate;
+ chain.SweepRate = std::pow(range, sweepSpeed / (sampleRate / 2.0));
+ chain.Ready = true;
+}
+
+float SoundMatrixDecoder::ProcessAllPass(float sample, AllPassChain& chain, bool negate) {
+ // First-order all-pass coefficient
+ double c = (1.0 - chain.Freq) / (1.0 + chain.Freq);
+ double input = static_cast(sample);
+
+ // Cascade of 4 first-order all-pass sections
+ chain.YHist[0] = c * (chain.YHist[0] + input) - chain.XHist[0];
+ chain.XHist[0] = input;
+
+ chain.YHist[1] = c * (chain.YHist[1] + chain.YHist[0]) - chain.XHist[1];
+ chain.XHist[1] = chain.YHist[0];
+
+ chain.YHist[2] = c * (chain.YHist[2] + chain.YHist[1]) - chain.XHist[2];
+ chain.XHist[2] = chain.YHist[1];
+
+ chain.YHist[3] = c * (chain.YHist[3] + chain.YHist[2]) - chain.XHist[3];
+ chain.XHist[3] = chain.YHist[2];
+
+ double result = negate ? -chain.YHist[3] : chain.YHist[3];
+
+ // Sweep the frequency for time-varying decorrelation
+ chain.Freq *= chain.SweepRate;
+
+ if (chain.Freq > chain.FreqMax) {
+ chain.SweepRate = 1.0 / mAllPassBaseRate;
+ } else if (chain.Freq < chain.FreqMin) {
+ chain.SweepRate = mAllPassBaseRate;
+ }
+
+ return static_cast(result);
+}
+
+float SoundMatrixDecoder::ProcessDelay(float sample, CircularDelay& buffer) {
+ float output = buffer.Data[buffer.Head];
+ buffer.Data[buffer.Head] = sample;
+ buffer.Head = (buffer.Head + 1) % buffer.Length;
+ return output;
+}
+
+float SoundMatrixDecoder::Saturate(float value) {
+ if (value > 1.0f) {
+ return 1.0f;
+ }
+ if (value < -1.0f) {
+ return -1.0f;
+ }
+ return value;
+}
+
+std::tuple SoundMatrixDecoder::Process(const float* stereoInput, size_t samplePairs) {
+ // Resize output buffer if needed (6 channels per sample pair)
+ size_t samplesNeeded = samplePairs * 6;
+ if (mSurroundBuffer.size() < samplesNeeded) {
+ mSurroundBuffer.resize(samplesNeeded);
+ }
+
+ for (size_t i = 0; i < samplePairs; ++i) {
+ float inL = stereoInput[i * 2];
+ float inR = stereoInput[i * 2 + 1];
+
+ // Center: sum of L+R, band-limited
+ float ctr = (inL + inR) * Gains::gCenter;
+ ctr = ProcessFilter(ctr, mCenterHighPass, mCoefCenterHP);
+ ctr = ProcessFilter(ctr, mCenterLowPass, mCoefCenterLP);
+
+ // Front channels: attenuated direct signal
+ float frontL = inL * Gains::gFront;
+ float frontR = inR * Gains::gFront;
+
+ // Surround Left: L primary (inverted phase) + R secondary (shifted phase)
+ float slMain = inL * Gains::gSurroundPrimary;
+ slMain = ProcessFilter(slMain, mSurrLeftMainHP, mCoefSurroundHP);
+ slMain = ProcessAllPass(slMain, mPhaseLeftMain, true);
+
+ float slCross = inR * Gains::gSurroundSecondary;
+ slCross = ProcessFilter(slCross, mSurrLeftCrossHP, mCoefSurroundHP);
+ slCross = ProcessAllPass(slCross, mPhaseLeftCross, false);
+
+ float surrL = ProcessDelay(slMain + slCross, mDelaySurrLeft);
+
+ // Surround Right: R primary (shifted phase) + L secondary (inverted phase)
+ float srMain = inR * Gains::gSurroundPrimary;
+ srMain = ProcessFilter(srMain, mSurrRightMainHP, mCoefSurroundHP);
+ srMain = ProcessAllPass(srMain, mPhaseRightMain, false);
+
+ float srCross = inL * Gains::gSurroundSecondary;
+ srCross = ProcessFilter(srCross, mSurrRightCrossHP, mCoefSurroundHP);
+ srCross = ProcessAllPass(srCross, mPhaseRightCross, true);
+
+ float surrR = ProcessDelay(srMain + srCross, mDelaySurrRight);
+
+ // LFE: low-passed sum
+ float lfe = (inL + inR) * Gains::gCenter;
+ lfe = ProcessFilter(lfe, mSubLowPass, mCoefSubLP);
+
+ // Output: FL, FR, C, LFE, SL, SR
+ mSurroundBuffer[i * 6 + 0] = Saturate(frontL);
+ mSurroundBuffer[i * 6 + 1] = Saturate(frontR);
+ mSurroundBuffer[i * 6 + 2] = Saturate(ctr);
+ mSurroundBuffer[i * 6 + 3] = Saturate(lfe);
+ mSurroundBuffer[i * 6 + 4] = Saturate(surrL);
+ mSurroundBuffer[i * 6 + 5] = Saturate(surrR);
+ }
+
+ return { mSurroundBuffer.data(), samplesNeeded };
+}
diff --git a/src/game/config.cpp b/src/game/config.cpp
index 17ff9e4..f00bc09 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -460,6 +460,7 @@ bool save_sound_config(const std::filesystem::path& path) {
config_json["main_volume"] = zelda64::get_main_volume();
config_json["bgm_volume"] = zelda64::get_bgm_volume();
config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled();
+ config_json["surround_sound"] = zelda64::get_surround_sound_enabled();
return save_json_with_backups(path, config_json);
}
@@ -474,6 +475,7 @@ bool load_sound_config(const std::filesystem::path& path) {
call_if_key_exists(zelda64::set_main_volume, config_json, "main_volume");
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_surround_sound_enabled, config_json, "surround_sound");
return true;
}
diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp
index 9ad08be..222f7e1 100644
--- a/src/game/recomp_api.cpp
+++ b/src/game/recomp_api.cpp
@@ -13,6 +13,11 @@
#include "../patches/sound.h"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
+#include "audio_channels.h"
+
+// Forward declarations for audio channel functions defined in main.cpp
+void set_audio_channels(AudioChannelsSetting channels);
+AudioChannelsSetting get_audio_channels();
extern "C" void recomp_update_inputs(uint8_t* rdram, recomp_context* ctx) {
recomp::poll_inputs();
@@ -177,3 +182,17 @@ extern "C" void recomp_set_right_analog_suppressed(uint8_t* rdram, recomp_contex
recomp::set_right_analog_suppressed(suppressed);
}
+
+// Surround sound support
+extern "C" void recomp_set_audio_channels(uint8_t* rdram, recomp_context* ctx) {
+ s32 channels = _arg<0, s32>(rdram, ctx);
+
+ // Validate input
+ if (channels >= 0 && channels < audioMax) {
+ set_audio_channels(static_cast(channels));
+ }
+}
+
+extern "C" void recomp_get_audio_channels(uint8_t* rdram, recomp_context* ctx) {
+ _return(ctx, static_cast(get_audio_channels()));
+}
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 6c8262a..ba95b76 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include "nfd.h"
@@ -45,6 +46,9 @@
#include "../../patches/sound.h"
#include "../../patches/misc_funcs.h"
+#include "audio_channels.h"
+#include "sound_matrix_decoder.h"
+
#include "mods/mm_recomp_dpad_builtin.h"
#ifdef _WIN32
@@ -188,6 +192,10 @@ static uint32_t output_sample_rate = 48000;
constexpr uint32_t input_channels = 2;
static uint32_t output_channels = 2;
+// Surround sound support
+static AudioChannelsSetting audio_channel_setting = audioStereo;
+static std::unique_ptr sound_matrix_decoder;
+
// Terminology: a frame is a collection of samples for each channel. e.g. 2 input samples is one input frame. This is unrelated to graphical frames.
// Number of frames to duplicate for fixing interpolation at the start and end of a chunk.
@@ -241,25 +249,64 @@ void queue_samples(int16_t* audio_data, size_t sample_count) {
throw std::runtime_error("Error using SDL audio converter");
}
- uint64_t cur_queued_microseconds = uint64_t(SDL_GetQueuedAudioSize(audio_device)) / bytes_per_frame * 1000000 / sample_rate;
- uint32_t num_bytes_to_queue = audio_convert.len_cvt - output_channels * discarded_output_frames * sizeof(swap_buffer[0]);
- float* samples_to_queue = swap_buffer.data() + output_channels * discarded_output_frames / 2;
+ // Calculate the number of stereo frames after resampling (for both stereo and surround paths)
+ size_t resampled_stereo_frames = audio_convert.len_cvt / sizeof(float) / input_channels;
+ size_t frames_after_discard = resampled_stereo_frames - discarded_output_frames;
+ float* stereo_samples = swap_buffer.data() + discarded_output_frames / 2;
- // Prevent audio latency from building up by skipping samples in incoming audio when too many samples are already queued.
- // Skip samples based on how many microseconds of samples are queued already.
- uint32_t skip_factor = cur_queued_microseconds / 100000;
- if (skip_factor != 0) {
- uint32_t skip_ratio = 1 << skip_factor;
- num_bytes_to_queue /= skip_ratio;
- for (size_t i = 0; i < num_bytes_to_queue / (output_channels * sizeof(swap_buffer[0])); i++) {
- samples_to_queue[2 * i + 0] = samples_to_queue[2 * skip_ratio * i + 0];
- samples_to_queue[2 * i + 1] = samples_to_queue[2 * skip_ratio * i + 1];
+ // Handle surround sound matrix decoding
+ if (audio_channel_setting == audioMatrix51 && sound_matrix_decoder) {
+ static int surround_frame_count = 0;
+ if (surround_frame_count < 3) {
+ printf("Audio: Processing surround frame %d (matrix decoding active, %zu stereo frames)\n",
+ ++surround_frame_count, frames_after_discard);
}
- }
+ // Process stereo through the matrix decoder to get 5.1 surround
+ auto [surround_samples, surround_sample_count] = sound_matrix_decoder->Process(stereo_samples, frames_after_discard);
+
+ uint64_t cur_queued_microseconds = uint64_t(SDL_GetQueuedAudioSize(audio_device)) / (6 * sizeof(float)) * 1000000 / output_sample_rate;
+ uint32_t num_bytes_to_queue = surround_sample_count * sizeof(float);
- // Queue the swapped audio data.
- // Offset the data start by only half the discarded frame count as the other half of the discarded frames are at the end of the buffer.
- SDL_QueueAudio(audio_device, samples_to_queue, num_bytes_to_queue);
+ // Prevent audio latency from building up by skipping frames
+ uint32_t skip_factor = cur_queued_microseconds / 100000;
+ if (skip_factor != 0) {
+ uint32_t skip_ratio = 1 << skip_factor;
+ size_t frames_to_queue = surround_sample_count / 6;
+ size_t new_frames = frames_to_queue / skip_ratio;
+ // Surround buffer is const, so we need to copy to a temp buffer for skipping
+ static std::vector skip_buffer;
+ skip_buffer.resize(new_frames * 6);
+ for (size_t i = 0; i < new_frames; i++) {
+ for (size_t ch = 0; ch < 6; ch++) {
+ skip_buffer[i * 6 + ch] = surround_samples[i * skip_ratio * 6 + ch];
+ }
+ }
+ SDL_QueueAudio(audio_device, skip_buffer.data(), new_frames * 6 * sizeof(float));
+ } else {
+ SDL_QueueAudio(audio_device, surround_samples, num_bytes_to_queue);
+ }
+ } else {
+ // Stereo path (original behavior)
+ uint64_t cur_queued_microseconds = uint64_t(SDL_GetQueuedAudioSize(audio_device)) / bytes_per_frame * 1000000 / sample_rate;
+ uint32_t num_bytes_to_queue = audio_convert.len_cvt - output_channels * discarded_output_frames * sizeof(swap_buffer[0]);
+ float* samples_to_queue = swap_buffer.data() + output_channels * discarded_output_frames / 2;
+
+ // Prevent audio latency from building up by skipping samples in incoming audio when too many samples are already queued.
+ // Skip samples based on how many microseconds of samples are queued already.
+ uint32_t skip_factor = cur_queued_microseconds / 100000;
+ if (skip_factor != 0) {
+ uint32_t skip_ratio = 1 << skip_factor;
+ num_bytes_to_queue /= skip_ratio;
+ for (size_t i = 0; i < num_bytes_to_queue / (output_channels * sizeof(swap_buffer[0])); i++) {
+ samples_to_queue[2 * i + 0] = samples_to_queue[2 * skip_ratio * i + 0];
+ samples_to_queue[2 * i + 1] = samples_to_queue[2 * skip_ratio * i + 1];
+ }
+ }
+
+ // Queue the swapped audio data.
+ // Offset the data start by only half the discarded frame count as the other half of the discarded frames are at the end of the buffer.
+ SDL_QueueAudio(audio_device, samples_to_queue, num_bytes_to_queue);
+ }
}
size_t get_frames_remaining() {
@@ -304,6 +351,26 @@ void set_frequency(uint32_t freq) {
}
void reset_audio(uint32_t output_freq) {
+ // Close existing audio device if open
+ if (audio_device != 0) {
+ SDL_PauseAudioDevice(audio_device, 1);
+ SDL_ClearQueuedAudio(audio_device);
+ SDL_CloseAudioDevice(audio_device);
+ audio_device = 0;
+ }
+
+ // Set output channels based on audio channel setting
+ switch (audio_channel_setting) {
+ case audioMatrix51:
+ case audioRaw51:
+ output_channels = 6;
+ break;
+ case audioStereo:
+ default:
+ output_channels = 2;
+ break;
+ }
+
SDL_AudioSpec spec_desired{
.freq = (int)output_freq,
.format = AUDIO_F32,
@@ -316,7 +383,6 @@ void reset_audio(uint32_t output_freq) {
.userdata = nullptr
};
-
audio_device = SDL_OpenAudioDevice(nullptr, false, &spec_desired, nullptr, 0);
if (audio_device == 0) {
exit_error("SDL error opening audio device: %s\n", SDL_GetError());
@@ -325,6 +391,37 @@ void reset_audio(uint32_t output_freq) {
output_sample_rate = output_freq;
update_audio_converter();
+
+ printf("Audio initialized: %d channels, %d Hz (%s)\n", output_channels, output_freq, AudioChannelsSettingName(audio_channel_setting));
+}
+
+AudioChannelsSetting get_audio_channels() {
+ return audio_channel_setting;
+}
+
+void set_audio_channels(AudioChannelsSetting channels) {
+ if (audio_channel_setting == channels) {
+ return; // No change needed
+ }
+
+ printf("Changing audio channels from %s to %s\n",
+ AudioChannelsSettingName(audio_channel_setting),
+ AudioChannelsSettingName(channels));
+
+ audio_channel_setting = channels;
+
+ // Setup or teardown sound matrix decoder
+ if (channels == audioMatrix51) {
+ if (!sound_matrix_decoder) {
+ sound_matrix_decoder = std::make_unique(output_sample_rate);
+ }
+ } else {
+ // When switching away from matrix mode, release the decoder
+ sound_matrix_decoder.reset();
+ }
+
+ // Reinitialize audio device with new channel count
+ reset_audio(output_sample_rate);
}
extern RspUcodeFunc njpgdspMain;
@@ -666,6 +763,8 @@ int main(int argc, char** argv) {
REGISTER_FUNC(recomp_get_targeting_mode);
REGISTER_FUNC(recomp_get_bgm_volume);
REGISTER_FUNC(recomp_get_low_health_beeps_enabled);
+ REGISTER_FUNC(recomp_set_audio_channels);
+ REGISTER_FUNC(recomp_get_audio_channels);
REGISTER_FUNC(recomp_get_gyro_deltas);
REGISTER_FUNC(recomp_get_mouse_deltas);
REGISTER_FUNC(recomp_get_inverted_axes);
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp
index 6832b22..80c35cf 100644
--- a/src/ui/ui_config.cpp
+++ b/src/ui/ui_config.cpp
@@ -3,6 +3,7 @@
#include "zelda_sound.h"
#include "zelda_config.h"
#include "zelda_debug.h"
+#include "audio_channels.h"
#include "zelda_render.h"
#include "zelda_support.h"
#include "promptfont.h"
@@ -364,10 +365,12 @@ struct SoundOptionsContext {
std::atomic main_volume; // Option to control the volume of all sound
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.
+ std::atomic surround_sound_enabled; // Enable 5.1 surround sound matrix decoding
void reset() {
bgm_volume = 100;
main_volume = 100;
low_health_beeps_enabled = (int)true;
+ surround_sound_enabled = (int)false;
}
SoundOptionsContext() {
reset();
@@ -416,6 +419,23 @@ bool zelda64::get_low_health_beeps_enabled() {
return (bool)sound_options_context.low_health_beeps_enabled.load();
}
+// Forward declaration from main.cpp
+void set_audio_channels(AudioChannelsSetting channels);
+
+void zelda64::set_surround_sound_enabled(bool enabled) {
+ printf("UI: Setting surround sound to %s\n", enabled ? "enabled" : "disabled");
+ sound_options_context.surround_sound_enabled.store((int)enabled);
+ if (sound_options_model_handle) {
+ sound_options_model_handle.DirtyVariable("surround_sound_enabled");
+ }
+ // Update audio backend
+ set_audio_channels(enabled ? audioMatrix51 : audioStereo);
+}
+
+bool zelda64::get_surround_sound_enabled() {
+ return (bool)sound_options_context.surround_sound_enabled.load();
+}
+
struct DebugContext {
Rml::DataModelHandle model_handle;
std::vector area_names;
@@ -939,6 +959,17 @@ 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);
+
+ // Custom binding for surround sound that calls the setter to update audio channels
+ constructor.BindFunc("surround_sound_enabled",
+ [](Rml::Variant& out) {
+ out = sound_options_context.surround_sound_enabled.load();
+ },
+ [](const Rml::Variant& in) {
+ bool enabled = in.Get() != 0;
+ zelda64::set_surround_sound_enabled(enabled);
+ }
+ );
}
void make_debug_bindings(Rml::Context* context) {
From d2e771d3218e9a074af1429430b16558ec262b2f Mon Sep 17 00:00:00 2001
From: Tortuga Veloz
Date: Mon, 9 Feb 2026 12:01:30 +0100
Subject: [PATCH 2/8] Surround.
---
include/sound_matrix_decoder.h | 116 +++++++++++++++++++++++++++++++++
1 file changed, 116 insertions(+)
create mode 100644 include/sound_matrix_decoder.h
diff --git a/include/sound_matrix_decoder.h b/include/sound_matrix_decoder.h
new file mode 100644
index 0000000..b800015
--- /dev/null
+++ b/include/sound_matrix_decoder.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+/**
+ * Passive matrix decoder for stereo to 5.1 surround upmixing.
+ * Implements standard audio matrix decoding techniques using:
+ * - Linkwitz-Riley crossover filters for frequency band separation
+ * - All-pass filters for phase manipulation
+ * - Delay lines for surround channel timing
+ */
+class SoundMatrixDecoder {
+ public:
+ /**
+ * Construct and initialize the decoder with a specific sample rate.
+ * @param sampleRate The audio sample rate in Hz
+ */
+ SoundMatrixDecoder(int32_t sampleRate);
+ ~SoundMatrixDecoder() = default;
+
+ /**
+ * Reset filter states without recomputing coefficients.
+ * Useful when audio is interrupted to prevent clicks.
+ */
+ void ResetState();
+
+ /**
+ * Decode stereo to 5.1 surround
+ * @param stereoInput Interleaved stereo samples [L0, R0, L1, R1, ...]
+ * @param samplePairs Number of stereo sample pairs to process
+ * @return Pointer to internal buffer with interleaved 5.1 samples [FL, FR, C, LFE, SL, SR, ...]
+ */
+ std::tuple Process(const float* stereoInput, size_t samplePairs);
+
+ private:
+ // 4th-order IIR filter (Linkwitz-Riley) for 24dB/octave slopes
+ struct BiquadCascade {
+ double X[4] = {}; // Input history
+ double Y[4] = {}; // Output history
+ };
+
+ struct FilterCoefficients {
+ double A[5]; // Feedforward (numerator)
+ double B[4]; // Feedback (denominator, excluding b0=1)
+ };
+
+ // Sweeping all-pass for phase decorrelation
+ struct AllPassChain {
+ double Freq = 0;
+ double FreqMin = 0;
+ double FreqMax = 0;
+ double SweepRate = 0;
+ double XHist[4] = {};
+ double YHist[4] = {};
+ bool Ready = false;
+ };
+
+ // Circular delay buffer
+ static constexpr int gMaxDelay = 1024;
+ struct CircularDelay {
+ std::array Data = {};
+ int Head = 0;
+ int Length = 0;
+ };
+
+ // Filter design
+ FilterCoefficients DesignLowPass(double frequency, int32_t sampleRate);
+ FilterCoefficients DesignHighPass(double frequency, int32_t sampleRate);
+
+ // Signal processing
+ float ProcessFilter(float sample, BiquadCascade& state, const FilterCoefficients& coef);
+ void PrepareAllPass(AllPassChain& chain, int32_t sampleRate);
+ float ProcessAllPass(float sample, AllPassChain& chain, bool negate);
+ float ProcessDelay(float sample, CircularDelay& buffer);
+
+ static float Saturate(float value);
+
+ int32_t mDelayLength = 0;
+ double mAllPassBaseRate = 1.0; // Precomputed for ProcessAllPass
+
+ // Filter coefficients (computed once per sample rate)
+ FilterCoefficients mCoefCenterHP;
+ FilterCoefficients mCoefCenterLP;
+ FilterCoefficients mCoefSurroundHP;
+ FilterCoefficients mCoefSubLP;
+
+ // Per-channel filter states
+ BiquadCascade mCenterHighPass;
+ BiquadCascade mCenterLowPass;
+ BiquadCascade mSurrLeftMainHP;
+ BiquadCascade mSurrLeftCrossHP;
+ BiquadCascade mSurrRightMainHP;
+ BiquadCascade mSurrRightCrossHP;
+ BiquadCascade mSubLowPass;
+
+ // Phase processing
+ AllPassChain mPhaseLeftMain;
+ AllPassChain mPhaseLeftCross;
+ AllPassChain mPhaseRightMain;
+ AllPassChain mPhaseRightCross;
+
+ // Timing
+ CircularDelay mDelaySurrLeft;
+ CircularDelay mDelaySurrRight;
+
+ // Output buffer
+ std::vector mSurroundBuffer;
+};
From 98da5553ffb052d450e1f145c997533aab999659 Mon Sep 17 00:00:00 2001
From: Tortuga Veloz
Date: Mon, 9 Feb 2026 12:05:53 +0100
Subject: [PATCH 3/8] Fix issue.
---
src/main/main.cpp | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/main/main.cpp b/src/main/main.cpp
index ba95b76..d7a3e6d 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -252,14 +252,16 @@ void queue_samples(int16_t* audio_data, size_t sample_count) {
// Calculate the number of stereo frames after resampling (for both stereo and surround paths)
size_t resampled_stereo_frames = audio_convert.len_cvt / sizeof(float) / input_channels;
size_t frames_after_discard = resampled_stereo_frames - discarded_output_frames;
- float* stereo_samples = swap_buffer.data() + discarded_output_frames / 2;
+ // Offset matches stereo path: output_channels * discarded_output_frames / 2 = discarded_output_frames (when output_channels=2)
+ float* stereo_samples = swap_buffer.data() + input_channels * discarded_output_frames / 2;
// Handle surround sound matrix decoding
if (audio_channel_setting == audioMatrix51 && sound_matrix_decoder) {
static int surround_frame_count = 0;
- if (surround_frame_count < 3) {
- printf("Audio: Processing surround frame %d (matrix decoding active, %zu stereo frames)\n",
- ++surround_frame_count, frames_after_discard);
+ if (surround_frame_count < 5) {
+ printf("Audio: Processing surround frame %d: %zu stereo frames, offset=%u, resampled=%zu\n",
+ ++surround_frame_count, frames_after_discard,
+ input_channels * discarded_output_frames / 2, resampled_stereo_frames);
}
// Process stereo through the matrix decoder to get 5.1 surround
auto [surround_samples, surround_sample_count] = sound_matrix_decoder->Process(stereo_samples, frames_after_discard);
@@ -333,7 +335,8 @@ size_t get_frames_remaining() {
}
void update_audio_converter() {
- int ret = SDL_BuildAudioCVT(&audio_convert, AUDIO_F32, input_channels, sample_rate, AUDIO_F32, output_channels, output_sample_rate);
+ // Always convert to stereo - the SoundMatrixDecoder handles upmixing to surround if needed
+ int ret = SDL_BuildAudioCVT(&audio_convert, AUDIO_F32, input_channels, sample_rate, AUDIO_F32, input_channels, output_sample_rate);
if (ret < 0) {
printf("Error creating SDL audio converter: %s\n", SDL_GetError());
From ac123c4cab1411b4dc1d54d736dc45b44095ad55 Mon Sep 17 00:00:00 2001
From: Tortuga Veloz
Date: Tue, 10 Feb 2026 09:46:09 +0100
Subject: [PATCH 4/8] update audio options to select among 4 opts.
---
assets/config_menu/sound.rml | 48 +++++++++++++++++++++++++++---------
include/zelda_config.h | 19 ++++++++++++++
include/zelda_sound.h | 6 +++--
patches/sound_patches.c | 14 +++++++----
src/game/config.cpp | 4 +--
src/game/recomp_api.cpp | 4 +--
src/main/main.cpp | 6 -----
src/ui/ui_config.cpp | 35 +++++++++++++-------------
8 files changed, 90 insertions(+), 46 deletions(-)
diff --git a/assets/config_menu/sound.rml b/assets/config_menu/sound.rml
index 30fa2c7..d95f8a0 100644
--- a/assets/config_menu/sound.rml
+++ b/assets/config_menu/sound.rml
@@ -71,31 +71,55 @@
@@ -111,7 +135,7 @@
Toggles whether or not the low-health beeping sound plays.
- Enables 5.1 surround sound output using matrix decoding. Requires a surround sound system or virtual surround headphones.
+ 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.
diff --git a/include/zelda_config.h b/include/zelda_config.h
index 59b41f0..4b1e727 100644
--- a/include/zelda_config.h
+++ b/include/zelda_config.h
@@ -87,6 +87,25 @@ namespace zelda64 {
AnalogCamMode get_analog_cam_mode();
void set_analog_cam_mode(AnalogCamMode mode);
+ // Audio mode setting - mirrors the game's AudioOption enum
+ enum class AudioMode {
+ Stereo,
+ Mono,
+ Headphones,
+ Surround,
+ OptionCount
+ };
+
+ NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::AudioMode, {
+ {zelda64::AudioMode::Stereo, "Stereo"},
+ {zelda64::AudioMode::Mono, "Mono"},
+ {zelda64::AudioMode::Headphones, "Headphones"},
+ {zelda64::AudioMode::Surround, "Surround"}
+ });
+
+ AudioMode get_audio_mode();
+ void set_audio_mode(AudioMode mode);
+
void open_quit_game_prompt();
};
diff --git a/include/zelda_sound.h b/include/zelda_sound.h
index fb1a652..c296035 100644
--- a/include/zelda_sound.h
+++ b/include/zelda_sound.h
@@ -1,6 +1,8 @@
#ifndef __ZELDA_SOUND_H__
#define __ZELDA_SOUND_H__
+#include "zelda_config.h"
+
namespace zelda64 {
void reset_sound_settings();
void set_main_volume(int volume);
@@ -9,8 +11,8 @@ namespace zelda64 {
int get_bgm_volume();
void set_low_health_beeps_enabled(bool enabled);
bool get_low_health_beeps_enabled();
- void set_surround_sound_enabled(bool enabled);
- bool get_surround_sound_enabled();
+ AudioMode get_audio_mode();
+ void set_audio_mode(AudioMode mode);
}
#endif
diff --git a/patches/sound_patches.c b/patches/sound_patches.c
index 453df4c..b33ace2 100644
--- a/patches/sound_patches.c
+++ b/patches/sound_patches.c
@@ -369,9 +369,7 @@ RECOMP_PATCH void LifeMeter_UpdateSizeAndBeep(PlayState* play) {
}
extern s8 sSoundMode;
-// @recomp Surround sound output is now controlled by the recomp's config menu
-// (Sound -> Surround Sound 5.1), not by the in-game audio setting.
-// This function still controls the game's internal sound processing mode.
+// @recomp Patched to sync audio channels with the game's audio setting
RECOMP_PATCH void Audio_SetFileSelectSettings(s8 audioSetting) {
s8 soundMode;
@@ -379,23 +377,29 @@ RECOMP_PATCH void Audio_SetFileSelectSettings(s8 audioSetting) {
case SAVE_AUDIO_STEREO:
soundMode = SOUNDMODE_STEREO;
sSoundMode = SOUNDMODE_STEREO;
+ // @recomp Sync audio output to stereo
+ recomp_set_audio_channels(AUDIO_CHANNELS_STEREO);
break;
case SAVE_AUDIO_MONO:
soundMode = SOUNDMODE_MONO;
sSoundMode = SOUNDMODE_MONO;
+ // @recomp Sync audio output to stereo (mono is handled by the game's audio engine)
+ recomp_set_audio_channels(AUDIO_CHANNELS_STEREO);
break;
case SAVE_AUDIO_HEADSET:
soundMode = SOUNDMODE_HEADSET;
sSoundMode = SOUNDMODE_HEADSET;
+ // @recomp Sync audio output to stereo (headset mode is handled by the game's audio engine)
+ recomp_set_audio_channels(AUDIO_CHANNELS_STEREO);
break;
case SAVE_AUDIO_SURROUND:
soundMode = SOUNDMODE_SURROUND;
- // @recomp Use external surround mode - the actual 5.1 output is handled
- // by the matrix decoder when enabled in the recomp's config menu
sSoundMode = SOUNDMODE_SURROUND_EXTERNAL;
+ // @recomp Enable 5.1 matrix surround output when game's surround option is selected
+ recomp_set_audio_channels(AUDIO_CHANNELS_MATRIX_51);
break;
default:
diff --git a/src/game/config.cpp b/src/game/config.cpp
index f00bc09..063aa6b 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -460,7 +460,7 @@ bool save_sound_config(const std::filesystem::path& path) {
config_json["main_volume"] = zelda64::get_main_volume();
config_json["bgm_volume"] = zelda64::get_bgm_volume();
config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled();
- config_json["surround_sound"] = zelda64::get_surround_sound_enabled();
+ config_json["audio_mode"] = zelda64::get_audio_mode();
return save_json_with_backups(path, config_json);
}
@@ -475,7 +475,7 @@ bool load_sound_config(const std::filesystem::path& path) {
call_if_key_exists(zelda64::set_main_volume, config_json, "main_volume");
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_surround_sound_enabled, config_json, "surround_sound");
+ call_if_key_exists(zelda64::set_audio_mode, config_json, "audio_mode");
return true;
}
diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp
index 222f7e1..4567789 100644
--- a/src/game/recomp_api.cpp
+++ b/src/game/recomp_api.cpp
@@ -183,11 +183,11 @@ extern "C" void recomp_set_right_analog_suppressed(uint8_t* rdram, recomp_contex
recomp::set_right_analog_suppressed(suppressed);
}
-// Surround sound support
+// Surround sound support - called by the game when audio settings change
extern "C" void recomp_set_audio_channels(uint8_t* rdram, recomp_context* ctx) {
s32 channels = _arg<0, s32>(rdram, ctx);
- // Validate input
+ // Validate input and update the audio backend
if (channels >= 0 && channels < audioMax) {
set_audio_channels(static_cast(channels));
}
diff --git a/src/main/main.cpp b/src/main/main.cpp
index d7a3e6d..3e85458 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -257,12 +257,6 @@ void queue_samples(int16_t* audio_data, size_t sample_count) {
// Handle surround sound matrix decoding
if (audio_channel_setting == audioMatrix51 && sound_matrix_decoder) {
- static int surround_frame_count = 0;
- if (surround_frame_count < 5) {
- printf("Audio: Processing surround frame %d: %zu stereo frames, offset=%u, resampled=%zu\n",
- ++surround_frame_count, frames_after_discard,
- input_channels * discarded_output_frames / 2, resampled_stereo_frames);
- }
// Process stereo through the matrix decoder to get 5.1 surround
auto [surround_samples, surround_sample_count] = sound_matrix_decoder->Process(stereo_samples, frames_after_discard);
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp
index 80c35cf..5ea4c0e 100644
--- a/src/ui/ui_config.cpp
+++ b/src/ui/ui_config.cpp
@@ -365,12 +365,12 @@ struct SoundOptionsContext {
std::atomic main_volume; // Option to control the volume of all sound
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.
- std::atomic surround_sound_enabled; // Enable 5.1 surround sound matrix decoding
+ zelda64::AudioMode audio_mode; // Audio output mode (Stereo, Mono, Headphones, Surround)
void reset() {
bgm_volume = 100;
main_volume = 100;
low_health_beeps_enabled = (int)true;
- surround_sound_enabled = (int)false;
+ audio_mode = zelda64::AudioMode::Stereo;
}
SoundOptionsContext() {
reset();
@@ -422,18 +422,18 @@ bool zelda64::get_low_health_beeps_enabled() {
// Forward declaration from main.cpp
void set_audio_channels(AudioChannelsSetting channels);
-void zelda64::set_surround_sound_enabled(bool enabled) {
- printf("UI: Setting surround sound to %s\n", enabled ? "enabled" : "disabled");
- sound_options_context.surround_sound_enabled.store((int)enabled);
+void zelda64::set_audio_mode(zelda64::AudioMode mode) {
+ printf("UI: Setting audio mode to %d\n", (int)mode);
+ sound_options_context.audio_mode = mode;
if (sound_options_model_handle) {
- sound_options_model_handle.DirtyVariable("surround_sound_enabled");
+ sound_options_model_handle.DirtyVariable("audio_mode");
}
- // Update audio backend
- set_audio_channels(enabled ? audioMatrix51 : audioStereo);
+ // Update audio backend - only Surround mode uses 5.1 matrix decoding
+ set_audio_channels(mode == zelda64::AudioMode::Surround ? audioMatrix51 : audioStereo);
}
-bool zelda64::get_surround_sound_enabled() {
- return (bool)sound_options_context.surround_sound_enabled.load();
+zelda64::AudioMode zelda64::get_audio_mode() {
+ return sound_options_context.audio_mode;
}
struct DebugContext {
@@ -960,14 +960,15 @@ public:
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);
- // Custom binding for surround sound that calls the setter to update audio channels
- constructor.BindFunc("surround_sound_enabled",
- [](Rml::Variant& out) {
- out = sound_options_context.surround_sound_enabled.load();
- },
+ // Custom binding for audio mode that calls the setter to update audio channels
+ constructor.BindFunc("audio_mode",
+ [](Rml::Variant& out) { get_option(sound_options_context.audio_mode, out); },
[](const Rml::Variant& in) {
- bool enabled = in.Get() != 0;
- zelda64::set_surround_sound_enabled(enabled);
+ zelda64::AudioMode mode = zelda64::AudioMode::OptionCount;
+ from_json(in.Get(), mode);
+ if (mode != zelda64::AudioMode::OptionCount) {
+ zelda64::set_audio_mode(mode);
+ }
}
);
}
From 177fe6a4e531078907903881bd3a8dd4e1c3ab56 Mon Sep 17 00:00:00 2001
From: Tortuga Veloz
Date: Tue, 10 Feb 2026 10:17:49 +0100
Subject: [PATCH 5/8] Add Enhanced Surround option.
---
assets/config_menu/sound.rml | 34 ++++++++++++-
include/zelda_config.h | 4 ++
patches/sound.h | 1 +
patches/sound_patches.c | 93 ++++++++++++++++++++++++++++++++++++
patches/syms.ld | 1 +
src/game/config.cpp | 2 +
src/game/recomp_api.cpp | 4 ++
src/ui/ui_config.cpp | 16 ++++++-
8 files changed, 153 insertions(+), 2 deletions(-)
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",
From b007d1f34b019baf754a51ebadc21dd3295d401c Mon Sep 17 00:00:00 2001
From: Tortuga Veloz
Date: Wed, 11 Feb 2026 01:06:46 +0100
Subject: [PATCH 6/8] Added navigability with dpad.
---
assets/config_menu/sound.rml | 23 +++++++++++------------
1 file changed, 11 insertions(+), 12 deletions(-)
diff --git a/assets/config_menu/sound.rml b/assets/config_menu/sound.rml
index 169d3c0..11c6ec5 100644
--- a/assets/config_menu/sound.rml
+++ b/assets/config_menu/sound.rml
@@ -2,10 +2,10 @@
-