diff --git a/.gitmodules b/.gitmodules index 55ae48ea3..14a8bdde7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -125,7 +125,6 @@ [submodule "externals/sdl3"] path = externals/sdl3 url = https://github.com/shadexternals/sdl3.git - branch = main [submodule "externals/cpp-httplib"] path = externals/cpp-httplib diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a2fe5ff2..e1d05cdce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,8 +296,15 @@ set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioout_backend.h src/core/libraries/audio/audioout_error.h src/core/libraries/audio/sdl_audio_out.cpp + src/core/libraries/audio/openal_audio_out.cpp + src/core/libraries/audio/openal_manager.h src/core/libraries/ngs2/ngs2.cpp src/core/libraries/ngs2/ngs2.h + src/core/libraries/audio3d/audio3d.cpp + src/core/libraries/audio3d/audio3d_openal.cpp + src/core/libraries/audio3d/audio3d_openal.h + src/core/libraries/audio3d/audio3d.h + src/core/libraries/audio3d/audio3d_error.h ) set(GNM_LIB src/core/libraries/gnmdriver/gnmdriver.cpp @@ -462,9 +469,6 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/ngs2/ngs2_submixer.cpp src/core/libraries/ngs2/ngs2_submixer.h src/core/libraries/ajm/ajm_error.h - src/core/libraries/audio3d/audio3d.cpp - src/core/libraries/audio3d/audio3d.h - src/core/libraries/audio3d/audio3d_error.h src/core/libraries/game_live_streaming/gamelivestreaming.cpp src/core/libraries/game_live_streaming/gamelivestreaming.h src/core/libraries/remote_play/remoteplay.cpp diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 41a0f71c7..fe0226712 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -280,7 +280,6 @@ if (NOT TARGET CLI11::CLI11) add_subdirectory(CLI11) endif() - #openal if (NOT TARGET OpenAL::OpenAL) set(ALSOFT_ENABLE_MODULES OFF CACHE BOOL "" FORCE) diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp index ec16c8aff..d96dc02f6 100644 --- a/src/core/libraries/audio/audioout.cpp +++ b/src/core/libraries/audio/audioout.cpp @@ -205,7 +205,7 @@ s32 PS4_SYSV_ABI sceAudioOutInit() { return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT; } - audio = std::make_unique(); + audio = std::make_unique(); LOG_INFO(Lib_AudioOut, "Audio system initialized"); return ORBIS_OK; diff --git a/src/core/libraries/audio/audioout_backend.h b/src/core/libraries/audio/audioout_backend.h index 0f36f19c8..ce6e261ab 100644 --- a/src/core/libraries/audio/audioout_backend.h +++ b/src/core/libraries/audio/audioout_backend.h @@ -31,4 +31,9 @@ public: std::unique_ptr Open(PortOut& port) override; }; +class OpenALAudioOut final : public AudioOutBackend { +public: + std::unique_ptr Open(PortOut& port) override; +}; + } // namespace Libraries::AudioOut diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp new file mode 100644 index 000000000..e5fdda355 --- /dev/null +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -0,0 +1,833 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/config.h" +#include "common/logging/log.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_backend.h" +#include "core/libraries/audio/openal_manager.h" +#include "core/libraries/kernel/threads.h" + +// SIMD support detection +#if defined(__x86_64__) || defined(_M_X64) +#include +#define HAS_SSE2 +#endif + +namespace Libraries::AudioOut { + +// Volume constants +constexpr float VOLUME_0DB = 32768.0f; // 1 << 15 +constexpr float INV_VOLUME_0DB = 1.0f / VOLUME_0DB; +constexpr float VOLUME_EPSILON = 0.001f; +// Timing constants +constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms +constexpr u64 MIN_SLEEP_THRESHOLD_US = 10; +constexpr u64 TIMING_RESYNC_THRESHOLD_US = 100000; // Resync if >100ms behind + +// OpenAL constants +constexpr ALsizei NUM_BUFFERS = 6; +constexpr ALsizei BUFFER_QUEUE_THRESHOLD = 2; // Queue more buffers when below this + +// Channel positions +enum ChannelPos : u8 { + FL = 0, + FR = 1, + FC = 2, + LF = 3, + SL = 4, + SR = 5, + BL = 6, + BR = 7, + STD_SL = 6, + STD_SR = 7, + STD_BL = 4, + STD_BR = 5 +}; + +class OpenALPortBackend : public PortBackend { +public: + explicit OpenALPortBackend(const PortOut& port) + : frame_size(port.format_info.FrameSize()), guest_buffer_size(port.BufferSize()), + buffer_frames(port.buffer_frames), sample_rate(port.sample_rate), + num_channels(port.format_info.num_channels), is_float(port.format_info.is_float), + is_std(port.format_info.is_std), channel_layout(port.format_info.channel_layout), + device_registered(false), device_name(GetDeviceName(port.type)) { + + if (!Initialize(port.type)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL audio backend"); + } + } + + ~OpenALPortBackend() override { + // Unregister port before cleanup + if (device_registered) { + OpenALDevice::GetInstance().UnregisterPort(device_name); + } + Cleanup(); + } + + void Output(void* ptr) override { + if (!source || !convert) [[unlikely]] { + return; + } + if (ptr == nullptr) [[unlikely]] { + return; + } + if (!device_context->MakeCurrent(device_name)) { + return; + } + + UpdateVolumeIfChanged(); + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + // Convert audio data ONCE per call + if (use_native_float) { + convert(ptr, al_buffer_float.data(), buffer_frames, nullptr); + } else { + convert(ptr, al_buffer_s16.data(), buffer_frames, nullptr); + } + + // Reclaim processed buffers + ALint processed = 0; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + + while (processed > 0) { + ALuint buffer_id; + alSourceUnqueueBuffers(source, 1, &buffer_id); + if (alGetError() == AL_NO_ERROR) { + available_buffers.push_back(buffer_id); + processed--; + } else { + break; + } + } + + // Queue buffer + if (!available_buffers.empty()) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + + if (use_native_float) { + alBufferData(buffer_id, format, al_buffer_float.data(), buffer_size_bytes, + sample_rate); + } else { + alBufferData(buffer_id, format, al_buffer_s16.data(), buffer_size_bytes, + sample_rate); + } + alSourceQueueBuffers(source, 1, &buffer_id); + } + + // Check state and queue health + ALint state = 0; + ALint queued = 0; + alGetSourcei(source, AL_SOURCE_STATE, &state); + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + + if (state != AL_PLAYING && queued > 0) { + LOG_DEBUG(Lib_AudioOut, "Audio underrun detected (queued: {}), restarting source", + queued); + alSourcePlay(source); + } + + // Only sleep if we have healthy buffer queue + if (queued >= 2) { + HandleTiming(current_time); + } else { + next_output_time = current_time + period_us; + } + + last_output_time.store(current_time, std::memory_order_release); + output_count++; + } + void SetVolume(const std::array& ch_volumes) override { + if (!device_context->MakeCurrent(device_name)) { + return; + } + + if (!source) [[unlikely]] { + return; + } + + float max_channel_gain = 0.0f; + const u32 channels_to_check = std::min(num_channels, 8u); + + for (u32 i = 0; i < channels_to_check; i++) { + const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; + max_channel_gain = std::max(max_channel_gain, channel_gain); + } + + const float slider_gain = Config::getVolumeSlider() * 0.01f; + const float total_gain = max_channel_gain * slider_gain; + + const float current = current_gain.load(std::memory_order_acquire); + if (std::abs(total_gain - current) < VOLUME_EPSILON) { + return; + } + + alSourcef(source, AL_GAIN, total_gain); + + ALenum error = alGetError(); + if (error == AL_NO_ERROR) { + current_gain.store(total_gain, std::memory_order_release); + LOG_DEBUG(Lib_AudioOut, + "Set combined audio gain to {:.3f} (channel: {:.3f}, slider: {:.3f})", + total_gain, max_channel_gain, slider_gain); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set OpenAL source gain: {}", + GetALErrorString(error)); + } + } + + u64 GetLastOutputTime() const { + return last_output_time.load(std::memory_order_acquire); + } + +private: + bool Initialize(OrbisAudioOutPort type) { + // Register this port with the device manager + if (!OpenALDevice::GetInstance().RegisterPort(device_name)) { + if (device_name == "None") { + LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}", + static_cast(type)); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to register OpenAL device '{}'", device_name); + } + return false; + } + + device_registered = true; + device_context = &OpenALDevice::GetInstance(); + + // Make this device's context current + if (!device_context->MakeCurrent(device_name)) { + LOG_ERROR(Lib_AudioOut, "Failed to make OpenAL context current for device '{}'", + device_name); + return false; + } + + // Log device info + LOG_INFO(Lib_AudioOut, "Using OpenAL device for port type {}: '{}'", static_cast(type), + device_name); + + // Calculate timing parameters + period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; + + // Check for AL_EXT_FLOAT32 extension + has_float_ext = alIsExtensionPresent("AL_EXT_FLOAT32"); + if (has_float_ext && is_float) { + LOG_INFO(Lib_AudioOut, "AL_EXT_FLOAT32 extension detected - using native float format"); + } + + // Determine OpenAL format + if (!DetermineOpenALFormat()) { + LOG_ERROR(Lib_AudioOut, "Unsupported audio format for OpenAL"); + return false; + } + + // Allocate buffers based on format + if (use_native_float) { + 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); + } + + // Select optimal converter function + if (!SelectConverter()) { + return false; + } + + // Generate OpenAL source and buffers + if (!CreateOpenALObjects()) { + return false; + } + + // Initialize current gain + current_gain.store(Config::getVolumeSlider() * 0.01f, std::memory_order_relaxed); + alSourcef(source, AL_GAIN, current_gain.load(std::memory_order_relaxed)); + + // Prime buffers with silence + if (use_native_float) { + std::vector silence(buffer_frames * num_channels, 0.0f); + for (size_t i = 0; i < buffers.size() - 1; i++) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + alBufferData(buffer_id, format, silence.data(), buffer_size_bytes, sample_rate); + alSourceQueueBuffers(source, 1, &buffer_id); + } + } else { + std::vector silence(buffer_frames * num_channels, 0); + for (size_t i = 0; i < buffers.size() - 1; i++) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + alBufferData(buffer_id, format, silence.data(), buffer_size_bytes, sample_rate); + alSourceQueueBuffers(source, 1, &buffer_id); + } + } + + alSourcePlay(source); + + LOG_INFO(Lib_AudioOut, + "Initialized OpenAL backend ({} Hz, {} ch, {} format, {}) for device '{}'", + sample_rate, num_channels, is_float ? "float" : "int16", + use_native_float ? "native" : "converted", device_name); + return true; + } + + void Cleanup() { + if (!device_context || !device_context->MakeCurrent(device_name)) { + return; + } + + if (source) { + alSourceStop(source); + + ALint queued = 0; + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + while (queued-- > 0) { + ALuint buf; + alSourceUnqueueBuffers(source, 1, &buf); + } + + alDeleteSources(1, &source); + source = 0; + } + + if (!buffers.empty()) { + alDeleteBuffers(static_cast(buffers.size()), buffers.data()); + buffers.clear(); + } + } + + std::string GetDeviceName(OrbisAudioOutPort type) const { + switch (type) { + case OrbisAudioOutPort::Main: + case OrbisAudioOutPort::Bgm: + return Config::getMainOutputDevice(); + case OrbisAudioOutPort::PadSpk: + return Config::getPadSpkOutputDevice(); + default: + return Config::getMainOutputDevice(); + } + } + + void UpdateVolumeIfChanged() { + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + if (current_time - last_volume_check_time < VOLUME_CHECK_INTERVAL_US) { + return; + } + + last_volume_check_time = current_time; + + const float config_volume = Config::getVolumeSlider() * 0.01f; + const float stored_gain = current_gain.load(std::memory_order_acquire); + + if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) { + alSourcef(source, AL_GAIN, config_volume); + + ALenum error = alGetError(); + if (error == AL_NO_ERROR) { + current_gain.store(config_volume, std::memory_order_release); + LOG_DEBUG(Lib_AudioOut, "Updated audio gain to {:.3f}", config_volume); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set audio gain: {}", GetALErrorString(error)); + } + } + } + + void HandleTiming(u64 current_time) { + if (next_output_time == 0) [[unlikely]] { + next_output_time = current_time + period_us; + return; + } + + const s64 time_diff = static_cast(current_time - next_output_time); + + if (time_diff > static_cast(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] { + next_output_time = current_time + period_us; + } else if (time_diff < 0) { + const u64 time_to_wait = static_cast(-time_diff); + next_output_time += period_us; + + if (time_to_wait > MIN_SLEEP_THRESHOLD_US) { + const u64 sleep_duration = time_to_wait - MIN_SLEEP_THRESHOLD_US; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration)); + } + } else { + next_output_time += period_us; + } + } + + bool DetermineOpenALFormat() { + // Try to use native float formats if extension is available + if (is_float && has_float_ext) { + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO_FLOAT32; + use_native_float = true; + return true; + case 2: + format = AL_FORMAT_STEREO_FLOAT32; + use_native_float = true; + return true; + case 4: + format = alGetEnumValue("AL_FORMAT_QUAD32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + } + + LOG_WARNING( + Lib_AudioOut, + "Float format for {} channels not supported, falling back to S16 conversion", + num_channels); + } + + // Fall back to S16 formats (with conversion if needed) + use_native_float = false; + + if (is_float) { + // Will need to convert float to S16 + format = AL_FORMAT_MONO16; + + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + // Native 16-bit integer formats + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); + return false; + } + } + + return true; + } + + bool CreateOpenALObjects() { + alGenSources(1, &source); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL source"); + return false; + } + + buffers.resize(NUM_BUFFERS); + alGenBuffers(static_cast(buffers.size()), buffers.data()); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL buffers"); + alDeleteSources(1, &source); + source = 0; + return false; + } + + available_buffers = buffers; + + alSourcef(source, AL_PITCH, 1.0f); + alSourcef(source, AL_GAIN, 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); + + LOG_DEBUG(Lib_AudioOut, "Created OpenAL source {} with {} buffers", source, buffers.size()); + return true; + } + + bool SelectConverter() { + if (is_float && use_native_float) { + // Native float - just copy/remap if needed + switch (num_channels) { + case 1: + convert = &ConvertF32Mono; + break; + case 2: + convert = &ConvertF32Stereo; + break; + case 8: + convert = is_std ? &ConvertF32Std8CH : &ConvertF32_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else if (is_float && !use_native_float) { + // Float to S16 conversion needed + switch (num_channels) { + case 1: + convert = &ConvertF32ToS16Mono; + break; + case 2: +#ifdef HAS_SSE2 + convert = &ConvertF32ToS16StereoSIMD; +#else + convert = &ConvertF32ToS16Stereo; +#endif + break; + case 8: +#ifdef HAS_SSE2 + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH_SIMD; +#else + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH; +#endif + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + // S16 native - just copy + switch (num_channels) { + case 1: + convert = &ConvertS16Mono; + break; + case 2: + convert = &ConvertS16Stereo; + break; + case 8: + convert = &ConvertS16_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); + return false; + } + } + + return true; + } + + const char* GetALErrorString(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"; + } + } + + // Converter function type + using ConverterFunc = void (*)(const void* src, void* dst, u32 frames, const float* volumes); + + static inline s16 OrbisFloatToS16(float v) { + 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) + return 32767; + if (scaled <= -32768.0f) + return -32768; + + return static_cast(scaled + (scaled >= 0 ? 0.5f : -0.5f)); + } + static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + std::memcpy(d, s, frames * sizeof(s16)); + } + + static void ConvertS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + std::memcpy(d, s, num_samples * sizeof(s16)); + } + + static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + std::memcpy(d, s, num_samples * sizeof(s16)); + } + + // Float passthrough converters (for AL_EXT_FLOAT32) + static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * sizeof(float)); + } + + static void ConvertF32Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * 2 * sizeof(float)); + } + + static void ConvertF32_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * 8 * sizeof(float)); + } + + static void ConvertF32Std8CH(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 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]; + } + } + + // Float to S16 converters for OpenAL + static void ConvertF32ToS16Mono(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++) + d[i] = OrbisFloatToS16(s[i]); + } +#ifdef HAS_SSE2 + static void ConvertF32ToS16StereoSIMD(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(32768.0f); + const __m128 min_val = _mm_set1_ps(-32768.0f); + const __m128 max_val = _mm_set1_ps(32767.0f); + + const u32 num_samples = frames << 1; + u32 i = 0; + + // Process 8 samples at a time + for (; i + 8 <= num_samples; i += 8) { + // Load 8 floats + __m128 f1 = _mm_loadu_ps(&s[i]); + __m128 f2 = _mm_loadu_ps(&s[i + 4]); + + // Scale and clamp + f1 = _mm_mul_ps(f1, scale); + f2 = _mm_mul_ps(f2, scale); + f1 = _mm_max_ps(f1, min_val); + f2 = _mm_max_ps(f2, min_val); + f1 = _mm_min_ps(f1, max_val); + f2 = _mm_min_ps(f2, max_val); + + // Convert to int32 + __m128i i1 = _mm_cvtps_epi32(f1); + __m128i i2 = _mm_cvtps_epi32(f2); + + // Pack to int16 + __m128i packed = _mm_packs_epi32(i1, i2); + + // Store + _mm_storeu_si128(reinterpret_cast<__m128i*>(&d[i]), packed); + } + + // Handle remaining samples + for (; i < num_samples; i++) { + d[i] = OrbisFloatToS16(s[i]); + } + } +#elif + static void ConvertF32ToS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + for (u32 i = 0; i < num_samples; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#endif + +#ifdef HAS_SSE2 + static void ConvertF32ToS16_8CH_SIMD(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(32768.0f); + const __m128 min_val = _mm_set1_ps(-32768.0f); + const __m128 max_val = _mm_set1_ps(32767.0f); + + const u32 num_samples = frames << 3; + u32 i = 0; + + // Process 8 samples at a time (1 frame of 8CH audio) + for (; i + 8 <= num_samples; i += 8) { + __m128 f1 = _mm_loadu_ps(&s[i]); + __m128 f2 = _mm_loadu_ps(&s[i + 4]); + + f1 = _mm_mul_ps(f1, scale); + f2 = _mm_mul_ps(f2, scale); + f1 = _mm_max_ps(_mm_min_ps(f1, max_val), min_val); + f2 = _mm_max_ps(_mm_min_ps(f2, max_val), min_val); + + __m128i i1 = _mm_cvtps_epi32(f1); + __m128i i2 = _mm_cvtps_epi32(f2); + __m128i packed = _mm_packs_epi32(i1, i2); + + _mm_storeu_si128(reinterpret_cast<__m128i*>(&d[i]), packed); + } + + for (; i < num_samples; i++) { + d[i] = OrbisFloatToS16(s[i]); + } + } +#elif + static void ConvertF32ToS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + for (u32 i = 0; i < num_samples; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#endif + static void ConvertF32ToS16Std8CH(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 offset = i << 3; + + d[offset + FL] = OrbisFloatToS16(s[offset + FL]); + d[offset + FR] = OrbisFloatToS16(s[offset + FR]); + d[offset + FC] = OrbisFloatToS16(s[offset + FC]); + d[offset + LF] = OrbisFloatToS16(s[offset + LF]); + d[offset + SL] = OrbisFloatToS16(s[offset + STD_SL]); + d[offset + SR] = OrbisFloatToS16(s[offset + STD_SR]); + d[offset + BL] = OrbisFloatToS16(s[offset + STD_BL]); + d[offset + BR] = OrbisFloatToS16(s[offset + STD_BR]); + } + } + + // Audio format parameters + const u32 frame_size; + const u32 guest_buffer_size; + const u32 buffer_frames; + const u32 sample_rate; + const u32 num_channels; + const bool is_float; + const bool is_std; + const std::array channel_layout; + + alignas(64) u64 period_us{0}; + alignas(64) std::atomic last_output_time{0}; + u64 next_output_time{0}; + u64 last_volume_check_time{0}; + u32 output_count{0}; + + // OpenAL objects + OpenALDevice* device_context{nullptr}; + ALuint source{0}; + std::vector buffers; + std::vector available_buffers; + ALenum format{AL_FORMAT_STEREO16}; + + // Buffer management + u32 buffer_size_bytes{0}; + std::vector al_buffer_s16; // For S16 formats + std::vector al_buffer_float; // For float formats + + // Extension support + bool has_float_ext{false}; + bool use_native_float{false}; + + // Converter function pointer + ConverterFunc convert{nullptr}; + + // Volume management + alignas(64) std::atomic current_gain{1.0f}; + + std::string device_name; + bool device_registered; +}; + +std::unique_ptr OpenALAudioOut::Open(PortOut& port) { + return std::make_unique(port); +} + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio/openal_manager.h b/src/core/libraries/audio/openal_manager.h new file mode 100644 index 000000000..4a6ef7920 --- /dev/null +++ b/src/core/libraries/audio/openal_manager.h @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Libraries::AudioOut { + +struct DeviceContext { + ALCdevice* device{nullptr}; + ALCcontext* context{nullptr}; + std::string device_name; + int port_count{0}; + + bool IsValid() const { + return device != nullptr && context != nullptr; + } + + void Cleanup() { + if (context) { + alcDestroyContext(context); + context = nullptr; + } + if (device) { + alcCloseDevice(device); + device = nullptr; + } + port_count = 0; + } +}; + +class OpenALDevice { +public: + static OpenALDevice& GetInstance() { + static OpenALDevice instance; + return instance; + } + + // Register a port that uses this device + bool RegisterPort(const std::string& device_name) { + std::lock_guard lock(mutex); + + // Handle "Default Device" alias + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + // Find or create device context for this device name + auto it = devices.find(actual_device_name); + if (it != devices.end()) { + // Device exists, increment count + it->second.port_count++; + LOG_INFO(Lib_AudioOut, "Reusing OpenAL device '{}', port count: {}", actual_device_name, + it->second.port_count); + return true; + } + + // Create new device + DeviceContext ctx; + if (!InitializeDevice(ctx, actual_device_name)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL device '{}'", actual_device_name); + return false; + } + + ctx.port_count = 1; + devices[actual_device_name] = ctx; + + LOG_INFO(Lib_AudioOut, "Created new OpenAL device '{}'", actual_device_name); + return true; + } + + // Unregister a port + void UnregisterPort(const std::string& device_name) { + std::lock_guard lock(mutex); + + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + auto it = devices.find(actual_device_name); + if (it != devices.end()) { + it->second.port_count--; + LOG_INFO(Lib_AudioOut, "Port unregistered from '{}', remaining ports: {}", + actual_device_name, it->second.port_count); + + if (it->second.port_count <= 0) { + LOG_INFO(Lib_AudioOut, "Cleaning up OpenAL device '{}'", actual_device_name); + it->second.Cleanup(); + devices.erase(it); + } + } + } + + bool MakeCurrent(const std::string& device_name) { + std::lock_guard lock(mutex); + + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + auto it = devices.find(actual_device_name); + if (it == devices.end() || !it->second.IsValid()) { + return false; + } + + // Store current device for this thread (simplified - in practice you might want + // thread-local storage) + current_context = it->second.context; + return alcMakeContextCurrent(it->second.context); + } + + void ReleaseContext() { + std::lock_guard lock(mutex); + alcMakeContextCurrent(nullptr); + current_context = nullptr; + } + + // Get the default device name + static std::string GetDefaultDeviceName() { + const ALCchar* default_device = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); + return default_device ? default_device : "Default Device"; + } + + // Check if device enumeration is supported + static bool IsDeviceEnumerationSupported() { + return alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT") || + alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"); + } + + // Get list of available devices + static std::vector GetAvailableDevices() { + std::vector devices_list; + + if (!alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) + return devices_list; + + const ALCchar* devices = nullptr; + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { + devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + } else { + devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); + } + + if (!devices) + return devices_list; + + const ALCchar* ptr = devices; + while (*ptr != '\0') { + devices_list.emplace_back(ptr); + ptr += std::strlen(ptr) + 1; + } + + return devices_list; + } + +private: + OpenALDevice() = default; + ~OpenALDevice() { + std::lock_guard lock(mutex); + for (auto& [name, ctx] : devices) { + ctx.Cleanup(); + } + devices.clear(); + } + + OpenALDevice(const OpenALDevice&) = delete; + OpenALDevice& operator=(const OpenALDevice&) = delete; + + bool InitializeDevice(DeviceContext& ctx, const std::string& device_name) { + // Handle disabled audio + if (device_name == "None") { + return false; + } + + // Open the requested device + if (device_name.empty() || device_name == "Default Device") { + ctx.device = alcOpenDevice(nullptr); + } else { + ctx.device = alcOpenDevice(device_name.c_str()); + if (!ctx.device) { + LOG_WARNING(Lib_AudioOut, "Device '{}' not found, falling back to default", + device_name); + ctx.device = alcOpenDevice(nullptr); + } + } + + if (!ctx.device) { + LOG_ERROR(Lib_AudioOut, "Failed to open OpenAL device"); + return false; + } + + // Create context + ctx.context = alcCreateContext(ctx.device, nullptr); + if (!ctx.context) { + LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context"); + alcCloseDevice(ctx.device); + ctx.device = nullptr; + return false; + } + + // Get actual device name + const ALCchar* actual_name = nullptr; + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { + actual_name = alcGetString(ctx.device, ALC_ALL_DEVICES_SPECIFIER); + } else { + actual_name = alcGetString(ctx.device, ALC_DEVICE_SPECIFIER); + } + ctx.device_name = actual_name ? actual_name : "Unknown"; + + LOG_INFO(Lib_AudioOut, "OpenAL device initialized: '{}'", ctx.device_name); + return true; + } + + std::unordered_map devices; + mutable std::mutex mutex; + ALCcontext* current_context{nullptr}; // For thread-local tracking +}; + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp new file mode 100644 index 000000000..f5085b333 --- /dev/null +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -0,0 +1,997 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_error.h" +#include "core/libraries/audio3d/audio3d_error.h" +#include "core/libraries/audio3d/audio3d_openal.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" + +namespace Libraries::Audio3dOpenAL { + +static constexpr u32 AUDIO3D_SAMPLE_RATE = 48000; + +static constexpr AudioOut::OrbisAudioOutParamFormat AUDIO3D_OUTPUT_FORMAT = + AudioOut::OrbisAudioOutParamFormat::S16Stereo; +static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2; + +static std::unique_ptr state; + +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()); + } + } + + return AudioOut::sceAudioOutClose(handle); +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( + const OrbisAudio3dPortId port_id, const Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, const s32 index, const u32 len, const u32 freq, + const AudioOut::OrbisAudioOutParamExtendedInformation param) { + LOG_INFO(Lib_Audio3d, + "called, port_id = {}, user_id = {}, type = {}, index = {}, len = {}, freq = {}", + port_id, user_id, type, index, len, freq); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + std::scoped_lock lock{state->ports[port_id].mutex}; + if (len != state->ports[port_id].parameters.granularity) { + LOG_ERROR(Lib_Audio3d, "len != state->ports[port_id].parameters.granularity"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const s32 handle = sceAudioOutOpen(user_id, static_cast(type), + index, len, freq, param); + if (handle < 0) { + return handle; + } + + // Track this handle in the port so sceAudio3dPortFlush can use it for sync. + state->ports[port_id].audioout_handles.push_back(handle); + return handle; +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { + LOG_DEBUG(Lib_Audio3d, "called, handle = {}, ptr = {}", handle, ptr); + + if (!ptr) { + LOG_ERROR(Lib_Audio3d, "!ptr"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (handle < 0 || (handle & 0xFFFF) > 25) { + LOG_ERROR(Lib_Audio3d, "handle < 0 || (handle & 0xFFFF) > 25"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + return AudioOut::sceAudioOutOutput(handle, ptr); +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* param, + const u32 num) { + LOG_DEBUG(Lib_Audio3d, "called, param = {}, num = {}", static_cast(param), num); + + if (!param || !num) { + LOG_ERROR(Lib_Audio3d, "!param || !num"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + return AudioOut::sceAudioOutOutputs(param, num); +} + +static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm& pcm, + const u32 num_channels, const u32 granularity) { + if (!pcm.sample_buffer || !pcm.num_samples) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + 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); + + queue.emplace_back(AudioData{ + .sample_buffer = copy, + .num_samples = granularity, + .num_channels = num_channels, + .format = pcm.format, + }); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dBedWrite(const OrbisAudio3dPortId port_id, const u32 num_channels, + const OrbisAudio3dFormat format, void* buffer, + const u32 num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, + OrbisAudio3dOutputRoute::ORBIS_AUDIO3D_OUTPUT_BOTH, false); +} + +s32 PS4_SYSV_ABI sceAudio3dBedWrite2(const OrbisAudio3dPortId port_id, const u32 num_channels, + const OrbisAudio3dFormat format, void* buffer, + const u32 num_samples, + const OrbisAudio3dOutputRoute output_route, + const bool restricted) { + LOG_DEBUG( + Lib_Audio3d, + "called, port_id = {}, num_channels = {}, format = {}, num_samples = {}, output_route " + "= {}, restricted = {}", + port_id, num_channels, magic_enum::enum_name(format), num_samples, + magic_enum::enum_name(output_route), restricted); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (output_route > OrbisAudio3dOutputRoute::ORBIS_AUDIO3D_OUTPUT_BOTH) { + LOG_ERROR(Lib_Audio3d, "output_route > ORBIS_AUDIO3D_OUTPUT_BOTH"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (format > OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT) { + LOG_ERROR(Lib_Audio3d, "format > ORBIS_AUDIO3D_FORMAT_FLOAT"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (num_channels != 2 && num_channels != 6 && num_channels != 8) { + LOG_ERROR(Lib_Audio3d, "num_channels must be 2, 6, or 8"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (!buffer || !num_samples) { + LOG_ERROR(Lib_Audio3d, "!buffer || !num_samples"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT) { + if ((reinterpret_cast(buffer) & 3) != 0) { + LOG_ERROR(Lib_Audio3d, "buffer & 3 != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + } else if (format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + if ((reinterpret_cast(buffer) & 1) != 0) { + LOG_ERROR(Lib_Audio3d, "buffer & 1 != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + } + + std::scoped_lock lock{state->ports[port_id].mutex}; + return ConvertAndEnqueue(state->ports[port_id].bed_queue, + OrbisAudio3dPcm{ + .format = format, + .sample_buffer = buffer, + .num_samples = num_samples, + }, + num_channels, state->ports[port_id].parameters.granularity); +} + +s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params) { + LOG_DEBUG(Lib_Audio3d, "called"); + if (params) { + auto default_params = OrbisAudio3dOpenParameters{ + .size_this = 0x20, + .granularity = 0x100, + .rate = OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000, + .max_objects = 512, + .queue_depth = 2, + .buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH, + }; + memcpy(params, &default_params, 0x20); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dInitialize(const s64 reserved) { + LOG_INFO(Lib_Audio3d, "called, reserved = {}", reserved); + + if (reserved != 0) { + LOG_ERROR(Lib_Audio3d, "reserved != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (state) { + LOG_ERROR(Lib_Audio3d, "already initialized"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + state = std::make_unique(); + + if (const auto init_ret = AudioOut::sceAudioOutInit(); + init_ret < 0 && init_ret != ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT) { + return init_ret; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectReserve(const OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId* object_id) { + LOG_INFO(Lib_Audio3d, "called, port_id = {}, object_id = {}", port_id, + static_cast(object_id)); + + if (!object_id) { + LOG_ERROR(Lib_Audio3d, "!object_id"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + *object_id = ORBIS_AUDIO3D_OBJECT_INVALID; + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + // Enforce the max_objects limit set at PortOpen time. + if (port.objects.size() >= port.parameters.max_objects) { + LOG_ERROR(Lib_Audio3d, "port has no available objects (max_objects = {})", + port.parameters.max_objects); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + // Counter lives in the Port so it resets when the port is closed and reopened. + do { + ++port.next_object_id; + } while (port.next_object_id == 0 || + port.next_object_id == static_cast(ORBIS_AUDIO3D_OBJECT_INVALID) || + port.objects.contains(port.next_object_id)); + + *object_id = port.next_object_id; + port.objects.emplace(*object_id, ObjectState{}); + LOG_INFO(Lib_Audio3d, "reserved object_id = {}", *object_id); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(const OrbisAudio3dPortId port_id, + const OrbisAudio3dObjectId object_id, + const OrbisAudio3dAttributeId attribute_id, + const void* attribute, const u64 attribute_size) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, object_id = {}, attribute_id = {:#x}, size = {}", + port_id, object_id, static_cast(attribute_id), attribute_size); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + if (!port.objects.contains(object_id)) { + LOG_DEBUG(Lib_Audio3d, "object_id {} not reserved (race with Unreserve?), no-op", + object_id); + return ORBIS_OK; + } + + if (!attribute_size && + attribute_id != OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + LOG_ERROR(Lib_Audio3d, "!attribute_size for non-reset attribute"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + 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(); + 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); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, + const u64 num_attributes, + const OrbisAudio3dAttribute* attribute_array) { + LOG_DEBUG(Lib_Audio3d, + "called, port_id = {}, object_id = {}, num_attributes = {}, attribute_array = {}", + port_id, object_id, num_attributes, fmt::ptr(attribute_array)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!num_attributes || !attribute_array) { + LOG_ERROR(Lib_Audio3d, "!num_attributes || !attribute_array"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + if (!port.objects.contains(object_id)) { + LOG_DEBUG(Lib_Audio3d, "object_id {} not reserved", object_id); + return ORBIS_OK; + } + + 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) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + obj.pcm_queue.clear(); + obj.persistent_attributes.clear(); + LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); + break; // Only one reset is needed even if listed multiple times. + } + } + + // Second pass: apply all other attributes. + for (u64 i = 0; i < num_attributes; i++) { + const auto& attr = attribute_array[i]; + + switch (attr.attribute_id) { + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE: + break; // Already applied in first pass above. + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PCM: { + if (attr.value_size < sizeof(OrbisAudio3dPcm)) { + LOG_ERROR(Lib_Audio3d, "PCM attribute value_size too small"); + continue; + } + const auto pcm = static_cast(attr.value); + // Object audio is always mono (1 channel). + if (const auto ret = + ConvertAndEnqueue(obj.pcm_queue, *pcm, 1, port.parameters.granularity); + ret != ORBIS_OK) { + return ret; + } + 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( + src, src + attr.value_size); + } + LOG_DEBUG(Lib_Audio3d, "Stored attribute {:#x} for object {}", + static_cast(attr.attribute_id), object_id); + break; + } + } + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(const OrbisAudio3dPortId port_id, + const OrbisAudio3dObjectId object_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, object_id = {}", port_id, object_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + if (!port.objects.contains(object_id)) { + LOG_ERROR(Lib_Audio3d, "object_id not reserved"); + 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); + } + + port.objects.erase(object_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + + 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", + port.parameters.queue_depth); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + const u32 granularity = port.parameters.granularity; + const u32 out_samples = granularity * AUDIO3D_OUTPUT_NUM_CHANNELS; + + // ---- FLOAT MIX BUFFER ---- + float* mix_float = static_cast(std::calloc(out_samples, sizeof(float))); + if (!mix_float) + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + + auto mix_in = [&](std::deque& queue, const float gain) { + 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(); + std::free(data.sample_buffer); + return; + } + + AudioData data = queue.front(); + queue.pop_front(); + + const u32 frames = std::min(granularity, data.num_samples); + const u32 channels = data.num_channels; + + if (data.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + const s16* src = reinterpret_cast(data.sample_buffer); + + for (u32 i = 0; i < frames; i++) { + float left = 0.0f; + float right = 0.0f; + + if (channels == 1) { + float v = src[i] / 32768.0f; + left = v; + right = v; + } else { + left = src[i * channels + 0] / 32768.0f; + right = src[i * channels + 1] / 32768.0f; + } + + mix_float[i * 2 + 0] += left * gain; + mix_float[i * 2 + 1] += right * gain; + } + } else { // FLOAT input + const float* src = reinterpret_cast(data.sample_buffer); + + for (u32 i = 0; i < frames; i++) { + float left = 0.0f; + float right = 0.0f; + + if (channels == 1) { + left = src[i]; + right = src[i]; + } else { + left = src[i * channels + 0]; + right = src[i * channels + 1]; + } + + mix_float[i * 2 + 0] += left * gain; + mix_float[i * 2 + 1] += right * gain; + } + } + + std::free(data.sample_buffer); + }; + + // Bed is mixed at full gain (1.0). + mix_in(port.bed_queue, 1.0f); + + // Mix all object PCM queues, applying each object's GAIN persistent attribute. + for (auto& [obj_id, obj] : port.objects) { + float gain = 0.0f; + const auto gain_key = + static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN); + if (obj.persistent_attributes.contains(gain_key)) { + const auto& blob = obj.persistent_attributes.at(gain_key); + if (blob.size() >= sizeof(float)) { + std::memcpy(&gain, blob.data(), sizeof(float)); + } + } + mix_in(obj.pcm_queue, gain); + } + + s16* mix_s16 = static_cast(std::malloc(out_samples * sizeof(s16))); + if (!mix_s16) { + std::free(mix_float); + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + } + + for (u32 i = 0; i < out_samples; i++) { + float v = std::clamp(mix_float[i], -1.0f, 1.0f); + mix_s16[i] = static_cast(v * 32767.0f); + } + + 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}); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortClose(const OrbisAudio3dPortId port_id) { + LOG_INFO(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + { + std::scoped_lock lock{port.mutex}; + + if (port.audio_out_handle >= 0) { + AudioOut::sceAudioOutClose(port.audio_out_handle); + port.audio_out_handle = -1; + } + + for (const s32 handle : port.audioout_handles) { + AudioOut::sceAudioOutClose(handle); + } + port.audioout_handles.clear(); + + for (auto& data : port.mixed_queue) { + std::free(data.sample_buffer); + } + + for (auto& data : port.bed_queue) { + std::free(data.sample_buffer); + } + + for (auto& [obj_id, obj] : port.objects) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + } + } + + state->ports.erase(port_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortCreate() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortDestroy() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + 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 (ret < 0) { + return ret; + } + } + return ORBIS_OK; + } + + if (port.mixed_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(); })) { + const s32 ret = sceAudio3dPortAdvance(port_id); + if (ret != ORBIS_OK && ret != ORBIS_AUDIO3D_ERROR_NOT_READY) { + return ret; + } + } + } + + if (port.mixed_queue.empty()) { + return ORBIS_OK; + } + + if (port.audio_out_handle < 0) { + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; + ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); + port.audio_out_handle = + AudioOut::sceAudioOutOpen(0xFF, AudioOut::OrbisAudioOutPort::Audio3d, 0, + port.parameters.granularity, AUDIO3D_SAMPLE_RATE, ext_info); + if (port.audio_out_handle < 0) { + return port.audio_out_handle; + } + } + + // Drain all queued mixed frames, blocking on each until consumed. + while (!port.mixed_queue.empty()) { + AudioData frame = port.mixed_queue.front(); + port.mixed_queue.pop_front(); + const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); + if (ret < 0) { + return ret; + } + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortFreeState() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetList() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetParameters() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(const OrbisAudio3dPortId port_id, u32* queue_level, + u32* queue_available) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, queue_level = {}, queue_available = {}", port_id, + static_cast(queue_level), static_cast(queue_available)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!queue_level && !queue_available) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + const size_t size = port.mixed_queue.size(); + + if (queue_level) { + *queue_level = static_cast(size); + } + + if (queue_available) { + const u32 depth = port.parameters.queue_depth; + *queue_available = (size < depth) ? static_cast(depth - size) : 0u; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetState() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortOpen(const Libraries::UserService::OrbisUserServiceUserId user_id, + const OrbisAudio3dOpenParameters* parameters, + OrbisAudio3dPortId* port_id) { + LOG_INFO(Lib_Audio3d, "called, user_id = {}, parameters = {}, id = {}", user_id, + static_cast(parameters), static_cast(port_id)); + + if (!state) { + LOG_ERROR(Lib_Audio3d, "!initialized"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + if (!parameters || !port_id) { + LOG_ERROR(Lib_Audio3d, "!parameters || !id"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const int id = static_cast(state->ports.size()) + 1; + + if (id > 3) { + LOG_ERROR(Lib_Audio3d, "id > 3"); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + *port_id = id; + auto& port = state->ports[id]; + std::memcpy( + &port.parameters, parameters, + std::min(parameters->size_this, static_cast(sizeof(OrbisAudio3dOpenParameters)))); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, + const OrbisAudio3dBlocking blocking) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, blocking = {}", port_id, + magic_enum::enum_name(blocking)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + + if (port.parameters.buffer_mode != + OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH) { + LOG_ERROR(Lib_Audio3d, "port doesn't have push capability"); + return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; + } + + const u32 depth = port.parameters.queue_depth; + + if (port.audio_out_handle < 0) { + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; + ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); + + port.audio_out_handle = + AudioOut::sceAudioOutOpen(0xFF, AudioOut::OrbisAudioOutPort::Audio3d, 0, + port.parameters.granularity, AUDIO3D_SAMPLE_RATE, ext_info); + + if (port.audio_out_handle < 0) + return port.audio_out_handle; + } + + // Function that submits exactly one frame (if available). + auto submit_one_frame = [&](bool& submitted) -> s32 { + AudioData frame; + { + std::scoped_lock lock{port.mutex}; + + if (port.mixed_queue.empty()) { + submitted = false; + return ORBIS_OK; + } + + 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); + + if (ret < 0) + return ret; + + submitted = true; + return ORBIS_OK; + }; + + // If not full, return immediately. + { + std::scoped_lock lock{port.mutex}; + if (port.mixed_queue.size() < depth) { + return ORBIS_OK; + } + } + + // Submit one frame to free space. + bool submitted = false; + s32 ret = submit_one_frame(submitted); + if (ret < 0) + return ret; + + if (!submitted) + return ORBIS_OK; + + // ASYNC: free exactly one slot and return. + if (blocking == OrbisAudio3dBlocking::ORBIS_AUDIO3D_BLOCKING_ASYNC) { + return ORBIS_OK; + } + + // SYNC: ensure at least one slot is free (drain until size < depth). + while (true) { + { + std::scoped_lock lock{port.mutex}; + if (port.mixed_queue.size() < depth) + break; + } + + bool drained = false; + ret = submit_one_frame(drained); + if (ret < 0) + return ret; + + if (!drained) + break; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +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; + } + + if (!attribute) { + LOG_ERROR(Lib_Audio3d, "!attribute"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + // TODO + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dStrError() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dTerminate() { + LOG_INFO(Lib_Audio3d, "called"); + + if (!state) { + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + std::vector port_ids; + for (const auto& [id, _] : state->ports) { + port_ids.push_back(id); + } + for (const auto id : port_ids) { + sceAudio3dPortClose(id); + } + + state.reset(); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("pZlOm1aF3aA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutClose); + LIB_FUNCTION("ucEsi62soTo", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOpen); + LIB_FUNCTION("7NYEzJ9SJbM", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOutput); + LIB_FUNCTION("HbxYY27lK6E", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOutputs); + LIB_FUNCTION("9tEwE0GV0qo", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dBedWrite); + LIB_FUNCTION("xH4Q9UILL3o", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dBedWrite2); + LIB_FUNCTION("lvWMW6vEqFU", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dCreateSpeakerArray); + LIB_FUNCTION("8hm6YdoQgwg", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dDeleteSpeakerArray); + LIB_FUNCTION("Im+jOoa5WAI", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetDefaultOpenParameters); + LIB_FUNCTION("kEqqyDkmgdI", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMemorySize); + LIB_FUNCTION("-R1DukFq7Dk", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMixCoefficients); + LIB_FUNCTION("-Re+pCWvwjQ", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMixCoefficients2); + LIB_FUNCTION("UmCvjSmuZIw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dInitialize); + LIB_FUNCTION("jO2tec4dJ2M", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectReserve); + LIB_FUNCTION("V1FBFpNIAzk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectSetAttribute); + LIB_FUNCTION("4uyHN9q4ZeU", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectSetAttributes); + LIB_FUNCTION("1HXxo-+1qCw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectUnreserve); + LIB_FUNCTION("lw0qrdSjZt8", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortAdvance); + LIB_FUNCTION("OyVqOeVNtSk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortClose); + LIB_FUNCTION("UHFOgVNz0kk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortCreate); + LIB_FUNCTION("Mw9mRQtWepY", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortDestroy); + LIB_FUNCTION("ZOGrxWLgQzE", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortFlush); + LIB_FUNCTION("uJ0VhGcxCTQ", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortFreeState); + LIB_FUNCTION("9ZA23Ia46Po", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dPortGetAttributesSupported); + LIB_FUNCTION("SEggctIeTcI", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetList); + LIB_FUNCTION("flPcUaXVXcw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetParameters); + LIB_FUNCTION("YaaDbDwKpFM", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetQueueLevel); + LIB_FUNCTION("CKHlRW2E9dA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetState); + LIB_FUNCTION("iRX6GJs9tvE", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetStatus); + LIB_FUNCTION("XeDDK0xJWQA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortOpen); + LIB_FUNCTION("VEVhZ9qd4ZY", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortPush); + LIB_FUNCTION("-pzYDZozm+M", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortQueryDebug); + LIB_FUNCTION("Yq9bfUQ0uJg", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortSetAttribute); + LIB_FUNCTION("QfNXBrKZeI0", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dReportRegisterHandler); + LIB_FUNCTION("psv2gbihC1A", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dReportUnregisterHandler); + LIB_FUNCTION("yEYXcbAGK14", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dSetGpuRenderer); + LIB_FUNCTION("Aacl5qkRU6U", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dStrError); + LIB_FUNCTION("WW1TS2iz5yc", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dTerminate); +} + +} // namespace Libraries::Audio3dOpenAL diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h new file mode 100644 index 000000000..75be72066 --- /dev/null +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "common/types.h" +#include "core/libraries/audio/audioout.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Audio3dOpenAL { + +constexpr int ORBIS_AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; + +enum class OrbisAudio3dRate : u32 { + ORBIS_AUDIO3D_RATE_48000 = 0, +}; + +enum class OrbisAudio3dBufferMode : u32 { + ORBIS_AUDIO3D_BUFFER_NO_ADVANCE = 0, + ORBIS_AUDIO3D_BUFFER_ADVANCE_NO_PUSH = 1, + ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2, +}; + +struct OrbisAudio3dOpenParameters { + u64 size_this; + u32 granularity; + OrbisAudio3dRate rate; + u32 max_objects; + u32 queue_depth; + OrbisAudio3dBufferMode buffer_mode; + int : 32; + u32 num_beds; +}; + +enum class OrbisAudio3dFormat : u32 { + ORBIS_AUDIO3D_FORMAT_S16 = 0, + ORBIS_AUDIO3D_FORMAT_FLOAT = 1, +}; + +enum class OrbisAudio3dOutputRoute : u32 { + ORBIS_AUDIO3D_OUTPUT_BOTH = 0, + ORBIS_AUDIO3D_OUTPUT_HMU_ONLY = 1, + ORBIS_AUDIO3D_OUTPUT_TV_ONLY = 2, +}; + +enum class OrbisAudio3dBlocking : u32 { + ORBIS_AUDIO3D_BLOCKING_ASYNC = 0, + ORBIS_AUDIO3D_BLOCKING_SYNC = 1, +}; + +struct OrbisAudio3dPcm { + OrbisAudio3dFormat format; + void* sample_buffer; + u32 num_samples; +}; + +enum class OrbisAudio3dAttributeId : u32 { + ORBIS_AUDIO3D_ATTRIBUTE_PCM = 1, + ORBIS_AUDIO3D_ATTRIBUTE_POSITION = 2, + ORBIS_AUDIO3D_ATTRIBUTE_GAIN = 3, + ORBIS_AUDIO3D_ATTRIBUTE_SPREAD = 4, + ORBIS_AUDIO3D_ATTRIBUTE_PRIORITY = 5, + ORBIS_AUDIO3D_ATTRIBUTE_PASSTHROUGH = 6, + ORBIS_AUDIO3D_ATTRIBUTE_AMBISONICS = 7, + ORBIS_AUDIO3D_ATTRIBUTE_APPLICATION_SPECIFIC = 8, + ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE = 9, + ORBIS_AUDIO3D_ATTRIBUTE_RESTRICTED = 10, + ORBIS_AUDIO3D_ATTRIBUTE_OUTPUT_ROUTE = 11, +}; + +using OrbisAudio3dPortId = u32; +using OrbisAudio3dObjectId = u32; +using OrbisAudio3dAmbisonics = u32; + +struct OrbisAudio3dAttribute { + OrbisAudio3dAttributeId attribute_id; + int : 32; + void* value; + u64 value_size; +}; + +struct AudioData { + u8* sample_buffer; + u32 num_samples; + u32 num_channels{1}; + OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; +}; + +struct ObjectState { + std::deque pcm_queue; + std::unordered_map> persistent_attributes; +}; + +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; + // Reserved objects and their state. + std::unordered_map objects; + // Increasing counter for generating unique object IDs within this port. + OrbisAudio3dObjectId next_object_id{0}; + // Bed audio queue. + std::deque bed_queue; + // Mixed stereo frames ready to be consumed by sceAudio3dPortPush. + std::deque mixed_queue; +}; + +struct Audio3dState { + std::unordered_map ports; +}; + +s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, + Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, s32 index, u32 len, u32 freq, + AudioOut::OrbisAudioOutParamExtendedInformation param); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(s32 handle, void* ptr); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* param, u32 num); +s32 PS4_SYSV_ABI sceAudio3dBedWrite(OrbisAudio3dPortId port_id, u32 num_channels, + OrbisAudio3dFormat format, void* buffer, u32 num_samples); +s32 PS4_SYSV_ABI sceAudio3dBedWrite2(OrbisAudio3dPortId port_id, u32 num_channels, + OrbisAudio3dFormat format, void* buffer, u32 num_samples, + OrbisAudio3dOutputRoute output_route, bool restricted); +s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray(); +s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray(); +s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize(); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients(); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2(); +s32 PS4_SYSV_ABI sceAudio3dInitialize(s64 reserved); +s32 PS4_SYSV_ABI sceAudio3dObjectReserve(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId* object_id); +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, + OrbisAudio3dAttributeId attribute_id, + const void* attribute, u64 attribute_size); +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, u64 num_attributes, + const OrbisAudio3dAttribute* attribute_array); +s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id); +s32 PS4_SYSV_ABI sceAudio3dPortAdvance(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortClose(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortCreate(); +s32 PS4_SYSV_ABI sceAudio3dPortDestroy(); +s32 PS4_SYSV_ABI sceAudio3dPortFlush(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortFreeState(); +s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(); +s32 PS4_SYSV_ABI sceAudio3dPortGetList(); +s32 PS4_SYSV_ABI sceAudio3dPortGetParameters(); +s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId port_id, u32* queue_level, + u32* queue_available); +s32 PS4_SYSV_ABI sceAudio3dPortGetState(); +s32 PS4_SYSV_ABI sceAudio3dPortGetStatus(); +s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUserId user_id, + const OrbisAudio3dOpenParameters* parameters, + OrbisAudio3dPortId* port_id); +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); +s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler(); +s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler(); +s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer(); +s32 PS4_SYSV_ABI sceAudio3dStrError(); +s32 PS4_SYSV_ABI sceAudio3dTerminate(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Audio3dOpenAL diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index a09e61d74..f273332d0 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -6,6 +6,7 @@ #include "core/libraries/audio/audioin.h" #include "core/libraries/audio/audioout.h" #include "core/libraries/audio3d/audio3d.h" +#include "core/libraries/audio3d/audio3d_openal.h" #include "core/libraries/avplayer/avplayer.h" #include "core/libraries/camera/camera.h" #include "core/libraries/companion/companion_httpd.h" @@ -126,7 +127,8 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::AvPlayer::RegisterLib(sym); Libraries::Videodec::RegisterLib(sym); Libraries::Videodec2::RegisterLib(sym); - Libraries::Audio3d::RegisterLib(sym); + // Libraries::Audio3d::RegisterLib(sym); + Libraries::Audio3dOpenAL::RegisterLib(sym); Libraries::Ime::RegisterLib(sym); Libraries::GameLiveStreaming::RegisterLib(sym); Libraries::SharePlay::RegisterLib(sym);