From 619b9ccaa931fe85ae4f60af6ad7307e25c4fbeb Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 8 Jul 2026 17:49:23 +0300 Subject: [PATCH 01/14] spatial openal support --- src/core/libraries/audio3d/audio3d_openal.cpp | 264 +++++++++++++++++- src/core/libraries/audio3d/audio3d_openal.h | 16 ++ 2 files changed, 274 insertions(+), 6 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 43d7d02c7..90a02ae0b 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -2,16 +2,23 @@ // SPDX-License-Identifier: GPL-2.0-or-later #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,6 +31,228 @@ static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2; static std::unique_ptr state; +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 SpatialReclaimProcessedLocked(Port& port) { + ALint processed = 0; + alGetSourcei(port.bed.source, AL_BUFFERS_PROCESSED, &processed); + + while (processed-- > 0) { + ALuint buffer_id = 0; + alSourceUnqueueBuffers(port.bed.source, 1, &buffer_id); + if (alGetError() != AL_NO_ERROR) { + break; + } + port.bed.available.push_back(buffer_id); + } +} + +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)) { + ALuint source = port.bed.source; + if (source != 0) { + alSourceStop(source); + + ALint queued = 0; + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + while (queued-- > 0) { + ALuint buffer_id = 0; + alSourceUnqueueBuffers(source, 1, &buffer_id); + } + + alDeleteSources(1, &source); + } + if (!port.bed.buffers.empty()) { + alDeleteBuffers(static_cast(port.bed.buffers.size()), + reinterpret_cast(port.bed.buffers.data())); + } + } + + port.bed = SpatialBed{}; + 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(); + + ALuint source = 0; + alGenSources(1, &source); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to generate spatial source"); + device.UnregisterPort(port.device_name); + return false; + } + + const u32 ring_size = port.parameters.queue_depth + SPATIAL_RING_SLACK; + port.bed.buffers.resize(ring_size); + alGenBuffers(static_cast(ring_size), + reinterpret_cast(port.bed.buffers.data())); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to generate spatial buffers"); + alDeleteSources(1, &source); + port.bed.buffers.clear(); + device.UnregisterPort(port.device_name); + return false; + } + port.bed.available = port.bed.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); + + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; + alSourcef(source, AL_GAIN, slider_gain); + port.current_gain = slider_gain; + + port.bed.source = source; + 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, ring_size); + return true; +} + +static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { + const u32 bytes = frame.num_samples * AUDIO3D_OUTPUT_NUM_CHANNELS * sizeof(s16); + 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"); + std::free(frame.sample_buffer); + return ORBIS_OK; + } + + SpatialUpdateVolumeLocked(port); + SpatialReclaimProcessedLocked(port); + + if (!port.bed.available.empty()) { + const ALuint buffer_id = port.bed.available.back(); + port.bed.available.pop_back(); + + alGetError(); + alBufferData(buffer_id, AL_FORMAT_STEREO16, frame.sample_buffer, + static_cast(bytes), AUDIO3D_SAMPLE_RATE); + ALuint queue_id = buffer_id; + alSourceQueueBuffers(port.bed.source, 1, &queue_id); + + if (const ALenum err = alGetError(); err != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to queue spatial frame: {}", ALErrorString(err)); + port.bed.available.push_back(buffer_id); + std::free(frame.sample_buffer); + return ORBIS_OK; + } + + // Per-source underrun restart, same as the AudioOut OpenAL backend. + ALint al_state = 0; + alGetSourcei(port.bed.source, AL_SOURCE_STATE, &al_state); + if (al_state != AL_PLAYING) { + alSourcePlay(port.bed.source); + } + + std::free(frame.sample_buffer); + return ORBIS_OK; + } + } + + if (waited_us >= SPATIAL_SUBMIT_TIMEOUT_US) { + LOG_WARNING(Lib_Audio3d, "Spatial submit timed out, dropping frame"); + std::free(frame.sample_buffer); + 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; + } +} + s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { LOG_INFO(Lib_Audio3d, "called, handle = {}", handle); @@ -588,6 +817,8 @@ 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; @@ -678,7 +909,9 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { 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 = @@ -693,8 +926,14 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { 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; } @@ -957,7 +1196,15 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, const u32 depth = port.parameters.queue_depth; - if (port.audio_out_handle < 0) { + // Prefer queueing on the port's own OpenAL source else fall back to AudioOut if + // spatial init failed. + 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); @@ -984,8 +1231,13 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, 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; diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h index 4dac49d65..eae9606c5 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 @@ -101,6 +102,12 @@ struct ObjectState { std::unordered_map> persistent_attributes; }; +struct SpatialBed { + u32 source{0}; // ALuint + std::vector buffers; // ALuint ring, sized queue_depth + slack + std::vector available; // reclaimed / never-queued buffer ids +}; + struct Port { mutable std::recursive_mutex mutex; OrbisAudio3dOpenParameters parameters{}; @@ -116,6 +123,15 @@ struct Port { std::deque bed_queue; // Mixed stereo frames ready to be consumed by sceAudio3dPortPush. std::deque mixed_queue; + + // Spatial (direct OpenAL) output state. + bool spatial_init_attempted{false}; + bool spatial_ready{false}; + std::string device_name; + u64 period_us{0}; + u64 last_volume_check_us{0}; + float current_gain{-1.0f}; + SpatialBed bed; }; struct Audio3dState { From 6066bda3f9414c6f77788cf6fb7c39016d9848b8 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 8 Jul 2026 19:18:44 +0300 Subject: [PATCH 02/14] sceAudio3dAudioOutOutput is now non-blocking. --- src/core/libraries/audio3d/audio3d.cpp | 124 +++++++++++++++-- src/core/libraries/audio3d/audio3d.h | 12 +- src/core/libraries/audio3d/audio3d_openal.cpp | 128 +++++++++++++++--- src/core/libraries/audio3d/audio3d_openal.h | 11 +- 4 files changed, 243 insertions(+), 32 deletions(-) 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 90a02ae0b..e266db7c1 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -31,6 +31,58 @@ 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)}; + } +} + +// Submit all sample buffers pending on the game's associated AudioOut handles. +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. @@ -172,6 +224,7 @@ static bool EnsureSpatial(Port& port) { } port.bed.available = port.bed.buffers; + // The bed carries the final stereo mix and must not be spatialized. 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); @@ -229,7 +282,6 @@ static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { return ORBIS_OK; } - // Per-source underrun restart, same as the AudioOut OpenAL backend. ALint al_state = 0; alGetSourcei(port.bed.source, AL_SOURCE_STATE, &al_state); if (al_state != AL_PLAYING) { @@ -256,12 +308,11 @@ static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { 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; }); } } @@ -293,8 +344,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; } @@ -311,6 +366,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); + + return static_cast(aout.samples_per_buffer); + } + } + } + + // Handle isn't associated with any Audio3d port; forward directly. return AudioOut::sceAudioOutOutput(handle, ptr); } @@ -323,7 +408,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, @@ -824,10 +916,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); @@ -883,9 +975,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; } @@ -1196,8 +1291,10 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, const u32 depth = port.parameters.queue_depth; - // Prefer queueing on the port's own OpenAL source else fall back to AudioOut if - // spatial init failed. + if (const s32 ret = DrainAssociatedPorts(port); ret < 0) { + return ret; + } + bool spatial; { std::scoped_lock lock{port.mutex}; @@ -1268,7 +1365,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, return ORBIS_OK; } - // SYNC: ensure at least one slot is free (drain until size < depth). while (true) { { std::scoped_lock lock{port.mutex}; diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h index eae9606c5..19f395611 100644 --- a/src/core/libraries/audio3d/audio3d_openal.h +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -102,6 +102,13 @@ struct ObjectState { std::unordered_map> persistent_attributes; }; +struct AssociatedAudioOutPort { + s32 handle{-1}; + u32 buffer_bytes{0}; + u32 samples_per_buffer{0}; + std::deque> pending; +}; + struct SpatialBed { u32 source{0}; // ALuint std::vector buffers; // ALuint ring, sized queue_depth + slack @@ -113,8 +120,8 @@ 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. From 24ef9d8beb1e2ecd282575a874e535be8480bfca Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 8 Jul 2026 19:53:30 +0300 Subject: [PATCH 03/14] objects now play on their own OpenAL sources --- src/core/libraries/audio3d/audio3d_openal.cpp | 406 +++++++++++++----- src/core/libraries/audio3d/audio3d_openal.h | 31 +- 2 files changed, 320 insertions(+), 117 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index e266db7c1..6f2a2567c 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -58,7 +58,6 @@ static AudioOutBufferInfo GetAudioOutBufferInfo(const AudioOut::OrbisAudioOutPar } } -// Submit all sample buffers pending on the game's associated AudioOut handles. static s32 DrainAssociatedPorts(Port& port) { while (true) { s32 handle = -1; @@ -109,20 +108,93 @@ static const char* ALErrorString(const ALenum error) { } } -static void SpatialReclaimProcessedLocked(Port& port) { +static void SpatialReclaimSource(SpatialSource& src) { ALint processed = 0; - alGetSourcei(port.bed.source, AL_BUFFERS_PROCESSED, &processed); + alGetSourcei(src.source, AL_BUFFERS_PROCESSED, &processed); while (processed-- > 0) { ALuint buffer_id = 0; - alSourceUnqueueBuffers(port.bed.source, 1, &buffer_id); + alSourceUnqueueBuffers(src.source, 1, &buffer_id); if (alGetError() != AL_NO_ERROR) { break; } - port.bed.available.push_back(buffer_id); + 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); + // Orbis GAIN is linear and may amplify (>1.0, see the Transport Layer spec); + // lift OpenAL's default 1.0 source gain cap. + 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 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); +} + static void SpatialUpdateVolumeLocked(Port& port) { const u64 now = Kernel::sceKernelGetProcessTime(); if (now - port.last_volume_check_us < SPATIAL_VOLUME_CHECK_INTERVAL_US) { @@ -156,26 +228,22 @@ static void DestroySpatial(Port& port) { auto& device = AudioOut::OpenALDevice::GetInstance(); if (device.MakeCurrent(port.device_name)) { - ALuint source = port.bed.source; - if (source != 0) { - alSourceStop(source); - - ALint queued = 0; - alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); - while (queued-- > 0) { - ALuint buffer_id = 0; - alSourceUnqueueBuffers(source, 1, &buffer_id); - } - - alDeleteSources(1, &source); + DestroySpatialSourceLocked(port.bed); + for (auto& [object_id, obj] : port.objects) { + DestroySpatialSourceLocked(obj.al); } - if (!port.bed.buffers.empty()) { - alDeleteBuffers(static_cast(port.bed.buffers.size()), - reinterpret_cast(port.bed.buffers.data())); + } else { + port.bed = SpatialSource{}; + for (auto& [object_id, obj] : port.objects) { + obj.al = SpatialSource{}; } } - port.bed = SpatialBed{}; + for (auto& bundle : port.spatial_queue) { + FreeBundle(bundle); + } + port.spatial_queue.clear(); + device.UnregisterPort(port.device_name); } @@ -203,39 +271,15 @@ static bool EnsureSpatial(Port& port) { alGetError(); - ALuint source = 0; - alGenSources(1, &source); - if (alGetError() != AL_NO_ERROR) { - LOG_ERROR(Lib_Audio3d, "Failed to generate spatial source"); + if (!CreateSpatialSource(port, port.bed)) { device.UnregisterPort(port.device_name); return false; } - const u32 ring_size = port.parameters.queue_depth + SPATIAL_RING_SLACK; - port.bed.buffers.resize(ring_size); - alGenBuffers(static_cast(ring_size), - reinterpret_cast(port.bed.buffers.data())); - if (alGetError() != AL_NO_ERROR) { - LOG_ERROR(Lib_Audio3d, "Failed to generate spatial buffers"); - alDeleteSources(1, &source); - port.bed.buffers.clear(); - device.UnregisterPort(port.device_name); - return false; - } - port.bed.available = port.bed.buffers; - - // The bed carries the final stereo mix and must not be spatialized. - 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); - const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; - alSourcef(source, AL_GAIN, slider_gain); + alSourcef(port.bed.source, AL_GAIN, slider_gain); port.current_gain = slider_gain; - port.bed.source = source; port.period_us = (1000000ULL * port.parameters.granularity + AUDIO3D_SAMPLE_RATE / 2) / AUDIO3D_SAMPLE_RATE; port.spatial_ready = true; @@ -243,12 +287,92 @@ static bool EnsureSpatial(Port& port) { LOG_INFO(Lib_Audio3d, "Spatial output initialized on OpenAL device '{}' (granularity {}, ring {})", port.device_name.empty() ? "Default Device" : port.device_name, - port.parameters.granularity, ring_size); + port.parameters.granularity, port.parameters.queue_depth + SPATIAL_RING_SLACK); return true; } -static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { - const u32 bytes = frame.num_samples * AUDIO3D_OUTPUT_NUM_CHANNELS * sizeof(s16); +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 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 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) { @@ -258,44 +382,44 @@ static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { if (!port.spatial_ready || !AudioOut::OpenALDevice::GetInstance().MakeCurrent(port.device_name)) { LOG_ERROR(Lib_Audio3d, "Spatial output lost, dropping frame"); - std::free(frame.sample_buffer); + FreeBundle(bundle); return ORBIS_OK; } SpatialUpdateVolumeLocked(port); - SpatialReclaimProcessedLocked(port); - if (!port.bed.available.empty()) { - const ALuint buffer_id = port.bed.available.back(); - port.bed.available.pop_back(); + 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; + } - alGetError(); - alBufferData(buffer_id, AL_FORMAT_STEREO16, frame.sample_buffer, - static_cast(bytes), AUDIO3D_SAMPLE_RATE); - ALuint queue_id = buffer_id; - alSourceQueueBuffers(port.bed.source, 1, &queue_id); + auto& src = it->second.al; + if (src.source == 0 && !CreateSpatialSource(port, src)) { + continue; + } - if (const ALenum err = alGetError(); err != AL_NO_ERROR) { - LOG_ERROR(Lib_Audio3d, "Failed to queue spatial frame: {}", ALErrorString(err)); - port.bed.available.push_back(buffer_id); - std::free(frame.sample_buffer); - return ORBIS_OK; + alSourcef(src.source, AL_GAIN, frame.gain * port.current_gain); + + 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); + } } - ALint al_state = 0; - alGetSourcei(port.bed.source, AL_SOURCE_STATE, &al_state); - if (al_state != AL_PLAYING) { - alSourcePlay(port.bed.source); - } - - std::free(frame.sample_buffer); + FreeBundle(bundle); return ORBIS_OK; } } if (waited_us >= SPATIAL_SUBMIT_TIMEOUT_US) { LOG_WARNING(Lib_Audio3d, "Spatial submit timed out, dropping frame"); - std::free(frame.sample_buffer); + FreeBundle(bundle); return ORBIS_OK; } @@ -305,9 +429,17 @@ static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { } } +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. 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}; @@ -390,12 +522,12 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { 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); } } } - // Handle isn't associated with any Audio3d port; forward directly. return AudioOut::sceAudioOutOutput(handle, ptr); } @@ -646,18 +778,17 @@ 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; } - // Store the attribute so it's available when we implement it. const auto* src = static_cast(attribute); obj.persistent_attributes[static_cast(attribute_id)].assign(src, src + attribute_size); @@ -700,12 +831,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. + break; } } - // Second pass: apply all other attributes. for (u64 i = 0; i < num_attributes; i++) { const auto& attr = attribute_array[i]; @@ -727,8 +858,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( @@ -766,6 +895,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; } @@ -779,14 +915,20 @@ 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", + // Initialize spatial output on the first Advance so all frames take one + // consistent path from the start. + 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; } @@ -862,18 +1004,24 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { // 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); + 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))); @@ -889,10 +1037,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; } @@ -988,7 +1143,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(), @@ -1000,7 +1155,7 @@ 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; } @@ -1017,7 +1172,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } - // Drain all queued mixed frames, blocking on each until consumed. + // Drain all queued frames, blocking on each until consumed. while (!port.mixed_queue.empty()) { AudioData frame = port.mixed_queue.front(); port.mixed_queue.pop_front(); @@ -1034,6 +1189,16 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } + 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; + } + } + return ORBIS_OK; } @@ -1103,7 +1268,8 @@ 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(); + // Exactly one of these queues is in use depending on the output path. + const size_t size = port.mixed_queue.size() + port.spatial_queue.size(); if (queue_level) { *queue_level = static_cast(size); @@ -1315,6 +1481,32 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, // 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}; @@ -1328,13 +1520,8 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, port.mixed_queue.pop_front(); } - s32 ret; - if (spatial) { - ret = SpatialSubmitFrame(port, frame); - } else { - ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); - std::free(frame.sample_buffer); - } + const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); if (ret < 0) return ret; @@ -1346,7 +1533,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, // 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; } } @@ -1365,10 +1552,11 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, 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; } diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h index 19f395611..662de6f4e 100644 --- a/src/core/libraries/audio3d/audio3d_openal.h +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -97,9 +97,27 @@ struct AudioData { OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; }; +struct SpatialSource { + u32 source{0}; // ALuint + 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}; +}; + +struct SpatialFrameBundle { + AudioData bed{}; + std::vector objects; }; struct AssociatedAudioOutPort { @@ -109,12 +127,6 @@ struct AssociatedAudioOutPort { std::deque> pending; }; -struct SpatialBed { - u32 source{0}; // ALuint - std::vector buffers; // ALuint ring, sized queue_depth + slack - std::vector available; // reclaimed / never-queued buffer ids -}; - struct Port { mutable std::recursive_mutex mutex; OrbisAudio3dOpenParameters parameters{}; @@ -128,8 +140,10 @@ struct 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 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}; @@ -138,7 +152,8 @@ struct Port { u64 period_us{0}; u64 last_volume_check_us{0}; float current_gain{-1.0f}; - SpatialBed bed; + SpatialSource bed; + std::vector spatial_scratch; }; struct Audio3dState { From 21d3d6a52875abcec450287c152aea0e4c440cab Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 09:12:36 +0300 Subject: [PATCH 04/14] objects are now actually spatialized, POSITION and SPREAD --- src/core/libraries/audio3d/audio3d_openal.cpp | 121 +++++++++++++----- src/core/libraries/audio3d/audio3d_openal.h | 13 +- 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 6f2a2567c..54988de07 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -2,13 +2,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include +#include #include #include #include #include +#include #include - #include "common/assert.h" #include "common/logging/log.h" #include "core/emulator_settings.h" @@ -148,8 +150,6 @@ static bool CreateSpatialSource(const Port& port, SpatialSource& src) { alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); alSourcei(source, AL_LOOPING, AL_FALSE); alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); - // Orbis GAIN is linear and may amplify (>1.0, see the Transport Layer spec); - // lift OpenAL's default 1.0 source gain cap. alSourcef(source, AL_MAX_GAIN, 16.0f); src.source = source; @@ -193,6 +193,12 @@ static void ResetObjectSpatialLocked(Port& port, ObjectState& obj) { } 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); + } } static void SpatialUpdateVolumeLocked(Port& port) { @@ -270,6 +276,11 @@ static bool EnsureSpatial(Port& port) { } alGetError(); + alDistanceModel(AL_NONE); + alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); + + // AL_EXT_SOURCE_RADIUS lets us approximate the Orbis SPREAD attribute. + port.source_radius_supported = alIsExtensionPresent("AL_EXT_SOURCE_RADIUS") == AL_TRUE; if (!CreateSpatialSource(port, port.bed)) { device.UnregisterPort(port.device_name); @@ -303,6 +314,49 @@ static float GetObjectGain(const ObjectState& obj) { 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 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; @@ -402,8 +456,19 @@ static s32 SpatialSubmitBundle(Port& port, SpatialFrameBundle bundle) { continue; } + // Snapshotted at Advance; volume slider applied on top. alSourcef(src.source, AL_GAIN, frame.gain * port.current_gain); + 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)) { @@ -437,9 +502,6 @@ static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { LOG_INFO(Lib_Audio3d, "called, handle = {}", 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}; @@ -789,6 +851,7 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(const OrbisAudio3dPortId port_id, return ORBIS_OK; } + // Store the attribute so it's available when we implement it. const auto* src = static_cast(attribute); obj.persistent_attributes[static_cast(attribute_id)].assign(src, src + attribute_size); @@ -833,10 +896,11 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, obj.persistent_attributes.clear(); ResetObjectSpatialLocked(port, obj); LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); - break; + 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]; @@ -858,6 +922,8 @@ 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( @@ -922,8 +988,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; } - // Initialize spatial output on the first Advance so all frames take one - // consistent path from the start. const bool spatial = EnsureSpatial(port); if ((spatial ? port.spatial_queue.size() : port.mixed_queue.size()) >= @@ -1016,6 +1080,8 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_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); bundle.objects.push_back(std::move(frame)); } } else { @@ -1144,7 +1210,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } 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(), [](const auto& kv) { return !kv.second.pcm_queue.empty(); })) { @@ -1172,7 +1237,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } - // Drain all queued frames, blocking on each until consumed. while (!port.mixed_queue.empty()) { AudioData frame = port.mixed_queue.front(); port.mixed_queue.pop_front(); @@ -1219,25 +1283,25 @@ 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_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; } @@ -1479,7 +1543,6 @@ 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{}; @@ -1530,7 +1593,6 @@ 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() + port.spatial_queue.size() < depth) { @@ -1538,7 +1600,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, } } - // Submit one frame to free space. bool submitted = false; s32 ret = submit_one_frame(submitted); if (ret < 0) @@ -1547,12 +1608,10 @@ 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}; @@ -1594,6 +1653,10 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } + LOG_INFO( + Lib_Audio3d, + "called (STUBBED), port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", + port_id, static_cast(attribute_id), attribute, attribute_size); // TODO return ORBIS_OK; diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h index 662de6f4e..81ec1cced 100644 --- a/src/core/libraries/audio3d/audio3d_openal.h +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -97,8 +97,14 @@ struct AudioData { OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; }; +struct OrbisAudio3dPosition { + float x; + float y; + float z; +}; + struct SpatialSource { - u32 source{0}; // ALuint + u32 source{0}; std::vector buffers; // ALuint ring, sized queue_depth + slack std::vector available; // reclaimed / never-queued buffer ids }; @@ -113,6 +119,9 @@ 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}; }; struct SpatialFrameBundle { @@ -141,6 +150,7 @@ struct Port { // Bed audio queue. std::deque bed_queue; // 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; @@ -148,6 +158,7 @@ struct Port { // Spatial (direct OpenAL) output state. bool spatial_init_attempted{false}; bool spatial_ready{false}; + bool source_radius_supported{false}; std::string device_name; u64 period_us{0}; u64 last_volume_check_us{0}; From 4326eb0053e5dbaf1f4f709ad6b0ff79b99bea77 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 10:18:01 +0300 Subject: [PATCH 05/14] Fixed ConvertS16Std8CH remap --- src/core/libraries/audio/openal_audio_out.cpp | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp index 015a32d3e..56c7e5134 100644 --- a/src/core/libraries/audio/openal_audio_out.cpp +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -240,8 +240,9 @@ private: al_buffer_float.resize(buffer_frames * num_channels); buffer_size_bytes = buffer_frames * num_channels * sizeof(float); } else { - al_buffer_s16.resize(buffer_frames * num_channels); - buffer_size_bytes = buffer_frames * num_channels * sizeof(s16); + const u32 out_channels = downmix_to_stereo ? 2u : num_channels; + al_buffer_s16.resize(buffer_frames * out_channels); + buffer_size_bytes = buffer_frames * out_channels * sizeof(s16); } // Select optimal converter function @@ -372,6 +373,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 +434,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 +442,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 +463,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 +471,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: @@ -506,6 +513,17 @@ private: } bool SelectConverter() { + if (downmix_to_stereo) { + // (which played 8ch interleaved data as garbled 4x-slow stereo). + 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 +574,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 +642,67 @@ 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]; + d[i * 2 + 0] = ClampSampleToS16(s[o + FL] + center + 0.7071f * s[o + 4]); + d[i * 2 + 1] = ClampSampleToS16(s[o + FR] + center + 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]; + d[i * 2 + 0] = ClampSampleToS16(s[o + FL] + center + 0.7071f * (s[o + 4] + s[o + 6])); + d[i * 2 + 1] = ClampSampleToS16(s[o + FR] + center + 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]; + d[i * 2 + 0] = OrbisFloatToS16(s[o + FL] + center + 0.7071f * s[o + 4]); + d[i * 2 + 1] = OrbisFloatToS16(s[o + FR] + center + 0.7071f * s[o + 5]); + } + } + 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]; + d[i * 2 + 0] = OrbisFloatToS16(s[o + FL] + center + 0.7071f * (s[o + 4] + s[o + 6])); + d[i * 2 + 1] = OrbisFloatToS16(s[o + FR] + center + 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,6 +893,7 @@ private: // Extension support bool has_float_ext{false}; bool use_native_float{false}; + bool downmix_to_stereo{false}; // Converter function pointer ConverterFunc convert{nullptr}; From a6d4b2685a5c893eab7e0b13ad97aed63e8b234f Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 14:06:30 +0300 Subject: [PATCH 06/14] enable HRTF if available --- src/core/emulator_settings.h | 13 ++++- src/core/libraries/audio/openal_manager.h | 62 ++++++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) 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_manager.h b/src/core/libraries/audio/openal_manager.h index 4a6ef7920..86841adce 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,21 @@ #include #include +#include "common/logging/log.h" +#include "core/emulator_settings.h" + +#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 +212,21 @@ private: return false; } - // Create context - ctx.context = alcCreateContext(ctx.device, nullptr); + // Create context, requesting HRTF per the user setting when the device + // supports ALC_SOFT_HRTF. In Auto mode OpenAL Soft decides on its own + const ALCint* attr_ptr = nullptr; + std::array attrs{}; + 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 = {ALC_HRTF_SOFT, hrtf_value, 0}; + attr_ptr = attrs.data(); + } + + ctx.context = alcCreateContext(ctx.device, attr_ptr); if (!ctx.context) { LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context"); alcCloseDevice(ctx.device); @@ -214,10 +243,39 @@ private: } ctx.device_name = actual_name ? actual_name : "Unknown"; + 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 From c84963cee6fe98d5fffb1910a73b3a7d1bda3cd0 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 14:25:41 +0300 Subject: [PATCH 07/14] Added OrbisAudio3dPassthrough support --- src/core/libraries/audio3d/audio3d_openal.cpp | 78 ++++++++++++++++--- src/core/libraries/audio3d/audio3d_openal.h | 9 +++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 54988de07..397f0b040 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -11,6 +11,7 @@ #include #include #include + #include "common/assert.h" #include "common/logging/log.h" #include "core/emulator_settings.h" @@ -199,6 +200,9 @@ static void ResetObjectSpatialLocked(Port& port, ObjectState& obj) { 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) { @@ -276,11 +280,12 @@ static bool EnsureSpatial(Port& port) { } alGetError(); + alDistanceModel(AL_NONE); alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); - // AL_EXT_SOURCE_RADIUS lets us approximate the Orbis SPREAD attribute. 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); @@ -339,6 +344,21 @@ static float GetObjectSpread(const ObjectState& obj) { 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; @@ -393,6 +413,23 @@ static const s16* ConvertObjectFrameToMonoS16(Port& port, const AudioData& data, 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); @@ -414,7 +451,6 @@ static bool SpatialUploadLocked(SpatialSource& src, const void* samples, const u 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) { @@ -456,9 +492,31 @@ static s32 SpatialSubmitBundle(Port& port, SpatialFrameBundle bundle) { continue; } - // Snapshotted at Advance; volume slider applied on top. 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 (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, @@ -502,6 +560,7 @@ static s32 SpatialSubmitFrame(Port& port, const AudioData& frame) { s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { LOG_INFO(Lib_Audio3d, "called, handle = {}", handle); + if (state) { for (auto& [port_id, port] : state->ports) { std::scoped_lock lock{port.mutex}; @@ -621,14 +680,12 @@ 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) { return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; } - // Copy min(provided, granularity) samples — extra are dropped, shortage stays zero. const u32 samples_to_copy = std::min(pcm.num_samples, granularity); std::memcpy(copy, pcm.sample_buffer, samples_to_copy * num_channels * bytes_per_sample); @@ -1065,7 +1122,6 @@ 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); SpatialFrameBundle bundle{}; @@ -1082,6 +1138,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { 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 { @@ -1237,6 +1294,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } + // Drain all queued frames, blocking on each until consumed. while (!port.mixed_queue.empty()) { AudioData frame = port.mixed_queue.front(); port.mixed_queue.pop_front(); @@ -1288,6 +1346,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId port_id 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, }; @@ -1550,7 +1609,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, 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()) { @@ -1600,6 +1658,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, } } + // Submit one frame to free space. bool submitted = false; s32 ret = submit_one_frame(submitted); if (ret < 0) @@ -1639,10 +1698,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, const OrbisAudio3dAttributeId 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); - if (!state->ports.contains(port_id)) { LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); return ORBIS_AUDIO3D_ERROR_INVALID_PORT; @@ -1657,7 +1712,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, Lib_Audio3d, "called (STUBBED), port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", port_id, static_cast(attribute_id), attribute, attribute_size); - // TODO return ORBIS_OK; } diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h index 81ec1cced..203b7a25b 100644 --- a/src/core/libraries/audio3d/audio3d_openal.h +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -103,6 +103,12 @@ struct OrbisAudio3dPosition { 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 @@ -122,6 +128,7 @@ struct SpatialObjectFrame { 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 { @@ -159,12 +166,14 @@ struct Port { 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; }; struct Audio3dState { From fb23d2986ab1f569c86dbe80003538b475fa00a9 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 14:48:18 +0300 Subject: [PATCH 08/14] LATE_REVERB_LEVEL through EFX reverb --- src/core/libraries/audio3d/audio3d_openal.cpp | 160 +++++++++++++++--- src/core/libraries/audio3d/audio3d_openal.h | 15 +- 2 files changed, 154 insertions(+), 21 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 397f0b040..69c52d847 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -10,8 +10,8 @@ #include #include #include +#include #include - #include "common/assert.h" #include "common/logging/log.h" #include "core/emulator_settings.h" @@ -187,6 +187,95 @@ static void FreeBundle(SpatialFrameBundle& bundle) { 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)) { @@ -242,6 +331,12 @@ static void DestroySpatial(Port& port) { 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) { @@ -249,6 +344,10 @@ static void DestroySpatial(Port& port) { } } + port.reverb_supported = false; + port.reverb_slot = 0; + port.reverb_effect = 0; + for (auto& bundle : port.spatial_queue) { FreeBundle(bundle); } @@ -280,7 +379,6 @@ static bool EnsureSpatial(Port& port) { } alGetError(); - alDistanceModel(AL_NONE); alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); @@ -296,6 +394,8 @@ static bool EnsureSpatial(Port& port) { alSourcef(port.bed.source, AL_GAIN, slider_gain); port.current_gain = slider_gain; + CreateSpatialReverbLocked(port); + port.period_us = (1000000ULL * port.parameters.granularity + AUDIO3D_SAMPLE_RATE / 2) / AUDIO3D_SAMPLE_RATE; port.spatial_ready = true; @@ -503,6 +603,13 @@ static s32 SpatialSubmitBundle(Port& port, SpatialFrameBundle bundle) { 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); @@ -680,12 +787,14 @@ 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) { return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; } + // Copy min(provided, granularity) samples — extra are dropped, shortage stays zero. const u32 samples_to_copy = std::min(pcm.num_samples, granularity); std::memcpy(copy, pcm.sample_buffer, samples_to_copy * num_channels * bytes_per_sample); @@ -855,7 +964,6 @@ s32 PS4_SYSV_ABI sceAudio3dObjectReserve(const OrbisAudio3dPortId port_id, 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 || @@ -942,7 +1050,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) { @@ -957,7 +1064,6 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, } } - // Second pass: apply all other attributes. for (u64 i = 0; i < num_attributes; i++) { const auto& attr = attribute_array[i]; @@ -1013,12 +1119,10 @@ s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; } - // Free any queued PCM audio for this object. for (auto& data : port.objects[object_id].pcm_queue) { 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)) { @@ -1066,7 +1170,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(); @@ -1294,7 +1397,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } } - // Drain all queued frames, blocking on each until consumed. while (!port.mixed_queue.empty()) { AudioData frame = port.mixed_queue.front(); port.mixed_queue.pop_front(); @@ -1351,8 +1453,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId port_id }; if (capabilities) { - // Writes up to num_capabilities supported capabilities, - // then sets num_capabilities to how many were written. const u32 caps_to_write = std::min(*num_capabilities, supported.size()); for (u32 i = 0; i < caps_to_write; i++) { capabilities[i] = supported[i]; @@ -1391,7 +1491,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(const OrbisAudio3dPortId port_id, u const auto& port = state->ports[port_id]; std::scoped_lock lock{port.mutex}; - // Exactly one of these queues is in use depending on the output path. const size_t size = port.mixed_queue.size() + port.spatial_queue.size(); if (queue_level) { @@ -1658,7 +1757,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, } } - // Submit one frame to free space. bool submitted = false; s32 ret = submit_one_frame(submitted); if (ret < 0) @@ -1696,8 +1794,12 @@ 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); + if (!state->ports.contains(port_id)) { LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); return ORBIS_AUDIO3D_ERROR_INVALID_PORT; @@ -1708,12 +1810,32 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } - LOG_INFO( - Lib_Audio3d, - "called (STUBBED), port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", - port_id, static_cast(attribute_id), attribute, attribute_size); + 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)); + port.late_reverb_level = std::clamp(level, 0.0f, 1.0f); + SpatialApplyReverbLevelLocked(port); + LOG_INFO(Lib_Audio3d, "late reverb level = {}", port.late_reverb_level); + return ORBIS_OK; + } + case OrbisAudio3dPortAttributeId::ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS: + case OrbisAudio3dPortAttributeId::ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE: + LOG_DEBUG(Lib_Audio3d, "port attribute {:#x} accepted (not implemented)", + static_cast(attribute_id)); + 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 203b7a25b..afa2ad513 100644 --- a/src/core/libraries/audio3d/audio3d_openal.h +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -83,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; @@ -174,6 +180,11 @@ struct Port { 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 { @@ -233,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(); From fa4d42d770ad14aa66184e4165c3b608248c5c52 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 15:58:48 +0300 Subject: [PATCH 09/14] reduced logging spam --- src/core/libraries/audio3d/audio3d_openal.cpp | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 69c52d847..45948d725 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -787,14 +787,12 @@ 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) { return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; } - // Copy min(provided, granularity) samples — extra are dropped, shortage stays zero. const u32 samples_to_copy = std::min(pcm.num_samples, granularity); std::memcpy(copy, pcm.sample_buffer, samples_to_copy * num_channels * bytes_per_sample); @@ -957,7 +955,6 @@ 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); @@ -1119,10 +1116,12 @@ s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; } + // Free any queued PCM audio for this object. for (auto& data : port.objects[object_id].pcm_queue) { 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)) { @@ -1225,6 +1224,7 @@ 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); SpatialFrameBundle bundle{}; @@ -1708,6 +1708,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, 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()) { @@ -1796,9 +1797,9 @@ s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_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)"); @@ -1821,7 +1822,13 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, } float level = 0.0f; std::memcpy(&level, attribute, sizeof(float)); - port.late_reverb_level = std::clamp(level, 0.0f, 1.0f); + 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; From 6ba50a986256dabb1664baef1daf3e8daa934b1b Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 19:05:53 +0300 Subject: [PATCH 10/14] added direct soft channels --- src/core/libraries/audio/openal_audio_out.cpp | 6 +++++- src/core/libraries/audio3d/audio3d_openal.cpp | 17 ++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp index 56c7e5134..2644a6d8a 100644 --- a/src/core/libraries/audio/openal_audio_out.cpp +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -508,13 +508,17 @@ private: alSourcei(source, AL_LOOPING, AL_FALSE); alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); + if ((num_channels == 2 || downmix_to_stereo) && + alIsExtensionPresent("AL_SOFT_direct_channels")) { + alSourcei(source, AL_DIRECT_CHANNELS_SOFT, AL_TRUE); + } + LOG_DEBUG(Lib_AudioOut, "Created OpenAL source {} with {} buffers", source, buffers.size()); return true; } bool SelectConverter() { if (downmix_to_stereo) { - // (which played 8ch interleaved data as garbled 4x-slow stereo). if (is_float) { convert = num_channels == 8 ? &DownmixF32_8CHToStereoS16 : &DownmixF32_6CHToStereoS16; diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp index 45948d725..c0a713f50 100644 --- a/src/core/libraries/audio3d/audio3d_openal.cpp +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -12,6 +12,7 @@ #include #include #include + #include "common/assert.h" #include "common/logging/log.h" #include "core/emulator_settings.h" @@ -394,6 +395,10 @@ static bool EnsureSpatial(Port& port) { 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 = @@ -551,6 +556,7 @@ static bool SpatialUploadLocked(SpatialSource& src, const void* samples, const u 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) { @@ -793,6 +799,7 @@ static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; } + // Copy min(provided, granularity) samples — extra are dropped, shortage stays zero. const u32 samples_to_copy = std::min(pcm.num_samples, granularity); std::memcpy(copy, pcm.sample_buffer, samples_to_copy * num_channels * bytes_per_sample); @@ -1082,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( @@ -1224,7 +1229,6 @@ 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); SpatialFrameBundle bundle{}; @@ -1370,6 +1374,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { } 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(), [](const auto& kv) { return !kv.second.pcm_queue.empty(); })) { @@ -1459,7 +1464,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId port_id } *num_capabilities = caps_to_write; } else { - // If capabilities is null, then just report the number of supported capabilities. *num_capabilities = static_cast(supported.size()); } return ORBIS_OK; @@ -1833,11 +1837,6 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, LOG_INFO(Lib_Audio3d, "late reverb level = {}", port.late_reverb_level); return ORBIS_OK; } - case OrbisAudio3dPortAttributeId::ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS: - case OrbisAudio3dPortAttributeId::ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE: - LOG_DEBUG(Lib_Audio3d, "port attribute {:#x} accepted (not implemented)", - static_cast(attribute_id)); - return ORBIS_OK; default: LOG_WARNING(Lib_Audio3d, "unknown port attribute {:#x} (size {})", static_cast(attribute_id), attribute_size); From 3405d982f28665439e1783f0608a42dda312f42a Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 23:12:50 +0300 Subject: [PATCH 11/14] tried to fix low sound --- src/core/libraries/audio/openal_audio_out.cpp | 96 ++++++++++++++++--- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp index 2644a6d8a..2cc5fedc4 100644 --- a/src/core/libraries/audio/openal_audio_out.cpp +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -10,6 +10,17 @@ #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_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 +109,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); @@ -235,6 +250,26 @@ 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; + } + // Allocate buffers based on format if (use_native_float) { al_buffer_float.resize(buffer_frames * num_channels); @@ -508,9 +543,13 @@ private: alSourcei(source, AL_LOOPING, AL_FALSE); alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); - if ((num_channels == 2 || downmix_to_stereo) && - alIsExtensionPresent("AL_SOFT_direct_channels")) { - alSourcei(source, AL_DIRECT_CHANNELS_SOFT, 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()); @@ -649,7 +688,6 @@ private: 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]; @@ -673,8 +711,9 @@ private: for (u32 i = 0; i < frames; i++) { const u32 o = i * 6; const float center = 0.7071f * s[o + FC]; - d[i * 2 + 0] = ClampSampleToS16(s[o + FL] + center + 0.7071f * s[o + 4]); - d[i * 2 + 1] = ClampSampleToS16(s[o + FR] + center + 0.7071f * s[o + 5]); + 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*) { @@ -683,8 +722,11 @@ private: for (u32 i = 0; i < frames; i++) { const u32 o = i << 3; const float center = 0.7071f * s[o + FC]; - d[i * 2 + 0] = ClampSampleToS16(s[o + FL] + center + 0.7071f * (s[o + 4] + s[o + 6])); - d[i * 2 + 1] = ClampSampleToS16(s[o + FR] + center + 0.7071f * (s[o + 5] + s[o + 7])); + 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*) { @@ -693,18 +735,46 @@ private: for (u32 i = 0; i < frames; i++) { const u32 o = i * 6; const float center = 0.7071f * s[o + FC]; - d[i * 2 + 0] = OrbisFloatToS16(s[o + FL] + center + 0.7071f * s[o + 4]); - d[i * 2 + 1] = OrbisFloatToS16(s[o + FR] + center + 0.7071f * s[o + 5]); + 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 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]; - d[i * 2 + 0] = OrbisFloatToS16(s[o + FL] + center + 0.7071f * (s[o + 4] + s[o + 6])); - d[i * 2 + 1] = OrbisFloatToS16(s[o + FR] + center + 0.7071f * (s[o + 5] + s[o + 7])); + 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])); } } @@ -898,6 +968,8 @@ private: 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}; From bd6232ecbb28a60b63731742beea518357f4cf6a Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 9 Jul 2026 23:55:57 +0300 Subject: [PATCH 12/14] resampling --- src/core/libraries/audio/openal_manager.h | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/core/libraries/audio/openal_manager.h b/src/core/libraries/audio/openal_manager.h index 86841adce..faafe3fa5 100644 --- a/src/core/libraries/audio/openal_manager.h +++ b/src/core/libraries/audio/openal_manager.h @@ -12,6 +12,7 @@ #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 @@ -212,21 +213,23 @@ private: return false; } - // Create context, requesting HRTF per the user setting when the device - // supports ALC_SOFT_HRTF. In Auto mode OpenAL Soft decides on its own - const ALCint* attr_ptr = nullptr; - std::array attrs{}; + 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 = {ALC_HRTF_SOFT, hrtf_value, 0}; - attr_ptr = attrs.data(); + attrs[attr_count++] = ALC_HRTF_SOFT; + attrs[attr_count++] = hrtf_value; } + attrs[attr_count] = 0; - ctx.context = alcCreateContext(ctx.device, attr_ptr); + ctx.context = alcCreateContext(ctx.device, attrs.data()); if (!ctx.context) { LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context"); alcCloseDevice(ctx.device); @@ -242,6 +245,13 @@ 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; From dbc948f48593c24ca075b2337d3ddff6fa550ad8 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 10 Jul 2026 09:15:35 +0300 Subject: [PATCH 13/14] more try to fix low q --- src/core/libraries/audio/openal_audio_out.cpp | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp index 2cc5fedc4..3776df154 100644 --- a/src/core/libraries/audio/openal_audio_out.cpp +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -14,6 +14,11 @@ // 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 @@ -270,6 +275,20 @@ private: 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 = false; + format = 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); + } + // Allocate buffers based on format if (use_native_float) { al_buffer_float.resize(buffer_frames * num_channels); @@ -654,7 +673,6 @@ private: if (std::abs(v) < 1.0e-20f) v = 0.0f; - // Sony behavior: +1.0f -> 32767, -1.0f -> -32768 const float scaled = v * 32768.0f; if (scaled >= 32767.0f) @@ -740,7 +758,6 @@ private: 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) { From ede7195046f62758333673a59bc204c00993abc7 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 10 Jul 2026 10:43:41 +0300 Subject: [PATCH 14/14] improved sound volume control --- src/core/libraries/audio/openal_audio_out.cpp | 79 ++++++++++++++++--- src/core/libraries/audio/sdl_audio_out.cpp | 5 +- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp index 3776df154..28dce421f 100644 --- a/src/core/libraries/audio/openal_audio_out.cpp +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -186,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; @@ -282,19 +283,19 @@ private: if (know_output_mode && stereo_output && !hrtf_active && num_channels >= 6 && !downmix_to_stereo) { downmix_to_stereo = true; - use_native_float = false; - format = AL_FORMAT_STEREO16; + 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); + 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 { - const u32 out_channels = downmix_to_stereo ? 2u : num_channels; al_buffer_s16.resize(buffer_frames * out_channels); buffer_size_bytes = buffer_frames * out_channels * sizeof(s16); } @@ -387,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) { @@ -577,7 +579,15 @@ private: bool SelectConverter() { if (downmix_to_stereo) { - if (is_float) { + 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 { @@ -673,6 +683,7 @@ private: if (std::abs(v) < 1.0e-20f) v = 0.0f; + // Sony behavior: +1.0f -> 32767, -1.0f -> -32768 const float scaled = v * 32768.0f; if (scaled >= 32767.0f) @@ -706,6 +717,7 @@ private: 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]; @@ -758,6 +770,7 @@ private: 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) { @@ -781,6 +794,53 @@ private: } } + 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); @@ -993,6 +1053,7 @@ private: // 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/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};