Merge pull request #2384 from dooplecks/surround-redux

Basic discrete surround sound support
This commit is contained in:
TakaRikka
2026-09-07 21:02:54 -07:00
committed by GitHub
13 changed files with 577 additions and 372 deletions
-4
View File
@@ -173,11 +173,7 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo
f32 pan = 0.5f;
f32 dolby = 0.0f;
#if TARGET_PC
u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode();
#else
u32 effectiveOutputMode = JASDriver::getOutputMode();
#endif
switch (effectiveOutputMode) {
case JAS_OUTPUT_MONO:
break;
+12
View File
@@ -3,6 +3,7 @@
#include "JSystem/JAudio2/JASDriverIF.h"
#include "JSystem/JAudio2/JASAiCtrl.h"
#include "JSystem/JAudio2/JASDSPInterface.h"
#include "dusk/settings.h"
#include <os.h>
void JASDriver::setDSPLevel(f32 param_0) {
@@ -30,7 +31,18 @@ void JASDriver::setOutputMode(u32 mode) {
}
u32 JASDriver::getOutputMode() {
#ifdef TARGET_PC
switch (dusk::getSettings().audio.outputMode) {
case dusk::AudioOutputMode::StereoSpeakers:
return JAS_OUTPUT_STEREO;
case dusk::AudioOutputMode::StereoHeadphones:
case dusk::AudioOutputMode::Surround6ch:
case dusk::AudioOutputMode::Surround8ch:
return JAS_OUTPUT_SURROUND;
}
#else
return JASDriver::JAS_SYSTEM_OUTPUT_MODE;
#endif
}
void JASDriver::waitSubFrame() {
+13 -3
View File
@@ -745,16 +745,26 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) {
f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ();
#if TARGET_PC
if (dusk::audio::EnableHrtf) {
const auto mode = dusk::getSettings().audio.outputMode.getValue();
if (mode >= dusk::AudioOutputMode::StereoHeadphones) {
// Normalize the direction so result is purely front/back orientation,
// independent of how far away the sound is
f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z;
f32 lenSq = param_0.x * param_0.x + param_0.z * param_0.z;
if (mode == dusk::AudioOutputMode::StereoHeadphones) {
// original HRTF math
lenSq += param_0.y * param_0.y;
}
if (lenSq < 0.0001f) {
return 0.5f;
}
f32 zNorm = param_0.z / sqrtf(lenSq);
f32 t = (zNorm + 1.0f) * 0.5f;
return 0.5f - 0.5f * cosf(t * static_cast<f32>(M_PI));
if (mode == dusk::AudioOutputMode::StereoHeadphones) {
// original HRTF math
return 0.5f - 0.5f * cosf(t * static_cast<f32>(M_PI));
} else {
return t;
}
}
#endif
if (fVar1 > mSetting.field_0x48) {
+10 -21
View File
@@ -876,7 +876,7 @@ void dMenu_Option_c::vib_init() {
void dMenu_Option_c::vib_move() {
bool upTrigger = mpStick->checkUpTrigger();
bool downTrigger = mpStick->checkDownTrigger();
IF_NOT_DUSK(bool downTrigger =) mpStick->checkDownTrigger();
bool leftTrigger = checkLeftTrigger();
bool rightTrigger = checkRightTrigger();
@@ -891,10 +891,14 @@ void dMenu_Option_c::vib_move() {
field_0x3ef = PROC_ATTEN_e;
#endif
Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
} else if (downTrigger) {
}
#ifndef TARGET_PC
else if (downTrigger) {
field_0x3ef = OPTION_SELECT(PROC_SOUND_e);
Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
} else if (leftTrigger) {
}
#endif
else if (leftTrigger) {
if (isRumbleSupported()) {
if (field_0x3ea == 0) {
field_0x3ea = 1;
@@ -1369,8 +1373,7 @@ void dMenu_Option_c::calibration_close2_move() {
void dMenu_Option_c::menuVisible() {
for (int i = 0; i < 6; i++) {
if (i < OPTION_SELECT(PROC_CHANGE_MOVE_e))
{
if (i < OPTION_SELECT(DUSK_IF_ELSE(PROC_SOUND_e, PROC_CHANGE_MOVE_e))) {
menuShow(i);
} else {
menuHide(i);
@@ -2478,7 +2481,7 @@ bool dMenu_Option_c::isRumbleSupported() {
#if TARGET_PC
bool dMenu_Option_c::pointerConfirmSelect() {
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) {
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) {
if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
dusk::menu_pointer::set_hover_target(i);
return false;
@@ -2502,7 +2505,7 @@ bool dMenu_Option_c::pointerConfirmSelect() {
bool dMenu_Option_c::dpdMenuMove() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) {
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) {
if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
continue;
}
@@ -2582,20 +2585,6 @@ bool dMenu_Option_c::dpdMenuMove() {
-1.0f, 0);
}
return true;
case PROC_SOUND_e:
if (field_0x3e9 == 0) {
field_0x3e9 = 2;
} else {
field_0x3e9--;
}
field_0x3da = 5;
mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]);
setSoundMode(dMo_soundMode[field_0x3e9]);
field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e);
field_0x3f5 = OPTION_SELECT(PROC_SOUND_e);
Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
-1.0f, 0);
return true;
}
}
#endif
+65 -20
View File
@@ -20,7 +20,7 @@
using namespace dusk::audio;
static OutputSubframe OutBuffer;
static std::array<f32, DSP_SUBFRAME_SIZE * OutputSubframe::NUM_CHANNELS> OutInterleaveBuffer;
static std::array<f32, DSP_SUBFRAME_SIZE * OutputSubframe::NUM_CHANNELS> OutInterleaveBufferFull;
static SDL_AudioStream* PlaybackStream;
@@ -43,21 +43,55 @@ static int RenderNewAudioFrame();
/**
* Render an audio subframe and output it to SDL3.
*/
static void RenderAudioSubframe();
static int RenderAudioSubframe();
static void InitSDL3Output() {
SDL_Init(SDL_INIT_AUDIO);
static size_t GetChannelCountForOutputMode(dusk::AudioOutputMode config) {
switch (config) {
default:
case dusk::AudioOutputMode::StereoSpeakers:
case dusk::AudioOutputMode::StereoHeadphones:
return 2;
case dusk::AudioOutputMode::Surround6ch:
return 6;
case dusk::AudioOutputMode::Surround8ch:
return 8;
}
}
constexpr SDL_AudioSpec spec = {
static bool InitSDL3Output() {
const auto speakerConfig = dusk::getSettings().audio.outputMode.getValue();
const auto desiredChannelCount = GetChannelCountForOutputMode(speakerConfig);
const bool hrtf = speakerConfig == dusk::AudioOutputMode::StereoHeadphones;
if (PlaybackStream && desiredChannelCount == OutChannelCount) {
JASCriticalSection section;
EnableHrtf = hrtf;
return false;
}
if (PlaybackStream) {
SDL_PauseAudioStreamDevice(PlaybackStream);
SDL_DestroyAudioStream(PlaybackStream);
} else {
SDL_Init(SDL_INIT_AUDIO);
}
const SDL_AudioSpec spec = {
SDL_AUDIO_F32,
2,
static_cast<int>(desiredChannelCount),
SampleRate,
};
PlaybackStream = SDL_OpenAudioDeviceStream(
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&spec,
&GetNewAudio,
nullptr);
SDL_AudioStream* newStream =
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, &GetNewAudio, nullptr);
{
JASCriticalSection section;
EnableHrtf = hrtf;
OutChannelCount = desiredChannelCount;
PlaybackStream = newStream;
}
return true;
}
void dusk::audio::Initialize() {
@@ -72,6 +106,12 @@ void dusk::audio::Initialize() {
SDL_ResumeAudioStreamDevice(PlaybackStream);
}
void dusk::audio::Reinitialize() {
if (InitSDL3Output()) {
SDL_ResumeAudioStreamDevice(PlaybackStream);
}
}
void dusk::audio::SetMasterVolume(const f32 value) {
JASCriticalSection section;
@@ -113,53 +153,58 @@ int RenderNewAudioFrame() {
ZoneScoped;
JASCriticalSection section;
const u32 countSubframes = JASDriver::getSubFrames();
int bytesWritten = 0;
JASAudioThread::setDSPSyncCount(countSubframes);
for (u32 i = 0; i < countSubframes; i++) {
RenderAudioSubframe();
bytesWritten += RenderAudioSubframe();
JASAudioThread::snIntCount -= 1;
}
return static_cast<u16>(countSubframes) * sizeof(OutputSubframe);
return bytesWritten;
}
static void InterleaveOutputData(const OutputSubframe& data, std::span<f32> target) {
assert(target.size() >= data.channels[0].size() * OutputSubframe::NUM_CHANNELS);
assert(target.size() >= data.channels[0].size() * OutChannelCount);
size_t outPos = 0;
for (size_t inPos = 0; inPos < data.channels[0].size(); inPos++) {
for (size_t channelIdx = 0; channelIdx < OutputSubframe::NUM_CHANNELS; channelIdx++) {
for (size_t channelIdx = 0; channelIdx < OutChannelCount; channelIdx++) {
target[outPos++] = data.channels[channelIdx][inPos];
}
}
}
void RenderAudioSubframe() {
int RenderAudioSubframe() {
ZoneScoped;
OutBuffer = {};
JASDriver::updateDSP();
DspRender(OutBuffer);
std::span<f32> OutInterleaveBuffer{OutInterleaveBufferFull.data(), static_cast<size_t>(DSP_SUBFRAME_SIZE * OutChannelCount)};
InterleaveOutputData(OutBuffer, OutInterleaveBuffer);
if (JASDriver::extMixCallback != nullptr && JASDriver::sMixMode == MIX_MODE_INTERLEAVE) {
static_assert(OutputSubframe::NUM_CHANNELS == 2); // This code only works with Stereo so far.
// NOTE: In the real game, this gets called on the entire audio frame, rather than the subframe.
// That's probably more efficient, but I didn't wanna change the code to calculate the
// entire audio buffers at once.
// This is only used for the movie player, and it seems to work fine with the smaller calls.
const auto mixData = JASDriver::extMixCallback(DSP_SUBFRAME_SIZE);
if (mixData) {
for (int i = 0; i < OutInterleaveBuffer.size(); i++) {
OutInterleaveBuffer[i] += static_cast<f32>(mixData[i]) / static_cast<f32>(0x7FFF);
for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) {
const auto oi = i * OutChannelCount;
OutInterleaveBuffer[oi] += static_cast<f32>(mixData[i * 2]) / 32767.0f;
OutInterleaveBuffer[oi + 1] += static_cast<f32>(mixData[i * 2 + 1]) / 32767.0f;
}
}
}
SDL_PutAudioStreamData(PlaybackStream, &OutInterleaveBuffer, sizeof(OutInterleaveBuffer));
auto bytesToWrite = OutInterleaveBuffer.size_bytes();
SDL_PutAudioStreamData(PlaybackStream, OutInterleaveBuffer.data(), bytesToWrite);
return bytesToWrite;
}
u32 dusk::audio::GetResetCount(int channelIdx) {
+2
View File
@@ -19,6 +19,8 @@ namespace dusk::audio {
*/
void Initialize();
void Reinitialize();
void SetEnableReverb(bool value);
void SetMasterVolume(f32 value);
+411 -303
View File
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -8,14 +8,20 @@
#include <array>
#include <cassert>
#include <span>
namespace dusk::audio {
constexpr int SampleRate = 32000;
enum class OutputChannel : u8 {
LEFT,
RIGHT,
// same as SDL channel layout for 7.1
FRONT_LEFT,
FRONT_RIGHT,
FRONT_CENTER,
LFE,
REAR_LEFT,
REAR_RIGHT,
SURROUND_LEFT,
SURROUND_RIGHT,
OutputChannel_MAX
};
@@ -122,16 +128,11 @@ namespace dusk::audio {
return channel.mBytesPerBlock;
}
/**
* Apply a volume level to audio data.
* Interpolates across the two provided volume levels to avoid clicking.
*/
void ApplyVolume(std::span<f32> dst, std::span<f32> src, f32 startVolume, f32 endVolume);
extern f32 MasterVolume;
extern f32 PrevMasterVolume;
extern bool EnableReverb;
extern bool DumpAudio;
extern bool EnableHrtf;
extern f32 HrtfGain;
extern u8 OutChannelCount;
}
+2 -1
View File
@@ -316,6 +316,7 @@ template class ConfigImpl<BloomMode>;
template class ConfigImpl<DepthOfFieldMode>;
template class ConfigImpl<DiscVerificationState>;
template class ConfigImpl<GameLanguage>;
template class ConfigImpl<AudioOutputMode>;
template <>
void ConfigImpl<FrameInterpMode>::loadFromJson(
@@ -639,4 +640,4 @@ void shutdown() {
s_activeChangeNotifications.clear();
}
} // namespace dusk::config
} // namespace dusk::config
+2 -2
View File
@@ -25,13 +25,13 @@ UserSettings g_userSettings = {
},
.audio = {
.outputMode {"audio.outputMode", AudioOutputMode::StereoSpeakers},
.masterVolume {"audio.masterVolume", 60},
.mainMusicVolume {"audio.mainMusicVolume", 100},
.subMusicVolume {"audio.subMusicVolume", 100},
.soundEffectsVolume {"audio.soundEffectsVolume", 100},
.fanfareVolume {"audio.fanfareVolume", 100},
.enableReverb {"audio.enableReverb", true},
.enableHrtf {"audio.enableHrtf", false},
.menuSounds {"audio.menuSounds", true},
},
@@ -256,13 +256,13 @@ void registerSettings() {
[](const int&, const int&) { dusk::ui::apply_scale(); });
// Audio
Register(g_userSettings.audio.outputMode);
Register(g_userSettings.audio.masterVolume);
Register(g_userSettings.audio.mainMusicVolume);
Register(g_userSettings.audio.subMusicVolume);
Register(g_userSettings.audio.soundEffectsVolume);
Register(g_userSettings.audio.fanfareVolume);
Register(g_userSettings.audio.enableReverb);
Register(g_userSettings.audio.enableHrtf);
Register(g_userSettings.audio.menuSounds);
// Game
+14 -1
View File
@@ -68,6 +68,13 @@ enum class MagicArmorMode : u8 {
COSMETIC = 4,
};
enum class AudioOutputMode : u8 {
StereoSpeakers = 0,
StereoHeadphones = 1, // spatial audio
Surround6ch = 2, // discrete 5.1
Surround8ch = 3, // discrete 7.1
};
namespace config {
template <>
struct ConfigEnumRange<BloomMode> {
@@ -123,6 +130,12 @@ struct ConfigEnumRange<MagicArmorMode> {
static constexpr auto max = MagicArmorMode::COSMETIC;
};
template <>
struct ConfigEnumRange<AudioOutputMode> {
static constexpr auto min = AudioOutputMode::StereoSpeakers;
static constexpr auto max = AudioOutputMode::Surround8ch;
};
template <>
struct ConfigValueTraits<ui::ControlLayout> {
static constexpr bool enabled = true;
@@ -150,13 +163,13 @@ struct UserSettings {
struct {
// Audio
ConfigVar<AudioOutputMode> outputMode;
ConfigVar<int> masterVolume;
ConfigVar<int> mainMusicVolume;
ConfigVar<int> subMusicVolume;
ConfigVar<int> soundEffectsVolume;
ConfigVar<int> fanfareVolume;
ConfigVar<bool> enableReverb;
ConfigVar<bool> enableHrtf;
ConfigVar<bool> menuSounds;
} audio;
+36 -7
View File
@@ -69,6 +69,13 @@ constexpr std::array kInterpolationModes = {
"Unlimited",
};
constexpr std::array kAudioOutputModeNames = {
"Stereo (Speakers)",
"Stereo (Headphones)",
"5.1 Surround",
"7.1 Surround",
};
constexpr std::array kTouchTargetingLabels = {
"Hybrid",
"Hold",
@@ -1041,6 +1048,35 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Output");
leftPane.register_control(
leftPane.add_select_button({
.key = "Output Mode",
.getValue = [] {
const auto idx = static_cast<int>(getSettings().audio.outputMode.getValue());
return Rml::String{kAudioOutputModeNames[idx]};
},
.isModified = [] {
const auto& setting = getSettings().audio.outputMode;
return setting.getValue() != setting.getDefaultValue();
},
}), rightPane, [](Pane& pane) {
for (int i = 0; i < static_cast<int>(kAudioOutputModeNames.size()); ++i) {
pane.add_button({
.text = kAudioOutputModeNames[i],
.isSelected = [i] {
const auto& setting = getSettings().audio.outputMode;
return setting.getValue() == static_cast<AudioOutputMode>(i);
},
}).on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().audio.outputMode.setValue(static_cast<AudioOutputMode>(i));
config::save();
audio::Reinitialize();
});
}
});
// TODO: Individual sliders for Main Music, Sub Music, Sound Effects, and Fanfare.
leftPane.add_section("Volume");
leftPane.register_control(
@@ -1073,13 +1109,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.helpText = "Enables the reverb effect in game audio.",
.onChange = [](bool value) { audio::SetEnableReverb(value); },
});
config_bool_select(leftPane, rightPane, getSettings().audio.enableHrtf,
{
.key = "Enable Spatial Sound",
.helpText =
"Emulate surround sound via HRTF. Recommended only for use with headphones!",
.onChange = [](bool value) { audio::EnableHrtf = value; },
});
config_bool_select(leftPane, rightPane, getSettings().audio.menuSounds,
{
.key = "Dusklight Menu Sounds",
-1
View File
@@ -731,7 +731,6 @@ int game_main(int argc, char* argv[]) {
dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f));
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
// Run ImGui UI loop if Aurora couldn't initialize a backend
if (auroraInfo.backend == BACKEND_NULL) {