mirror of
https://github.com/Zelda64Recomp/Zelda64Recomp.git
synced 2026-03-21 18:41:47 +00:00
Merge bae83bd11d into ab677e7661
This commit is contained in:
commit
ffacc4bc15
16 changed files with 873 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
<panel class="config" data-model="graphics_model">
|
||||
<template src="config-menu__graphics" />
|
||||
</panel>
|
||||
<tab class="tab" id="tab_sound">
|
||||
<tab class="tab" id="tab_sound" style="nav-down: #main_volume_input">
|
||||
<div>Sound</div>
|
||||
<div class="tab__indicator"></div>
|
||||
</tab>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<form class="config__form">
|
||||
<div class="config__hz-wrapper">
|
||||
<form class="config__form" id="conf-sound__form">
|
||||
<div class="config__hz-wrapper" id="conf-sound__hz-wrapper">
|
||||
<!-- Options -->
|
||||
<div class="config__wrapper" data-event-mouseout="set_cur_config_index(-1)">
|
||||
<div class="config__wrapper" data-event-mouseout="set_cur_config_index(-1)" id="conf-sound__wrapper">
|
||||
<div class="config-option" data-event-mouseover="set_cur_config_index(0)">
|
||||
<label class="config-option__title">Main Volume</label>
|
||||
<div class="config-option__range-wrapper config-option__list">
|
||||
|
|
@ -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: #audio_stereo"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="lhb_on">On</label>
|
||||
|
||||
|
|
@ -64,11 +64,93 @@
|
|||
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: #audio_mono"
|
||||
/>
|
||||
<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">Audio Mode</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="audio_mode"
|
||||
data-checked="audio_mode"
|
||||
value="Stereo"
|
||||
id="audio_stereo"
|
||||
style="nav-up: #lhb_on; nav-down: #enhanced_surround_on"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="audio_stereo">Stereo</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
data-event-blur="set_cur_config_index(-1)"
|
||||
data-event-focus="set_cur_config_index(3)"
|
||||
name="audio_mode"
|
||||
data-checked="audio_mode"
|
||||
value="Mono"
|
||||
id="audio_mono"
|
||||
style="nav-up: #lhb_off; nav-down: #enhanced_surround_off"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="audio_mono">Mono</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
data-event-blur="set_cur_config_index(-1)"
|
||||
data-event-focus="set_cur_config_index(3)"
|
||||
name="audio_mode"
|
||||
data-checked="audio_mode"
|
||||
value="Headphones"
|
||||
id="audio_headphones"
|
||||
style="nav-up: #lhb_off; nav-down: #enhanced_surround_off"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="audio_headphones">Headphones</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
data-event-blur="set_cur_config_index(-1)"
|
||||
data-event-focus="set_cur_config_index(3)"
|
||||
name="audio_mode"
|
||||
data-checked="audio_mode"
|
||||
value="Surround"
|
||||
id="audio_surround"
|
||||
style="nav-up: #lhb_off; nav-down: #enhanced_surround_off"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="audio_surround">Surround</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-option" data-event-mouseover="set_cur_config_index(4)">
|
||||
<label class="config-option__title">Enhanced Surround</label>
|
||||
<div class="config-option__list">
|
||||
<input
|
||||
type="radio"
|
||||
data-event-blur="set_cur_config_index(-1)"
|
||||
data-event-focus="set_cur_config_index(4)"
|
||||
name="enhanced_surround"
|
||||
data-checked="enhanced_surround_enabled"
|
||||
value="1"
|
||||
id="enhanced_surround_on"
|
||||
style="nav-up: #audio_stereo"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="enhanced_surround_on">On</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
data-event-blur="set_cur_config_index(-1)"
|
||||
data-event-focus="set_cur_config_index(4)"
|
||||
name="enhanced_surround"
|
||||
data-checked="enhanced_surround_enabled"
|
||||
value="0"
|
||||
id="enhanced_surround_off"
|
||||
style="nav-up: #audio_mono"
|
||||
/>
|
||||
<label class="config-option__tab-label" for="enhanced_surround_off">Off</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Descriptions -->
|
||||
<div class="config__wrapper">
|
||||
|
|
@ -81,9 +163,14 @@
|
|||
<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">
|
||||
Sets the audio output mode. <b>Stereo</b> outputs standard stereo audio. <b>Mono</b> outputs mono audio. <b>Headphones</b> optimizes audio for headphone listening. <b>Surround</b> enables 5.1 surround sound output using matrix decoding.
|
||||
</p>
|
||||
<p data-if="cur_config_index == 4">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</template>
|
||||
|
||||
18
include/audio_channels.h
Normal file
18
include/audio_channels.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
typedef enum AudioChannelsSetting {
|
||||
audioStereo,
|
||||
audioMatrix51,
|
||||
audioMax
|
||||
} AudioChannelsSetting;
|
||||
|
||||
inline const char* AudioChannelsSettingName(AudioChannelsSetting setting) {
|
||||
switch (setting) {
|
||||
case audioStereo:
|
||||
return "Stereo";
|
||||
case audioMatrix51:
|
||||
return "5.1 Matrix";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
116
include/sound_matrix_decoder.h
Normal file
116
include/sound_matrix_decoder.h
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
|
||||
#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<const float*, size_t> 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<float, gMaxDelay> 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<float> mSurroundBuffer;
|
||||
};
|
||||
|
|
@ -87,6 +87,29 @@ 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"}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
void open_quit_game_prompt();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,6 +11,8 @@ namespace zelda64 {
|
|||
int get_bgm_volume();
|
||||
void set_low_health_beeps_enabled(bool enabled);
|
||||
bool get_low_health_beeps_enabled();
|
||||
AudioMode get_audio_mode();
|
||||
void set_audio_mode(AudioMode mode);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,4 +6,10 @@
|
|||
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);
|
||||
DECLARE_FUNC(s32, recomp_get_enhanced_surround_enabled);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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,137 @@ RECOMP_PATCH void LifeMeter_UpdateSizeAndBeep(PlayState* play) {
|
|||
}
|
||||
}
|
||||
}
|
||||
extern s8 sSoundMode;
|
||||
|
||||
// @recomp Patched to sync audio channels with the game's audio setting
|
||||
RECOMP_PATCH void Audio_SetFileSelectSettings(s8 audioSetting) {
|
||||
s8 soundMode;
|
||||
|
||||
switch (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;
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,3 +53,6 @@ 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;
|
||||
recomp_get_enhanced_surround_enabled = 0x8F0000E0;
|
||||
|
|
|
|||
277
src/audio/sound_matrix_decoder.cpp
Normal file
277
src/audio/sound_matrix_decoder.cpp
Normal 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 };
|
||||
}
|
||||
|
|
@ -460,6 +460,8 @@ 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["audio_mode"] = zelda64::get_audio_mode();
|
||||
config_json["enhanced_surround"] = zelda64::get_enhanced_surround_enabled();
|
||||
|
||||
return save_json_with_backups(path, config_json);
|
||||
}
|
||||
|
|
@ -474,6 +476,8 @@ 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_audio_mode, config_json, "audio_mode");
|
||||
call_if_key_exists(zelda64::set_enhanced_surround_enabled, config_json, "enhanced_surround");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,21 @@ extern "C" void recomp_set_right_analog_suppressed(uint8_t* rdram, recomp_contex
|
|||
|
||||
recomp::set_right_analog_suppressed(suppressed);
|
||||
}
|
||||
|
||||
// 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 and update the audio backend
|
||||
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()));
|
||||
}
|
||||
|
||||
extern "C" void recomp_get_enhanced_surround_enabled(uint8_t* rdram, recomp_context* ctx) {
|
||||
_return(ctx, zelda64::get_enhanced_surround_enabled() ? 1 : 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,60 @@ 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;
|
||||
// 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;
|
||||
|
||||
// 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) {
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
// 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 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() {
|
||||
|
|
@ -286,7 +329,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());
|
||||
|
|
@ -304,6 +348,25 @@ 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:
|
||||
output_channels = 6;
|
||||
break;
|
||||
case audioStereo:
|
||||
default:
|
||||
output_channels = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_AudioSpec spec_desired{
|
||||
.freq = (int)output_freq,
|
||||
.format = AUDIO_F32,
|
||||
|
|
@ -316,7 +379,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 +387,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 +759,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);
|
||||
|
|
|
|||
|
|
@ -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,14 @@ 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.
|
||||
zelda64::AudioMode audio_mode; // Audio output mode (Stereo, Mono, Headphones, Surround)
|
||||
std::atomic<int> 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();
|
||||
|
|
@ -416,6 +421,34 @@ 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_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("audio_mode");
|
||||
}
|
||||
// Update audio backend - Surround mode uses 5.1 matrix decoding
|
||||
set_audio_channels(mode == zelda64::AudioMode::Surround ? audioMatrix51 : audioStereo);
|
||||
}
|
||||
|
||||
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<std::string> area_names;
|
||||
|
|
@ -482,7 +515,9 @@ class ConfigTabsetListener : public Rml::EventListener {
|
|||
if (tabs != nullptr) {
|
||||
size_t num_children = tabs->GetNumChildren();
|
||||
for (size_t i = 0; i < num_children; i++) {
|
||||
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
|
||||
if (!tabs->GetChild(i)->GetLocalProperty(Rml::PropertyId::NavDown)) {
|
||||
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -939,6 +974,19 @@ 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",
|
||||
[](Rml::Variant& out) { get_option(sound_options_context.audio_mode, out); },
|
||||
[](const Rml::Variant& in) {
|
||||
zelda64::AudioMode mode = zelda64::AudioMode::OptionCount;
|
||||
from_json(in.Get<std::string>(), mode);
|
||||
if (mode != zelda64::AudioMode::OptionCount) {
|
||||
zelda64::set_audio_mode(mode);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void make_debug_bindings(Rml::Context* context) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue