diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index aff785773..e72a7122f 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -47,6 +47,12 @@ enum AudioBackend : int { // Add more backends as needed }; +enum OpenALHrtfMode : int { + HrtfAuto, // Let OpenAL Soft decide (on for headphone-like stereo outputs) + HrtfOn, // Force HRTF binaural rendering + HrtfOff, // Never use HRTF +}; + template struct Setting { T default_value{}; @@ -336,6 +342,7 @@ struct AudioSettings { Setting openal_mic_device{"Default Device"}; Setting openal_main_output_device{"Default Device"}; Setting openal_padSpk_output_device{"Default Device"}; + Setting openal_hrtf{OpenALHrtfMode::HrtfAuto}; std::vector GetOverrideableFields() const { return std::vector{ @@ -349,14 +356,15 @@ struct AudioSettings { make_override("openal_main_output_device", &AudioSettings::openal_main_output_device), make_override("openal_padSpk_output_device", - &AudioSettings::openal_padSpk_output_device)}; + &AudioSettings::openal_padSpk_output_device), + make_override("openal_hrtf", &AudioSettings::openal_hrtf)}; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, audio_backend, sdl_mic_device, sdl_main_output_device, sdl_padSpk_output_device, openal_mic_device, openal_main_output_device, - openal_padSpk_output_device) + openal_padSpk_output_device, openal_hrtf) // ------------------------------- // GPU settings @@ -632,6 +640,7 @@ public: SETTING_FORWARD(m_audio, OpenALMicDevice, openal_mic_device) SETTING_FORWARD(m_audio, OpenALMainOutputDevice, openal_main_output_device) SETTING_FORWARD(m_audio, OpenALPadSpkOutputDevice, openal_padSpk_output_device) + SETTING_FORWARD(m_audio, OpenALHrtf, openal_hrtf) // Debug settings SETTING_FORWARD_BOOL(m_debug, DebugDump, debug_dump) diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp index 015a32d3e..28dce421f 100644 --- a/src/core/libraries/audio/openal_audio_out.cpp +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -10,6 +10,22 @@ #include #include #include + +// Fallbacks in case the alext.h in use predates these extensions. +#ifndef ALC_SOFT_output_mode +#define ALC_OUTPUT_MODE_SOFT 0x19AC +#define ALC_MONO_SOFT 0x1500 +#define ALC_STEREO_SOFT 0x1501 +#define ALC_STEREO_BASIC_SOFT 0x19AE +#define ALC_STEREO_UHJ_SOFT 0x19AF +#define ALC_STEREO_HRTF_SOFT 0x19B2 +#define ALC_SURROUND_5_1_SOFT 0x1504 +#define ALC_SURROUND_6_1_SOFT 0x1505 +#define ALC_SURROUND_7_1_SOFT 0x1506 +#endif +#ifndef AL_SOFT_direct_channels_remix +#define AL_REMIX_UNMATCHED_SOFT 0x0002 +#endif #include #include "common/logging/log.h" #include "core/emulator_settings.h" @@ -98,6 +114,10 @@ public: convert(ptr, al_buffer_s16.data(), buffer_frames, nullptr); } + if (fold_lfe) { + FoldLfeIntoFronts(); + } + // Reclaim processed buffers ALint processed = 0; alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); @@ -166,6 +186,7 @@ public: const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; max_channel_gain = std::max(max_channel_gain, channel_gain); } + game_gain.store(max_channel_gain, std::memory_order_release); const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; const float total_gain = max_channel_gain * slider_gain; @@ -235,13 +256,48 @@ private: return false; } + ALCdevice* alc_dev = alcGetContextsDevice(alcGetCurrentContext()); + ALCint output_mode = 0; + bool know_output_mode = false; + if (alc_dev && alcIsExtensionPresent(alc_dev, "ALC_SOFT_output_mode")) { + alcGetIntegerv(alc_dev, ALC_OUTPUT_MODE_SOFT, 1, &output_mode); + know_output_mode = true; + } + const bool output_has_lfe = output_mode == ALC_SURROUND_5_1_SOFT || + output_mode == ALC_SURROUND_6_1_SOFT || + output_mode == ALC_SURROUND_7_1_SOFT; + fold_lfe = know_output_mode && !output_has_lfe && num_channels >= 6 && !downmix_to_stereo; + if (fold_lfe) { + LOG_INFO(Lib_AudioOut, "Output has no LFE channel; folding buffer LFE into fronts"); + } + if (alc_dev && alcIsExtensionPresent(alc_dev, "ALC_SOFT_HRTF")) { + ALCint hrtf_on = 0; + alcGetIntegerv(alc_dev, ALC_HRTF_SOFT, 1, &hrtf_on); + hrtf_active = hrtf_on == ALC_TRUE; + } + + const bool stereo_output = output_mode == ALC_MONO_SOFT || output_mode == ALC_STEREO_SOFT || + output_mode == ALC_STEREO_BASIC_SOFT || + output_mode == ALC_STEREO_UHJ_SOFT || + output_mode == ALC_STEREO_HRTF_SOFT; + if (know_output_mode && stereo_output && !hrtf_active && num_channels >= 6 && + !downmix_to_stereo) { + downmix_to_stereo = true; + use_native_float = has_float_ext; + format = has_float_ext ? AL_FORMAT_STEREO_FLOAT32 : AL_FORMAT_STEREO16; + fold_lfe = false; // The downmix converters already carry LFE. + LOG_INFO(Lib_AudioOut, "Stereo output: using internal {}ch->stereo downmix ({})", + num_channels, has_float_ext ? "float" : "int16"); + } + // Allocate buffers based on format + const u32 out_channels = downmix_to_stereo ? 2u : num_channels; if (use_native_float) { - al_buffer_float.resize(buffer_frames * num_channels); - buffer_size_bytes = buffer_frames * num_channels * sizeof(float); + al_buffer_float.resize(buffer_frames * out_channels); + buffer_size_bytes = buffer_frames * out_channels * sizeof(float); } else { - al_buffer_s16.resize(buffer_frames * num_channels); - buffer_size_bytes = buffer_frames * num_channels * sizeof(s16); + al_buffer_s16.resize(buffer_frames * out_channels); + buffer_size_bytes = buffer_frames * out_channels * sizeof(s16); } // Select optimal converter function @@ -332,7 +388,8 @@ private: last_volume_check_time = current_time; - const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f; + const float config_volume = + EmulatorSettings.GetVolumeSlider() * 0.01f * game_gain.load(std::memory_order_acquire); const float stored_gain = current_gain.load(std::memory_order_acquire); if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) { @@ -372,6 +429,8 @@ private: } bool DetermineOpenALFormat() { + alGetError(); + // Try to use native float formats if extension is available if (is_float && has_float_ext) { switch (num_channels) { @@ -431,6 +490,7 @@ private: if (format == 0 || alGetError() != AL_NO_ERROR) { LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); format = AL_FORMAT_STEREO16; + downmix_to_stereo = true; } break; case 8: @@ -438,6 +498,7 @@ private: if (format == 0 || alGetError() != AL_NO_ERROR) { LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); format = AL_FORMAT_STEREO16; + downmix_to_stereo = true; } break; default: @@ -458,6 +519,7 @@ private: if (format == 0 || alGetError() != AL_NO_ERROR) { LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); format = AL_FORMAT_STEREO16; + downmix_to_stereo = true; } break; case 8: @@ -465,6 +527,7 @@ private: if (format == 0 || alGetError() != AL_NO_ERROR) { LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); format = AL_FORMAT_STEREO16; + downmix_to_stereo = true; } break; default: @@ -501,11 +564,38 @@ private: alSourcei(source, AL_LOOPING, AL_FALSE); alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); + if (alIsExtensionPresent("AL_SOFT_direct_channels")) { + if (num_channels == 2 || downmix_to_stereo) { + alSourcei(source, AL_DIRECT_CHANNELS_SOFT, AL_TRUE); + } else if (num_channels > 2 && !hrtf_active && + alIsExtensionPresent("AL_SOFT_direct_channels_remix")) { + alSourcei(source, AL_DIRECT_CHANNELS_SOFT, AL_REMIX_UNMATCHED_SOFT); + } + } + LOG_DEBUG(Lib_AudioOut, "Created OpenAL source {} with {} buffers", source, buffers.size()); return true; } bool SelectConverter() { + if (downmix_to_stereo) { + if (use_native_float) { + if (is_float) { + convert = + num_channels == 8 ? &DownmixF32_8CHToStereoF32 : &DownmixF32_6CHToStereoF32; + } else { + convert = + num_channels == 8 ? &DownmixS16_8CHToStereoF32 : &DownmixS16_6CHToStereoF32; + } + } else if (is_float) { + convert = + num_channels == 8 ? &DownmixF32_8CHToStereoS16 : &DownmixF32_6CHToStereoS16; + } else { + convert = num_channels == 8 ? &DownmixS16_8CHToStereo : &DownmixS16_6CHToStereo; + } + return true; + } + if (is_float && use_native_float) { // Native float - just copy/remap if needed switch (num_channels) { @@ -556,7 +646,7 @@ private: convert = &ConvertS16Stereo; break; case 8: - convert = &ConvertS16_8CH; + convert = is_std ? &ConvertS16Std8CH : &ConvertS16_8CH; break; default: LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); @@ -624,6 +714,146 @@ private: const u32 num_samples = frames << 3; std::memcpy(d, s, num_samples * sizeof(s16)); } + static void ConvertS16Std8CH(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const u32 offset = i << 3; + d[offset + FL] = s[offset + FL]; + d[offset + FR] = s[offset + FR]; + d[offset + FC] = s[offset + FC]; + d[offset + LF] = s[offset + LF]; + d[offset + SL] = s[offset + STD_SL]; + d[offset + SR] = s[offset + STD_SR]; + d[offset + BL] = s[offset + STD_BL]; + d[offset + BR] = s[offset + STD_BR]; + } + } + + static inline s16 ClampSampleToS16(const float v) { + return static_cast(std::clamp(v, -32768.0f, 32767.0f)); + } + + static void DownmixS16_6CHToStereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i * 6; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = ClampSampleToS16(s[o + FL] + center + lfe + 0.7071f * s[o + 4]); + d[i * 2 + 1] = ClampSampleToS16(s[o + FR] + center + lfe + 0.7071f * s[o + 5]); + } + } + static void DownmixS16_8CHToStereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i << 3; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = + ClampSampleToS16(s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6])); + d[i * 2 + 1] = + ClampSampleToS16(s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7])); + } + } + static void DownmixF32_6CHToStereoS16(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i * 6; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = OrbisFloatToS16(s[o + FL] + center + lfe + 0.7071f * s[o + 4]); + d[i * 2 + 1] = OrbisFloatToS16(s[o + FR] + center + lfe + 0.7071f * s[o + 5]); + } + } + + void FoldLfeIntoFronts() { + constexpr float LFE_GAIN = 0.7071f; + if (use_native_float) { + float* d = al_buffer_float.data(); + for (u32 i = 0; i < buffer_frames; i++) { + float* f = d + static_cast(i) * num_channels; + const float lfe = f[LF] * LFE_GAIN; + f[FL] += lfe; + f[FR] += lfe; + f[LF] = 0.0f; + } + } else { + s16* d = al_buffer_s16.data(); + for (u32 i = 0; i < buffer_frames; i++) { + s16* f = d + static_cast(i) * num_channels; + const float lfe = static_cast(f[LF]) * LFE_GAIN; + f[FL] = ClampSampleToS16(static_cast(f[FL]) + lfe); + f[FR] = ClampSampleToS16(static_cast(f[FR]) + lfe); + f[LF] = 0; + } + } + } + + static void DownmixS16_6CHToStereoF32(const void* src, void* dst, u32 frames, const float*) { + constexpr float INV = 1.0f / 32768.0f; + const s16* s = static_cast(src); + float* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i * 6; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = (s[o + FL] + center + lfe + 0.7071f * s[o + 4]) * INV; + d[i * 2 + 1] = (s[o + FR] + center + lfe + 0.7071f * s[o + 5]) * INV; + } + } + static void DownmixS16_8CHToStereoF32(const void* src, void* dst, u32 frames, const float*) { + constexpr float INV = 1.0f / 32768.0f; + const s16* s = static_cast(src); + float* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i << 3; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = (s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6])) * INV; + d[i * 2 + 1] = (s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7])) * INV; + } + } + static void DownmixF32_6CHToStereoF32(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i * 6; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = s[o + FL] + center + lfe + 0.7071f * s[o + 4]; + d[i * 2 + 1] = s[o + FR] + center + lfe + 0.7071f * s[o + 5]; + } + } + static void DownmixF32_8CHToStereoF32(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i << 3; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6]); + d[i * 2 + 1] = s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7]); + } + } + + static void DownmixF32_8CHToStereoS16(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + for (u32 i = 0; i < frames; i++) { + const u32 o = i << 3; + const float center = 0.7071f * s[o + FC]; + const float lfe = 0.5f * s[o + LF]; + d[i * 2 + 0] = + OrbisFloatToS16(s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6])); + d[i * 2 + 1] = + OrbisFloatToS16(s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7])); + } + } // Float passthrough converters (for AL_EXT_FLOAT32) static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) { @@ -814,12 +1044,16 @@ private: // Extension support bool has_float_ext{false}; bool use_native_float{false}; + bool downmix_to_stereo{false}; + bool fold_lfe{false}; + bool hrtf_active{false}; // Converter function pointer ConverterFunc convert{nullptr}; // Volume management alignas(64) std::atomic current_gain{1.0f}; + std::atomic game_gain{1.0f}; std::string device_name; bool device_registered; diff --git a/src/core/libraries/audio/openal_manager.h b/src/core/libraries/audio/openal_manager.h index 4a6ef7920..faafe3fa5 100644 --- a/src/core/libraries/audio/openal_manager.h +++ b/src/core/libraries/audio/openal_manager.h @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include #include #include @@ -8,6 +9,22 @@ #include #include +#include "common/logging/log.h" +#include "core/emulator_settings.h" + +// ALC_SOFT_HRTF constants, in case the alext.h in use predates the extension. +#ifndef ALC_SOFT_HRTF +#define ALC_HRTF_SOFT 0x1992 +#define ALC_DONT_CARE_SOFT 0x0002 +#define ALC_HRTF_STATUS_SOFT 0x1993 +#define ALC_HRTF_DISABLED_SOFT 0x0000 +#define ALC_HRTF_ENABLED_SOFT 0x0001 +#define ALC_HRTF_DENIED_SOFT 0x0002 +#define ALC_HRTF_REQUIRED_SOFT 0x0003 +#define ALC_HRTF_HEADPHONES_DETECTED_SOFT 0x0004 +#define ALC_HRTF_UNSUPPORTED_FORMAT_SOFT 0x0005 +#endif + namespace Libraries::AudioOut { struct DeviceContext { @@ -196,8 +213,23 @@ private: return false; } - // Create context - ctx.context = alcCreateContext(ctx.device, nullptr); + std::array attrs{}; + std::size_t attr_count = 0; + attrs[attr_count++] = ALC_FREQUENCY; + attrs[attr_count++] = 48000; + + const bool has_hrtf_ext = alcIsExtensionPresent(ctx.device, "ALC_SOFT_HRTF"); + if (has_hrtf_ext) { + const u32 hrtf_mode = EmulatorSettings.GetOpenALHrtf(); + const ALCint hrtf_value = hrtf_mode == OpenALHrtfMode::HrtfOn ? ALC_TRUE + : hrtf_mode == OpenALHrtfMode::HrtfOff ? ALC_FALSE + : ALC_DONT_CARE_SOFT; + attrs[attr_count++] = ALC_HRTF_SOFT; + attrs[attr_count++] = hrtf_value; + } + attrs[attr_count] = 0; + + ctx.context = alcCreateContext(ctx.device, attrs.data()); if (!ctx.context) { LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context"); alcCloseDevice(ctx.device); @@ -213,11 +245,47 @@ private: actual_name = alcGetString(ctx.device, ALC_DEVICE_SPECIFIER); } ctx.device_name = actual_name ? actual_name : "Unknown"; + ALCint mixer_rate = 0; + alcGetIntegerv(ctx.device, ALC_FREQUENCY, 1, &mixer_rate); + LOG_INFO(Lib_AudioOut, "OpenAL mixer rate for '{}': {} Hz", ctx.device_name, mixer_rate); + if (mixer_rate != 0 && mixer_rate != 48000) { + LOG_WARNING(Lib_AudioOut, + "OpenAL mixer is not running at 48000 Hz per-source resampling active"); + } + + if (has_hrtf_ext) { + ALCint status = ALC_HRTF_DISABLED_SOFT; + alcGetIntegerv(ctx.device, ALC_HRTF_STATUS_SOFT, 1, &status); + LOG_INFO(Lib_AudioOut, "OpenAL HRTF status for '{}': {}", ctx.device_name, + HrtfStatusString(status)); + } else { + LOG_INFO(Lib_AudioOut, "OpenAL device '{}' does not support ALC_SOFT_HRTF", + ctx.device_name); + } LOG_INFO(Lib_AudioOut, "OpenAL device initialized: '{}'", ctx.device_name); return true; } + static const char* HrtfStatusString(const ALCint status) { + switch (status) { + case ALC_HRTF_DISABLED_SOFT: + return "disabled"; + case ALC_HRTF_ENABLED_SOFT: + return "enabled"; + case ALC_HRTF_DENIED_SOFT: + return "denied by configuration"; + case ALC_HRTF_REQUIRED_SOFT: + return "required by configuration"; + case ALC_HRTF_HEADPHONES_DETECTED_SOFT: + return "enabled (headphones detected)"; + case ALC_HRTF_UNSUPPORTED_FORMAT_SOFT: + return "unsupported output format"; + default: + return "unknown"; + } + } + std::unordered_map devices; mutable std::mutex mutex; ALCcontext* current_context{nullptr}; // For thread-local tracking diff --git a/src/core/libraries/audio/sdl_audio_out.cpp b/src/core/libraries/audio/sdl_audio_out.cpp index b6706eff7..059376c12 100644 --- a/src/core/libraries/audio/sdl_audio_out.cpp +++ b/src/core/libraries/audio/sdl_audio_out.cpp @@ -109,6 +109,7 @@ public: const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; max_channel_gain = std::max(max_channel_gain, channel_gain); } + game_gain.store(max_channel_gain, std::memory_order_release); const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; // Faster than /100.0f const float total_gain = max_channel_gain * slider_gain; @@ -201,7 +202,8 @@ private: last_volume_check_time = current_time; - const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f; + const float config_volume = + EmulatorSettings.GetVolumeSlider() * 0.01f * game_gain.load(std::memory_order_acquire); const float stored_gain = current_gain.load(std::memory_order_acquire); // Only update if the difference is significant @@ -587,6 +589,7 @@ private: // Volume management alignas(64) std::atomic current_gain{1.0f}; + std::atomic game_gain{1.0f}; // SDL audio stream SDL_AudioStream* stream{nullptr}; diff --git a/src/core/libraries/audio3d/audio3d.cpp b/src/core/libraries/audio3d/audio3d.cpp index c6a444515..0d85d5c2a 100644 --- a/src/core/libraries/audio3d/audio3d.cpp +++ b/src/core/libraries/audio3d/audio3d.cpp @@ -24,15 +24,67 @@ static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2; static std::unique_ptr state; +struct AudioOutBufferInfo { + u32 channels; + u32 sample_size; +}; + +static AudioOutBufferInfo GetAudioOutBufferInfo(const AudioOut::OrbisAudioOutParamFormat format) { + using Format = AudioOut::OrbisAudioOutParamFormat; + switch (format) { + case Format::S16Mono: + return {1, sizeof(s16)}; + case Format::S16Stereo: + return {2, sizeof(s16)}; + case Format::S16_8CH: + case Format::S16_8CH_Std: + return {8, sizeof(s16)}; + case Format::FloatMono: + return {1, sizeof(float)}; + case Format::FloatStereo: + return {2, sizeof(float)}; + case Format::Float_8CH: + case Format::Float_8CH_Std: + return {8, sizeof(float)}; + default: + return {2, sizeof(s16)}; + } +} + +static s32 DrainAssociatedPorts(Port& port) { + while (true) { + s32 handle = -1; + std::vector buffer; + { + std::scoped_lock lock{port.mutex}; + const auto it = + std::find_if(port.audioout_ports.begin(), port.audioout_ports.end(), + [](const AssociatedAudioOutPort& p) { return !p.pending.empty(); }); + if (it == port.audioout_ports.end()) { + return ORBIS_OK; + } + handle = it->handle; + buffer = std::move(it->pending.front()); + it->pending.pop_front(); + } + + const s32 ret = AudioOut::sceAudioOutOutput(handle, buffer.data()); + if (ret < 0) { + return ret; + } + } +} + s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { LOG_INFO(Lib_Audio3d, "called, handle = {}", handle); - // Remove from any port that was tracking this handle. + // Remove from any port that was tracking this handle. Pending buffers that + // were never pushed are discarded, matching an immediate close. if (state) { for (auto& [port_id, port] : state->ports) { std::scoped_lock lock{port.mutex}; - auto& handles = port.audioout_handles; - handles.erase(std::remove(handles.begin(), handles.end(), handle), handles.end()); + std::erase_if(port.audioout_ports, + [&](const AssociatedAudioOutPort& p) { return p.handle == handle; }); } } @@ -64,8 +116,12 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( return handle; } - // Track this handle in the port so sceAudio3dPortFlush can use it for sync. - state->ports[port_id].audioout_handles.push_back(handle); + const auto info = GetAudioOutBufferInfo(param.data_format.Value()); + AssociatedAudioOutPort aout{}; + aout.handle = handle; + aout.buffer_bytes = len * info.channels * info.sample_size; + aout.samples_per_buffer = len * info.channels; + state->ports[port_id].audioout_ports.push_back(std::move(aout)); return handle; } @@ -82,6 +138,36 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { return ORBIS_AUDIO3D_ERROR_INVALID_PORT; } + if (state) { + for (auto& [port_id, port] : state->ports) { + std::scoped_lock lock{port.mutex}; + for (auto& aout : port.audioout_ports) { + if (aout.handle != handle) { + continue; + } + + if (aout.pending.size() >= port.parameters.queue_depth) { + LOG_DEBUG(Lib_Audio3d, + "AudioOut handle {} queue full ({}) without Push, " + "submitting oldest", + handle, aout.pending.size()); + std::vector oldest = std::move(aout.pending.front()); + aout.pending.pop_front(); + const s32 ret = AudioOut::sceAudioOutOutput(handle, oldest.data()); + if (ret < 0) { + return ret; + } + } + + const u8* src = static_cast(ptr); + aout.pending.emplace_back(src, src + aout.buffer_bytes); + + // Mirror sceAudioOutOutput's return of samples sent. + return static_cast(aout.samples_per_buffer); + } + } + } + return AudioOut::sceAudioOutOutput(handle, ptr); } @@ -94,7 +180,14 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* p return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } - return AudioOut::sceAudioOutOutputs(param, num); + for (u32 i = 0; i < num; i++) { + const s32 ret = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr); + if (ret < 0) { + return ret; + } + } + + return ORBIS_OK; } static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm& pcm, @@ -593,10 +686,10 @@ s32 PS4_SYSV_ABI sceAudio3dPortClose(const OrbisAudio3dPortId port_id) { port.audio_out_handle = -1; } - for (const s32 handle : port.audioout_handles) { - AudioOut::sceAudioOutClose(handle); + for (const auto& aout : port.audioout_ports) { + AudioOut::sceAudioOutClose(aout.handle); } - port.audioout_handles.clear(); + port.audioout_ports.clear(); for (auto& data : port.mixed_queue) { std::free(data.sample_buffer); @@ -652,9 +745,12 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { auto& port = state->ports[port_id]; std::scoped_lock lock{port.mutex}; - if (!port.audioout_handles.empty()) { - for (const s32 handle : port.audioout_handles) { - const s32 ret = AudioOut::sceAudioOutOutput(handle, nullptr); + if (!port.audioout_ports.empty()) { + if (const s32 ret = DrainAssociatedPorts(port); ret < 0) { + return ret; + } + for (const auto& aout : port.audioout_ports) { + const s32 ret = AudioOut::sceAudioOutOutput(aout.handle, nullptr); if (ret < 0) { return ret; } @@ -957,6 +1053,10 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, const u32 depth = port.parameters.queue_depth; + if (const s32 ret = DrainAssociatedPorts(port); ret < 0) { + return ret; + } + if (port.audio_out_handle < 0) { AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); diff --git a/src/core/libraries/audio3d/audio3d.h b/src/core/libraries/audio3d/audio3d.h index 7e298696d..018b46baa 100644 --- a/src/core/libraries/audio3d/audio3d.h +++ b/src/core/libraries/audio3d/audio3d.h @@ -103,13 +103,21 @@ struct ObjectState { std::unordered_map> persistent_attributes; }; +// An AudioOut port opened by the game through sceAudio3dAudioOutOpen. +struct AssociatedAudioOutPort { + s32 handle{-1}; + u32 buffer_bytes{0}; + u32 samples_per_buffer{0}; + std::deque> pending; +}; + struct Port { mutable std::recursive_mutex mutex; OrbisAudio3dOpenParameters parameters{}; // Opened lazily on the first sceAudio3dPortPush call. s32 audio_out_handle{-1}; - // Handles explicitly opened by the game via sceAudio3dAudioOutOpen. - std::vector audioout_handles; + // AudioOut ports explicitly opened by the game via sceAudio3dAudioOutOpen. + std::vector audioout_ports; // Reserved objects and their state. std::unordered_map objects; // increasing counter for generating unique object IDs within this port. diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 43d7d02c7..c0a713f50 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -2,16 +2,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include +#include +#include #include +#include +#include +#include +#include #include #include "common/assert.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/audio/audioout.h" #include "core/libraries/audio/audioout_error.h" +#include "core/libraries/audio/openal_manager.h" #include "core/libraries/audio3d/audio3d_error.h" #include "core/libraries/audio3d/audio3d_openal.h" #include "core/libraries/error_codes.h" +#include "core/libraries/kernel/time.h" #include "core/libraries/libs.h" namespace Libraries::Audio3dOpenAL { @@ -24,15 +35,650 @@ static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2; static std::unique_ptr state; +struct AudioOutBufferInfo { + u32 channels; + u32 sample_size; +}; + +static AudioOutBufferInfo GetAudioOutBufferInfo(const AudioOut::OrbisAudioOutParamFormat format) { + using Format = AudioOut::OrbisAudioOutParamFormat; + switch (format) { + case Format::S16Mono: + return {1, sizeof(s16)}; + case Format::S16Stereo: + return {2, sizeof(s16)}; + case Format::S16_8CH: + case Format::S16_8CH_Std: + return {8, sizeof(s16)}; + case Format::FloatMono: + return {1, sizeof(float)}; + case Format::FloatStereo: + return {2, sizeof(float)}; + case Format::Float_8CH: + case Format::Float_8CH_Std: + return {8, sizeof(float)}; + default: + return {2, sizeof(s16)}; + } +} + +static s32 DrainAssociatedPorts(Port& port) { + while (true) { + s32 handle = -1; + std::vector buffer; + { + std::scoped_lock lock{port.mutex}; + const auto it = + std::find_if(port.audioout_ports.begin(), port.audioout_ports.end(), + [](const AssociatedAudioOutPort& p) { return !p.pending.empty(); }); + if (it == port.audioout_ports.end()) { + return ORBIS_OK; + } + handle = it->handle; + buffer = std::move(it->pending.front()); + it->pending.pop_front(); + } + + const s32 ret = AudioOut::sceAudioOutOutput(handle, buffer.data()); + if (ret < 0) { + return ret; + } + } +} + +static constexpr u32 SPATIAL_RING_SLACK = 2; +static constexpr u64 SPATIAL_VOLUME_CHECK_INTERVAL_US = 50000; +// Give up on a frame if the device hasn't consumed anything for this long. +static constexpr u64 SPATIAL_SUBMIT_TIMEOUT_US = 1000000; +// Minimum sleep while waiting for a free ring slot. +static constexpr u64 SPATIAL_MIN_SLEEP_US = 500; + +static const char* ALErrorString(const ALenum error) { + switch (error) { + case AL_NO_ERROR: + return "AL_NO_ERROR"; + case AL_INVALID_NAME: + return "AL_INVALID_NAME"; + case AL_INVALID_ENUM: + return "AL_INVALID_ENUM"; + case AL_INVALID_VALUE: + return "AL_INVALID_VALUE"; + case AL_INVALID_OPERATION: + return "AL_INVALID_OPERATION"; + case AL_OUT_OF_MEMORY: + return "AL_OUT_OF_MEMORY"; + default: + return "Unknown AL error"; + } +} + +static void SpatialReclaimSource(SpatialSource& src) { + ALint processed = 0; + alGetSourcei(src.source, AL_BUFFERS_PROCESSED, &processed); + + while (processed-- > 0) { + ALuint buffer_id = 0; + alSourceUnqueueBuffers(src.source, 1, &buffer_id); + if (alGetError() != AL_NO_ERROR) { + break; + } + src.available.push_back(buffer_id); + } +} + +static bool CreateSpatialSource(const Port& port, SpatialSource& src) { + alGetError(); + + ALuint source = 0; + alGenSources(1, &source); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to generate spatial source"); + return false; + } + + const u32 ring_size = port.parameters.queue_depth + SPATIAL_RING_SLACK; + src.buffers.resize(ring_size); + alGenBuffers(static_cast(ring_size), reinterpret_cast(src.buffers.data())); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to generate spatial buffers"); + alDeleteSources(1, &source); + src.buffers.clear(); + return false; + } + src.available = src.buffers; + + alSourcef(source, AL_PITCH, 1.0f); + alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + alSourcei(source, AL_LOOPING, AL_FALSE); + alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); + alSourcef(source, AL_MAX_GAIN, 16.0f); + + src.source = source; + return true; +} + +static void DestroySpatialSourceLocked(SpatialSource& src) { + if (src.source != 0) { + alSourceStop(src.source); + + ALint queued = 0; + alGetSourcei(src.source, AL_BUFFERS_QUEUED, &queued); + while (queued-- > 0) { + ALuint buffer_id = 0; + alSourceUnqueueBuffers(src.source, 1, &buffer_id); + } + + alDeleteSources(1, &src.source); + } + if (!src.buffers.empty()) { + alDeleteBuffers(static_cast(src.buffers.size()), + reinterpret_cast(src.buffers.data())); + } + src = SpatialSource{}; +} + +static void FreeBundle(SpatialFrameBundle& bundle) { + std::free(bundle.bed.sample_buffer); + bundle.bed.sample_buffer = nullptr; + for (auto& frame : bundle.objects) { + std::free(frame.pcm.sample_buffer); + frame.pcm.sample_buffer = nullptr; + } + bundle.objects.clear(); +} + +static LPALGENEFFECTS p_alGenEffects; +static LPALDELETEEFFECTS p_alDeleteEffects; +static LPALEFFECTI p_alEffecti; +static LPALEFFECTF p_alEffectf; +static LPALGENAUXILIARYEFFECTSLOTS p_alGenAuxiliaryEffectSlots; +static LPALDELETEAUXILIARYEFFECTSLOTS p_alDeleteAuxiliaryEffectSlots; +static LPALAUXILIARYEFFECTSLOTI p_alAuxiliaryEffectSloti; +static LPALAUXILIARYEFFECTSLOTF p_alAuxiliaryEffectSlotf; + +static bool LoadEfxFunctions() { + static bool attempted = false; + static bool ok = false; + if (attempted) { + return ok; + } + attempted = true; + + p_alGenEffects = reinterpret_cast(alGetProcAddress("alGenEffects")); + p_alDeleteEffects = reinterpret_cast(alGetProcAddress("alDeleteEffects")); + p_alEffecti = reinterpret_cast(alGetProcAddress("alEffecti")); + p_alEffectf = reinterpret_cast(alGetProcAddress("alEffectf")); + p_alGenAuxiliaryEffectSlots = reinterpret_cast( + alGetProcAddress("alGenAuxiliaryEffectSlots")); + p_alDeleteAuxiliaryEffectSlots = reinterpret_cast( + alGetProcAddress("alDeleteAuxiliaryEffectSlots")); + p_alAuxiliaryEffectSloti = + reinterpret_cast(alGetProcAddress("alAuxiliaryEffectSloti")); + p_alAuxiliaryEffectSlotf = + reinterpret_cast(alGetProcAddress("alAuxiliaryEffectSlotf")); + + ok = p_alGenEffects && p_alDeleteEffects && p_alEffecti && p_alEffectf && + p_alGenAuxiliaryEffectSlots && p_alDeleteAuxiliaryEffectSlots && + p_alAuxiliaryEffectSloti && p_alAuxiliaryEffectSlotf; + return ok; +} + +static void CreateSpatialReverbLocked(Port& port) { + ALCcontext* context = alcGetCurrentContext(); + ALCdevice* alc_device = context ? alcGetContextsDevice(context) : nullptr; + if (!alc_device || !alcIsExtensionPresent(alc_device, "ALC_EXT_EFX") || !LoadEfxFunctions()) { + LOG_INFO(Lib_Audio3d, "EFX unavailable; LATE_REVERB_LEVEL will have no effect"); + return; + } + + alGetError(); + + ALuint effect = 0; + ALuint slot = 0; + p_alGenEffects(1, &effect); + p_alGenAuxiliaryEffectSlots(1, &slot); + if (alGetError() != AL_NO_ERROR) { + if (effect != 0) { + p_alDeleteEffects(1, &effect); + } + if (slot != 0) { + p_alDeleteAuxiliaryEffectSlots(1, &slot); + } + LOG_WARNING(Lib_Audio3d, "Failed to create EFX reverb objects"); + return; + } + + p_alEffecti(effect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); + p_alEffectf(effect, AL_REVERB_DECAY_TIME, 1.8f); + p_alAuxiliaryEffectSloti(slot, AL_EFFECTSLOT_EFFECT, static_cast(effect)); + p_alAuxiliaryEffectSlotf(slot, AL_EFFECTSLOT_GAIN, + std::clamp(port.late_reverb_level, 0.0f, 1.0f)); + + if (alGetError() != AL_NO_ERROR) { + p_alDeleteAuxiliaryEffectSlots(1, &slot); + p_alDeleteEffects(1, &effect); + LOG_WARNING(Lib_Audio3d, "Failed to configure EFX reverb"); + return; + } + + port.reverb_effect = effect; + port.reverb_slot = slot; + port.reverb_supported = true; + LOG_INFO(Lib_Audio3d, "EFX late reverb ready (level {})", port.late_reverb_level); +} + +static void SpatialApplyReverbLevelLocked(Port& port) { + if (!port.reverb_supported || !port.spatial_ready || + !AudioOut::OpenALDevice::GetInstance().MakeCurrent(port.device_name)) { + return; + } + p_alAuxiliaryEffectSlotf(port.reverb_slot, AL_EFFECTSLOT_GAIN, + std::clamp(port.late_reverb_level, 0.0f, 1.0f)); +} + +static void ResetObjectSpatialLocked(Port& port, ObjectState& obj) { + if (obj.al.source == 0 || !port.spatial_ready || + !AudioOut::OpenALDevice::GetInstance().MakeCurrent(port.device_name)) { + return; + } + alSourceStop(obj.al.source); + SpatialReclaimSource(obj.al); + alSourcei(obj.al.source, AL_SOURCE_RELATIVE, AL_TRUE); + alSource3f(obj.al.source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSourcef(obj.al.source, AL_GAIN, 0.0f); + if (port.source_radius_supported) { + alSourcef(obj.al.source, AL_SOURCE_RADIUS, 0.0f); + } + if (port.direct_channels_supported) { + alSourcei(obj.al.source, AL_DIRECT_CHANNELS_SOFT, AL_FALSE); + } +} + +static void SpatialUpdateVolumeLocked(Port& port) { + const u64 now = Kernel::sceKernelGetProcessTime(); + if (now - port.last_volume_check_us < SPATIAL_VOLUME_CHECK_INTERVAL_US) { + return; + } + port.last_volume_check_us = now; + + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; + if (std::abs(slider_gain - port.current_gain) < 0.001f) { + return; + } + + alSourcef(port.bed.source, AL_GAIN, slider_gain); + if (const ALenum err = alGetError(); err == AL_NO_ERROR) { + port.current_gain = slider_gain; + } else { + LOG_ERROR(Lib_Audio3d, "Failed to set spatial gain: {}", ALErrorString(err)); + } +} + +static void DestroySpatial(Port& port) { + if (!port.spatial_init_attempted) { + return; + } + port.spatial_init_attempted = false; + + if (!port.spatial_ready) { + return; + } + port.spatial_ready = false; + + auto& device = AudioOut::OpenALDevice::GetInstance(); + if (device.MakeCurrent(port.device_name)) { + DestroySpatialSourceLocked(port.bed); + for (auto& [object_id, obj] : port.objects) { + DestroySpatialSourceLocked(obj.al); + } + if (port.reverb_supported) { + ALuint slot = port.reverb_slot; + ALuint effect = port.reverb_effect; + p_alDeleteAuxiliaryEffectSlots(1, &slot); + p_alDeleteEffects(1, &effect); + } + } else { + port.bed = SpatialSource{}; + for (auto& [object_id, obj] : port.objects) { + obj.al = SpatialSource{}; + } + } + + port.reverb_supported = false; + port.reverb_slot = 0; + port.reverb_effect = 0; + + for (auto& bundle : port.spatial_queue) { + FreeBundle(bundle); + } + port.spatial_queue.clear(); + + device.UnregisterPort(port.device_name); +} + +static bool EnsureSpatial(Port& port) { + if (port.spatial_init_attempted) { + return port.spatial_ready; + } + port.spatial_init_attempted = true; + + port.device_name = EmulatorSettings.GetOpenALMainOutputDevice(); + auto& device = AudioOut::OpenALDevice::GetInstance(); + + if (!device.RegisterPort(port.device_name)) { + LOG_WARNING(Lib_Audio3d, + "OpenAL device '{}' unavailable for spatial output, using AudioOut path", + port.device_name); + return false; + } + + if (!device.MakeCurrent(port.device_name)) { + LOG_ERROR(Lib_Audio3d, "Failed to make OpenAL context current for spatial output"); + device.UnregisterPort(port.device_name); + return false; + } + + alGetError(); + alDistanceModel(AL_NONE); + alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); + + port.source_radius_supported = alIsExtensionPresent("AL_EXT_SOURCE_RADIUS") == AL_TRUE; + port.direct_channels_supported = alIsExtensionPresent("AL_SOFT_direct_channels") == AL_TRUE; + + if (!CreateSpatialSource(port, port.bed)) { + device.UnregisterPort(port.device_name); + return false; + } + + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; + alSourcef(port.bed.source, AL_GAIN, slider_gain); + port.current_gain = slider_gain; + + if (port.direct_channels_supported) { + alSourcei(port.bed.source, AL_DIRECT_CHANNELS_SOFT, AL_TRUE); + } + + CreateSpatialReverbLocked(port); + + port.period_us = + (1000000ULL * port.parameters.granularity + AUDIO3D_SAMPLE_RATE / 2) / AUDIO3D_SAMPLE_RATE; + port.spatial_ready = true; + + LOG_INFO(Lib_Audio3d, + "Spatial output initialized on OpenAL device '{}' (granularity {}, ring {})", + port.device_name.empty() ? "Default Device" : port.device_name, + port.parameters.granularity, port.parameters.queue_depth + SPATIAL_RING_SLACK); + return true; +} + +static float GetObjectGain(const ObjectState& obj) { + float gain = 0.0f; + const auto gain_key = static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN); + if (obj.persistent_attributes.contains(gain_key)) { + const auto& blob = obj.persistent_attributes.at(gain_key); + if (blob.size() >= sizeof(float)) { + std::memcpy(&gain, blob.data(), sizeof(float)); + } + } + return gain; +} + +static bool GetObjectPosition(const ObjectState& obj, OrbisAudio3dPosition& out) { + const auto key = static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_POSITION); + if (!obj.persistent_attributes.contains(key)) { + return false; + } + const auto& blob = obj.persistent_attributes.at(key); + if (blob.size() < sizeof(OrbisAudio3dPosition)) { + return false; + } + std::memcpy(&out, blob.data(), sizeof(OrbisAudio3dPosition)); + return true; +} + +static float GetObjectSpread(const ObjectState& obj) { + float spread = 0.0f; + const auto key = static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_SPREAD); + if (obj.persistent_attributes.contains(key)) { + const auto& blob = obj.persistent_attributes.at(key); + if (blob.size() >= sizeof(float)) { + std::memcpy(&spread, blob.data(), sizeof(float)); + } + } + return spread; +} + +static OrbisAudio3dPassthrough GetObjectPassthrough(const ObjectState& obj) { + u32 value = 0; + const auto key = static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PASSTHROUGH); + if (obj.persistent_attributes.contains(key)) { + const auto& blob = obj.persistent_attributes.at(key); + if (blob.size() >= sizeof(u32)) { + std::memcpy(&value, blob.data(), sizeof(u32)); + } + } + if (value > static_cast(OrbisAudio3dPassthrough::ORBIS_AUDIO3D_PASSTHROUGH_RIGHT)) { + value = 0; + } + return static_cast(value); +} + +static float SpreadToRadius(const OrbisAudio3dPosition& pos, float spread) { + constexpr float PI = 3.14159265358979323846f; + constexpr float MAX_RADIUS = 100.0f; + constexpr float HEAD_RADIUS = 0.1f; + + const float distance = std::sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z); + if (distance <= HEAD_RADIUS) { + return MAX_RADIUS; + } + + spread = std::clamp(spread, 0.0f, 2.0f * PI); + if (spread >= PI * 0.98f) { + return MAX_RADIUS; + } + + return std::min(distance * std::tan(spread * 0.5f), MAX_RADIUS); +} + +static const s16* ConvertObjectFrameToMonoS16(Port& port, const AudioData& data, + const u32 granularity) { + auto& out = port.spatial_scratch; + out.assign(granularity, 0); + + const u32 frames = std::min(granularity, data.num_samples); + const u32 channels = std::max(data.num_channels, 1); + + if (data.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + const s16* src = reinterpret_cast(data.sample_buffer); + if (channels == 1) { + std::memcpy(out.data(), src, frames * sizeof(s16)); + } else { + for (u32 i = 0; i < frames; i++) { + s32 sum = 0; + for (u32 c = 0; c < channels; c++) { + sum += src[i * channels + c]; + } + out[i] = static_cast(sum / static_cast(channels)); + } + } + } else { // FLOAT input + const float* src = reinterpret_cast(data.sample_buffer); + for (u32 i = 0; i < frames; i++) { + float sum = 0.0f; + for (u32 c = 0; c < channels; c++) { + sum += src[i * channels + c]; + } + const float v = std::clamp(sum / static_cast(channels), -1.0f, 1.0f); + out[i] = static_cast(v * 32767.0f); + } + } + + return out.data(); +} + +static const s16* ConvertObjectFrameToPassthroughS16(Port& port, const AudioData& data, + const u32 granularity, + const OrbisAudio3dPassthrough passthrough) { + const s16* mono = ConvertObjectFrameToMonoS16(port, data, granularity); + + auto& out = port.spatial_scratch_stereo; + out.assign(granularity * 2, 0); + + const u32 ear = + passthrough == OrbisAudio3dPassthrough::ORBIS_AUDIO3D_PASSTHROUGH_RIGHT ? 1u : 0u; + for (u32 i = 0; i < granularity; i++) { + out[i * 2 + ear] = mono[i]; + } + + return out.data(); +} + +static bool SpatialUploadLocked(SpatialSource& src, const void* samples, const u32 bytes, + const ALenum format) { + SpatialReclaimSource(src); + if (src.available.empty()) { + return false; + } + + const ALuint buffer_id = src.available.back(); + src.available.pop_back(); + + alGetError(); + alBufferData(buffer_id, format, samples, static_cast(bytes), AUDIO3D_SAMPLE_RATE); + ALuint queue_id = buffer_id; + alSourceQueueBuffers(src.source, 1, &queue_id); + + if (const ALenum err = alGetError(); err != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to queue spatial buffer: {}", ALErrorString(err)); + src.available.push_back(buffer_id); + return true; // Slot existed; the frame itself is dropped. + } + + // Per-source underrun restart, same as the AudioOut OpenAL backend. + ALint al_state = 0; + alGetSourcei(src.source, AL_SOURCE_STATE, &al_state); + if (al_state != AL_PLAYING) { + alSourcePlay(src.source); + } + + return true; +} + +static s32 SpatialSubmitBundle(Port& port, SpatialFrameBundle bundle) { + const u32 bed_bytes = bundle.bed.num_samples * AUDIO3D_OUTPUT_NUM_CHANNELS * sizeof(s16); + const u32 granularity = port.parameters.granularity; + u64 waited_us = 0; + + while (true) { + { + std::scoped_lock lock{port.mutex}; + + if (!port.spatial_ready || + !AudioOut::OpenALDevice::GetInstance().MakeCurrent(port.device_name)) { + LOG_ERROR(Lib_Audio3d, "Spatial output lost, dropping frame"); + FreeBundle(bundle); + return ORBIS_OK; + } + + SpatialUpdateVolumeLocked(port); + + if (SpatialUploadLocked(port.bed, bundle.bed.sample_buffer, bed_bytes, + AL_FORMAT_STEREO16)) { + for (auto& frame : bundle.objects) { + const auto it = port.objects.find(frame.object_id); + if (it == port.objects.end()) { + // Object was unreserved between Advance and Push. + continue; + } + + auto& src = it->second.al; + if (src.source == 0 && !CreateSpatialSource(port, src)) { + continue; + } + + alSourcef(src.source, AL_GAIN, frame.gain * port.current_gain); + + const bool passthrough = + frame.passthrough != + OrbisAudio3dPassthrough::ORBIS_AUDIO3D_PASSTHROUGH_NONE; + + if (port.direct_channels_supported) { + alSourcei(src.source, AL_DIRECT_CHANNELS_SOFT, + passthrough ? AL_TRUE : AL_FALSE); + } + + if (port.reverb_supported) { + alSource3i(src.source, AL_AUXILIARY_SEND_FILTER, + passthrough ? AL_EFFECTSLOT_NULL + : static_cast(port.reverb_slot), + 0, AL_FILTER_NULL); + } + + if (passthrough) { + alSourcei(src.source, AL_SOURCE_RELATIVE, AL_TRUE); + alSource3f(src.source, AL_POSITION, 0.0f, 0.0f, 0.0f); + + const s16* stereo = ConvertObjectFrameToPassthroughS16( + port, frame.pcm, granularity, frame.passthrough); + if (!SpatialUploadLocked(src, stereo, granularity * 2 * sizeof(s16), + AL_FORMAT_STEREO16)) { + LOG_WARNING(Lib_Audio3d, "Object {} ring full, dropping frame", + frame.object_id); + } + continue; + } + + if (frame.has_position) { + alSourcei(src.source, AL_SOURCE_RELATIVE, AL_FALSE); + alSource3f(src.source, AL_POSITION, frame.position.x, frame.position.y, + -frame.position.z); + if (port.source_radius_supported) { + alSourcef(src.source, AL_SOURCE_RADIUS, + SpreadToRadius(frame.position, frame.spread)); + } + } + + const s16* mono = ConvertObjectFrameToMonoS16(port, frame.pcm, granularity); + if (!SpatialUploadLocked(src, mono, granularity * sizeof(s16), + AL_FORMAT_MONO16)) { + LOG_WARNING(Lib_Audio3d, "Object {} ring full, dropping frame", + frame.object_id); + } + } + + FreeBundle(bundle); + return ORBIS_OK; + } + } + + if (waited_us >= SPATIAL_SUBMIT_TIMEOUT_US) { + LOG_WARNING(Lib_Audio3d, "Spatial submit timed out, dropping frame"); + FreeBundle(bundle); + return ORBIS_OK; + } + + const u64 sleep_us = std::max(port.period_us / 2, SPATIAL_MIN_SLEEP_US); + std::this_thread::sleep_for(std::chrono::microseconds(sleep_us)); + waited_us += sleep_us; + } +} + +static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { + SpatialFrameBundle bundle{}; + bundle.bed = frame; + return SpatialSubmitBundle(port, std::move(bundle)); +} + s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { LOG_INFO(Lib_Audio3d, "called, handle = {}", handle); - // Remove from any port that was tracking this handle. if (state) { for (auto& [port_id, port] : state->ports) { std::scoped_lock lock{port.mutex}; - auto& handles = port.audioout_handles; - handles.erase(std::remove(handles.begin(), handles.end(), handle), handles.end()); + std::erase_if(port.audioout_ports, + [&](const AssociatedAudioOutPort& p) { return p.handle == handle; }); } } @@ -64,8 +710,12 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( return handle; } - // Track this handle in the port so sceAudio3dPortFlush can use it for sync. - state->ports[port_id].audioout_handles.push_back(handle); + const auto info = GetAudioOutBufferInfo(param.data_format.Value()); + AssociatedAudioOutPort aout{}; + aout.handle = handle; + aout.buffer_bytes = len * info.channels * info.sample_size; + aout.samples_per_buffer = len * info.channels; + state->ports[port_id].audioout_ports.push_back(std::move(aout)); return handle; } @@ -82,6 +732,36 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { return ORBIS_AUDIO3D_ERROR_INVALID_PORT; } + if (state) { + for (auto& [port_id, port] : state->ports) { + std::scoped_lock lock{port.mutex}; + for (auto& aout : port.audioout_ports) { + if (aout.handle != handle) { + continue; + } + + if (aout.pending.size() >= port.parameters.queue_depth) { + LOG_DEBUG(Lib_Audio3d, + "AudioOut handle {} queue full ({}) without Push, " + "submitting oldest", + handle, aout.pending.size()); + std::vector oldest = std::move(aout.pending.front()); + aout.pending.pop_front(); + const s32 ret = AudioOut::sceAudioOutOutput(handle, oldest.data()); + if (ret < 0) { + return ret; + } + } + + const u8* src = static_cast(ptr); + aout.pending.emplace_back(src, src + aout.buffer_bytes); + + // Mirror sceAudioOutOutput's return of samples sent. + return static_cast(aout.samples_per_buffer); + } + } + } + return AudioOut::sceAudioOutOutput(handle, ptr); } @@ -94,7 +774,14 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* p return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } - return AudioOut::sceAudioOutOutputs(param, num); + for (u32 i = 0; i < num; i++) { + const s32 ret = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr); + if (ret < 0) { + return ret; + } + } + + return ORBIS_OK; } static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm& pcm, @@ -106,7 +793,6 @@ static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm const u32 bytes_per_sample = (pcm.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) ? sizeof(s16) : sizeof(float); - // Always allocate exactly granularity samples (zeroed = silence for padding). const u32 dst_bytes = granularity * num_channels * bytes_per_sample; u8* copy = static_cast(std::calloc(1, dst_bytes)); if (!copy) { @@ -276,14 +962,12 @@ s32 PS4_SYSV_ABI sceAudio3dObjectReserve(const OrbisAudio3dPortId port_id, auto& port = state->ports[port_id]; std::scoped_lock lock{port.mutex}; - // Enforce the max_objects limit set at PortOpen time. if (port.objects.size() >= port.parameters.max_objects) { LOG_ERROR(Lib_Audio3d, "port has no available objects (max_objects = {})", port.parameters.max_objects); return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; } - // Counter lives in the Port so it resets when the port is closed and reopened. do { ++port.next_object_id; } while (port.next_object_id == 0 || @@ -325,13 +1009,13 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(const OrbisAudio3dPortId port_id, auto& obj = port.objects[object_id]; - // RESET_STATE clears all attributes and queued PCM; it takes no value. if (attribute_id == OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { for (auto& data : obj.pcm_queue) { std::free(data.sample_buffer); } obj.pcm_queue.clear(); obj.persistent_attributes.clear(); + ResetObjectSpatialLocked(port, obj); LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); return ORBIS_OK; } @@ -370,7 +1054,6 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, auto& obj = port.objects[object_id]; - // First pass: handle RESET_STATE. for (u64 i = 0; i < num_attributes; i++) { if (attribute_array[i].attribute_id == OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { @@ -379,12 +1062,12 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, } obj.pcm_queue.clear(); obj.persistent_attributes.clear(); + ResetObjectSpatialLocked(port, obj); LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); break; // Only one reset is needed even if listed multiple times. } } - // Second pass: apply all other attributes. for (u64 i = 0; i < num_attributes; i++) { const auto& attr = attribute_array[i]; @@ -406,8 +1089,6 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, break; } default: { - // Store the other attributes in the ObjectState so they're available when we - // implement them. if (attr.value && attr.value_size > 0) { const auto* src = static_cast(attr.value); obj.persistent_attributes[static_cast(attr.attribute_id)].assign( @@ -445,6 +1126,13 @@ s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(const OrbisAudio3dPortId port_id, std::free(data.sample_buffer); } + // Release the object's OpenAL source, if it was ever created. + if (auto& obj = port.objects[object_id]; + obj.al.source != 0 && port.spatial_ready && + AudioOut::OpenALDevice::GetInstance().MakeCurrent(port.device_name)) { + DestroySpatialSourceLocked(obj.al); + } + port.objects.erase(object_id); return ORBIS_OK; } @@ -458,14 +1146,18 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { } auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; if (port.parameters.buffer_mode == OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_NO_ADVANCE) { LOG_ERROR(Lib_Audio3d, "port doesn't have advance capability"); return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; } - if (port.mixed_queue.size() >= port.parameters.queue_depth) { - LOG_WARNING(Lib_Audio3d, "mixed queue full (depth={}), dropping advance", + const bool spatial = EnsureSpatial(port); + + if ((spatial ? port.spatial_queue.size() : port.mixed_queue.size()) >= + port.parameters.queue_depth) { + LOG_WARNING(Lib_Audio3d, "queue full (depth={}), dropping advance", port.parameters.queue_depth); return ORBIS_AUDIO3D_ERROR_NOT_READY; } @@ -482,7 +1174,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { if (queue.empty()) return; - // default gain is 0.0 — objects with no GAIN set are silent. if (gain == 0.0f) { AudioData data = queue.front(); queue.pop_front(); @@ -538,21 +1229,29 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { std::free(data.sample_buffer); }; - // Bed is mixed at full gain (1.0). mix_in(port.bed_queue, 1.0f); - // Mix all object PCM queues, applying each object's GAIN persistent attribute. - for (auto& [obj_id, obj] : port.objects) { - float gain = 0.0f; - const auto gain_key = - static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN); - if (obj.persistent_attributes.contains(gain_key)) { - const auto& blob = obj.persistent_attributes.at(gain_key); - if (blob.size() >= sizeof(float)) { - std::memcpy(&gain, blob.data(), sizeof(float)); + SpatialFrameBundle bundle{}; + + if (spatial) { + for (auto& [obj_id, obj] : port.objects) { + if (obj.pcm_queue.empty()) { + continue; } + SpatialObjectFrame frame{}; + frame.object_id = obj_id; + frame.pcm = obj.pcm_queue.front(); + obj.pcm_queue.pop_front(); + frame.gain = GetObjectGain(obj); + frame.has_position = GetObjectPosition(obj, frame.position); + frame.spread = GetObjectSpread(obj); + frame.passthrough = GetObjectPassthrough(obj); + bundle.objects.push_back(std::move(frame)); + } + } else { + for (auto& [obj_id, obj] : port.objects) { + mix_in(obj.pcm_queue, GetObjectGain(obj)); } - mix_in(obj.pcm_queue, gain); } s16* mix_s16 = static_cast(std::malloc(out_samples * sizeof(s16))); @@ -568,10 +1267,17 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { std::free(mix_float); - port.mixed_queue.push_back(AudioData{.sample_buffer = reinterpret_cast(mix_s16), - .num_samples = granularity, - .num_channels = AUDIO3D_OUTPUT_NUM_CHANNELS, - .format = OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}); + const AudioData out_frame{.sample_buffer = reinterpret_cast(mix_s16), + .num_samples = granularity, + .num_channels = AUDIO3D_OUTPUT_NUM_CHANNELS, + .format = OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; + + if (spatial) { + bundle.bed = out_frame; + port.spatial_queue.push_back(std::move(bundle)); + } else { + port.mixed_queue.push_back(out_frame); + } return ORBIS_OK; } @@ -588,15 +1294,17 @@ s32 PS4_SYSV_ABI sceAudio3dPortClose(const OrbisAudio3dPortId port_id) { { std::scoped_lock lock{port.mutex}; + DestroySpatial(port); + if (port.audio_out_handle >= 0) { AudioOut::sceAudioOutClose(port.audio_out_handle); port.audio_out_handle = -1; } - for (const s32 handle : port.audioout_handles) { - AudioOut::sceAudioOutClose(handle); + for (const auto& aout : port.audioout_ports) { + AudioOut::sceAudioOutClose(aout.handle); } - port.audioout_handles.clear(); + port.audioout_ports.clear(); for (auto& data : port.mixed_queue) { std::free(data.sample_buffer); @@ -652,9 +1360,12 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { auto& port = state->ports[port_id]; std::scoped_lock lock{port.mutex}; - if (!port.audioout_handles.empty()) { - for (const s32 handle : port.audioout_handles) { - const s32 ret = AudioOut::sceAudioOutOutput(handle, nullptr); + if (!port.audioout_ports.empty()) { + if (const s32 ret = DrainAssociatedPorts(port); ret < 0) { + return ret; + } + for (const auto& aout : port.audioout_ports) { + const s32 ret = AudioOut::sceAudioOutOutput(aout.handle, nullptr); if (ret < 0) { return ret; } @@ -662,7 +1373,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { return ORBIS_OK; } - if (port.mixed_queue.empty()) { + if (port.mixed_queue.empty() && port.spatial_queue.empty()) { // Only mix if there's actually something to mix. if (!port.bed_queue.empty() || std::any_of(port.objects.begin(), port.objects.end(), @@ -674,11 +1385,13 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } - if (port.mixed_queue.empty()) { + if (port.mixed_queue.empty() && port.spatial_queue.empty()) { return ORBIS_OK; } - if (port.audio_out_handle < 0) { + const bool spatial = EnsureSpatial(port); + + if (!spatial && port.audio_out_handle < 0) { AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); port.audio_out_handle = @@ -689,12 +1402,27 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } - // Drain all queued mixed frames, blocking on each until consumed. while (!port.mixed_queue.empty()) { AudioData frame = port.mixed_queue.front(); port.mixed_queue.pop_front(); - const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); - std::free(frame.sample_buffer); + + s32 ret; + if (spatial) { + ret = SpatialSubmitFrame(port, frame); + } else { + ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); + } + if (ret < 0) { + return ret; + } + } + + while (!port.spatial_queue.empty()) { + SpatialFrameBundle bundle = std::move(port.spatial_queue.front()); + port.spatial_queue.pop_front(); + + const s32 ret = SpatialSubmitBundle(port, std::move(bundle)); if (ret < 0) { return ret; } @@ -720,25 +1448,23 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId port_id return ORBIS_AUDIO3D_ERROR_INVALID_PORT; } - // We support three attributes, PCM, Gain, and ResetState - // In the future, supported attributes should be stored in the port. + static constexpr std::array supported = { + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PCM, + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_POSITION, + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN, + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_SPREAD, + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PASSTHROUGH, + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE, + }; + if (capabilities) { - // Writes up to num_capabilities supported capabilities, - // then sets num_capabilities to how many were written. - u32 caps_to_write = *num_capabilities; - if (caps_to_write >= 1) { - capabilities[0] = OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PCM; + const u32 caps_to_write = std::min(*num_capabilities, supported.size()); + for (u32 i = 0; i < caps_to_write; i++) { + capabilities[i] = supported[i]; } - if (caps_to_write >= 2) { - capabilities[1] = OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN; - } - if (caps_to_write >= 3) { - capabilities[2] = OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE; - } - *num_capabilities = std::min(caps_to_write, 3); + *num_capabilities = caps_to_write; } else { - // If capabilities is null, then just report the number of supported capabilities. - *num_capabilities = 3; + *num_capabilities = static_cast(supported.size()); } return ORBIS_OK; } @@ -769,7 +1495,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(const OrbisAudio3dPortId port_id, u const auto& port = state->ports[port_id]; std::scoped_lock lock{port.mutex}; - const size_t size = port.mixed_queue.size(); + const size_t size = port.mixed_queue.size() + port.spatial_queue.size(); if (queue_level) { *queue_level = static_cast(size); @@ -957,7 +1683,17 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, const u32 depth = port.parameters.queue_depth; - if (port.audio_out_handle < 0) { + if (const s32 ret = DrainAssociatedPorts(port); ret < 0) { + return ret; + } + + bool spatial; + { + std::scoped_lock lock{port.mutex}; + spatial = EnsureSpatial(port); + } + + if (!spatial && port.audio_out_handle < 0) { AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); @@ -969,8 +1705,33 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, return port.audio_out_handle; } - // Function that submits exactly one frame (if available). auto submit_one_frame = [&](bool& submitted) -> s32 { + if (spatial) { + SpatialFrameBundle bundle{}; + { + std::scoped_lock lock{port.mutex}; + + if (!port.mixed_queue.empty()) { + // Defensive: drain any CPU-mixed leftovers as bed-only frames. + bundle.bed = port.mixed_queue.front(); + port.mixed_queue.pop_front(); + } else if (!port.spatial_queue.empty()) { + bundle = std::move(port.spatial_queue.front()); + port.spatial_queue.pop_front(); + } else { + submitted = false; + return ORBIS_OK; + } + } + + const s32 ret = SpatialSubmitBundle(port, std::move(bundle)); + if (ret < 0) + return ret; + + submitted = true; + return ORBIS_OK; + } + AudioData frame; { std::scoped_lock lock{port.mutex}; @@ -994,15 +1755,13 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, return ORBIS_OK; }; - // If not full, return immediately. { std::scoped_lock lock{port.mutex}; - if (port.mixed_queue.size() < depth) { + if (port.mixed_queue.size() + port.spatial_queue.size() < depth) { return ORBIS_OK; } } - // Submit one frame to free space. bool submitted = false; s32 ret = submit_one_frame(submitted); if (ret < 0) @@ -1011,16 +1770,14 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, if (!submitted) return ORBIS_OK; - // ASYNC: free exactly one slot and return. if (blocking == OrbisAudio3dBlocking::ORBIS_AUDIO3D_BLOCKING_ASYNC) { return ORBIS_OK; } - // SYNC: ensure at least one slot is free (drain until size < depth). while (true) { { std::scoped_lock lock{port.mutex}; - if (port.mixed_queue.size() < depth) + if (port.mixed_queue.size() + port.spatial_queue.size() < depth) break; } @@ -1042,11 +1799,11 @@ s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { } s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, - const OrbisAudio3dAttributeId attribute_id, + const OrbisAudio3dPortAttributeId attribute_id, void* attribute, const u64 attribute_size) { - LOG_INFO(Lib_Audio3d, - "called, port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", - port_id, static_cast(attribute_id), attribute, attribute_size); + LOG_DEBUG(Lib_Audio3d, + "called, port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", + port_id, static_cast(attribute_id), attribute, attribute_size); if (!state->ports.contains(port_id)) { LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); @@ -1058,9 +1815,33 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } - // TODO + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; - return ORBIS_OK; + switch (attribute_id) { + case OrbisAudio3dPortAttributeId::ORBIS_AUDIO3D_PORT_ATTRIBUTE_LATE_REVERB_LEVEL: { + if (attribute_size < sizeof(float)) { + LOG_ERROR(Lib_Audio3d, "LATE_REVERB_LEVEL attribute_size < sizeof(float)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + float level = 0.0f; + std::memcpy(&level, attribute, sizeof(float)); + level = std::clamp(level, 0.0f, 1.0f); + + if (std::abs(level - port.late_reverb_level) < 0.001f) { + return ORBIS_OK; + } + + port.late_reverb_level = level; + SpatialApplyReverbLevelLocked(port); + LOG_INFO(Lib_Audio3d, "late reverb level = {}", port.late_reverb_level); + return ORBIS_OK; + } + default: + LOG_WARNING(Lib_Audio3d, "unknown port attribute {:#x} (size {})", + static_cast(attribute_id), attribute_size); + return ORBIS_OK; + } } s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler() { diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h index 4dac49d65..afa2ad513 100644 --- a/src/core/libraries/audio3d/audio3d_openal.h +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -82,6 +83,12 @@ enum class OrbisAudio3dAttributeId : u32 { ORBIS_AUDIO3D_ATTRIBUTE_OUTPUT_ROUTE = 11, }; +enum class OrbisAudio3dPortAttributeId : u32 { + ORBIS_AUDIO3D_PORT_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001, + ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002, + ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003, +}; + struct OrbisAudio3dAttribute { OrbisAudio3dAttributeId attribute_id; int : 32; @@ -96,9 +103,50 @@ struct AudioData { OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; }; +struct OrbisAudio3dPosition { + float x; + float y; + float z; +}; + +enum class OrbisAudio3dPassthrough : u32 { + ORBIS_AUDIO3D_PASSTHROUGH_NONE = 0, + ORBIS_AUDIO3D_PASSTHROUGH_LEFT = 1, + ORBIS_AUDIO3D_PASSTHROUGH_RIGHT = 2, +}; + +struct SpatialSource { + u32 source{0}; + std::vector buffers; // ALuint ring, sized queue_depth + slack + std::vector available; // reclaimed / never-queued buffer ids +}; + struct ObjectState { std::deque pcm_queue; std::unordered_map> persistent_attributes; + SpatialSource al; +}; + +struct SpatialObjectFrame { + OrbisAudio3dObjectId object_id{}; + AudioData pcm{}; + float gain{0.0f}; + OrbisAudio3dPosition position{0.0f, 0.0f, 0.0f}; + bool has_position{false}; + float spread{0.0f}; + OrbisAudio3dPassthrough passthrough{OrbisAudio3dPassthrough::ORBIS_AUDIO3D_PASSTHROUGH_NONE}; +}; + +struct SpatialFrameBundle { + AudioData bed{}; + std::vector objects; +}; + +struct AssociatedAudioOutPort { + s32 handle{-1}; + u32 buffer_bytes{0}; + u32 samples_per_buffer{0}; + std::deque> pending; }; struct Port { @@ -106,16 +154,37 @@ struct Port { OrbisAudio3dOpenParameters parameters{}; // Opened lazily on the first sceAudio3dPortPush call. s32 audio_out_handle{-1}; - // Handles explicitly opened by the game via sceAudio3dAudioOutOpen. - std::vector audioout_handles; + // AudioOut ports explicitly opened by the game via sceAudio3dAudioOutOpen. + std::vector audioout_ports; // Reserved objects and their state. std::unordered_map objects; // Increasing counter for generating unique object IDs within this port. OrbisAudio3dObjectId next_object_id{0}; // Bed audio queue. std::deque bed_queue; - // Mixed stereo frames ready to be consumed by sceAudio3dPortPush. + // Mixed stereo frames ready to be consumed by sceAudio3dPortPush + // (CPU-mix fallback path only). std::deque mixed_queue; + // Per-tick frame bundles produced by PortAdvance in the spatial path. + std::deque spatial_queue; + + // Spatial (direct OpenAL) output state. + bool spatial_init_attempted{false}; + bool spatial_ready{false}; + bool source_radius_supported{false}; + bool direct_channels_supported{false}; + std::string device_name; + u64 period_us{0}; + u64 last_volume_check_us{0}; + float current_gain{-1.0f}; + SpatialSource bed; + std::vector spatial_scratch; + std::vector spatial_scratch_stereo; + // EFX late reverb + bool reverb_supported{false}; + u32 reverb_slot{0}; // ALuint + u32 reverb_effect{0}; // ALuint + float late_reverb_level{0.0f}; }; struct Audio3dState { @@ -175,8 +244,8 @@ s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUser s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking); s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug(); s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(OrbisAudio3dPortId port_id, - OrbisAudio3dAttributeId attribute_id, void* attribute, - u64 attribute_size); + OrbisAudio3dPortAttributeId attribute_id, + void* attribute, u64 attribute_size); s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler(); s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler(); s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer();