Add surround option.

This commit is contained in:
Tortuga Veloz 2026-02-09 12:01:09 +01:00
parent ab677e7661
commit dacf76962e
13 changed files with 555 additions and 20 deletions

View file

@ -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

View file

@ -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;"
/>
<label class="config-option__tab-label" for="lhb_on">On</label>
@ -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;"
/>
<label class="config-option__tab-label" for="lhb_off">Off</label>
</div>
</div>
<div class="config-option" data-event-mouseover="set_cur_config_index(3)">
<label class="config-option__title">Surround Sound (5.1)</label>
<div class="config-option__list">
<input
type="radio"
data-event-blur="set_cur_config_index(-1)"
data-event-focus="set_cur_config_index(3)"
name="surround"
data-checked="surround_sound_enabled"
value="1"
id="surround_on"
style="nav-up: #lhb_on"
/>
<label class="config-option__tab-label" for="surround_on">On</label>
<input
type="radio"
data-event-blur="set_cur_config_index(-1)"
data-event-focus="set_cur_config_index(3)"
name="surround"
data-checked="surround_sound_enabled"
value="0"
id="surround_off"
style="nav-up: #lhb_on"
/>
<label class="config-option__tab-label" for="surround_off">Off</label>
</div>
</div>
</div>
<!-- Descriptions -->
<div class="config__wrapper">
@ -81,6 +110,9 @@
<p data-if="cur_config_index == 2">
Toggles whether or not the low-health beeping sound plays.
</p>
<p data-if="cur_config_index == 3">
Enables 5.1 surround sound output using matrix decoding. Requires a surround sound system or virtual surround headphones.
</p>
</div>
</div>
</form>

21
include/audio_channels.h Normal file
View file

@ -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";
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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

View file

@ -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);
}

View file

@ -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;

View file

@ -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<float>(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<double>(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<float>(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<const float*, size_t> 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 };
}

View file

@ -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;
}

View file

@ -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<AudioChannelsSetting>(channels));
}
}
extern "C" void recomp_get_audio_channels(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<s32>(get_audio_channels()));
}

View file

@ -7,6 +7,7 @@
#include <numeric>
#include <stdexcept>
#include <cinttypes>
#include <memory>
#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<SoundMatrixDecoder> 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<float> 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<SoundMatrixDecoder>(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);

View file

@ -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<int> main_volume; // Option to control the volume of all sound
std::atomic<int> bgm_volume;
std::atomic<int> 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<int> 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<std::string> 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<int>() != 0;
zelda64::set_surround_sound_enabled(enabled);
}
);
}
void make_debug_bindings(Rml::Context* context) {