// SPDX-FileCopyrightText: Copyright 2025-2026 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(u32 granularity, u32 rate, s64 reserved, OrbisAudio3dPortId* port_id) { LOG_INFO(Lib_Audio3d, "called, granularity = {}, rate = {}, reserved = {}, port_id = {}", granularity, rate, reserved, static_cast(port_id)); if (!port_id || reserved) { LOG_INFO(Lib_Audio3d, "!port_id || reserved"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } OrbisAudio3dOpenParameters local_params{}; local_params.size_this = 0x10; local_params.granularity = granularity; local_params.rate = static_cast(rate); return sceAudio3dPortOpen(static_cast(0xFF), &local_params, port_id); } 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(OrbisAudio3dPortId port_id, OrbisAudio3dAttributeId* capabilities, u32* num_capabilities) { LOG_DEBUG(Lib_Audio3d, "called"); if (!num_capabilities) { return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (!state->ports.contains(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. 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; } 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); } else { // If capabilities is null, then just report the number of supported capabilities. *num_capabilities = 3; } 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 = {}, port_id = {}", user_id, static_cast(parameters), static_cast(port_id)); if (user_id != 0xFF || !parameters || !port_id) { LOG_ERROR(Lib_Audio3d, "user_id != 0xFF || !parameters || !port_id"); if (port_id) *port_id = ORBIS_AUDIO3D_PORT_INVALID; return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } *port_id = ORBIS_AUDIO3D_PORT_INVALID; if (!state) { LOG_ERROR(Lib_Audio3d, "!initialized"); return ORBIS_AUDIO3D_ERROR_NOT_READY; } OrbisAudio3dOpenParameters effective{ .size_this = 0x28, .granularity = parameters->granularity, .rate = parameters->rate, .max_objects = 512, .queue_depth = 2, .buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_NO_ADVANCE, ._pad = 0, .num_beds = 2, }; switch (parameters->size_this & ~0x7ull) { case 0x10: break; case 0x18: effective.max_objects = parameters->max_objects; effective.queue_depth = parameters->queue_depth; effective.buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_NO_PUSH; break; case 0x20: effective.max_objects = parameters->max_objects; effective.queue_depth = parameters->queue_depth; effective.buffer_mode = parameters->buffer_mode; break; case 0x28: effective.max_objects = parameters->max_objects; effective.queue_depth = parameters->queue_depth; effective.buffer_mode = parameters->buffer_mode; effective.num_beds = parameters->num_beds; break; default: LOG_ERROR(Lib_Audio3d, "invalid size_this"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.rate != OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000) { LOG_ERROR(Lib_Audio3d, "unsupported rate"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.granularity < 0x100) { LOG_ERROR(Lib_Audio3d, "granularity < 0x100"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if ((effective.granularity & 0xFF) != 0) { LOG_ERROR(Lib_Audio3d, "granularity not aligned to 0x100"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.max_objects == 0) { LOG_ERROR(Lib_Audio3d, "max_objects == 0"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.queue_depth == 0) { LOG_ERROR(Lib_Audio3d, "queue_depth == 0"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.granularity == 0x100 && effective.queue_depth > 0x40) { LOG_ERROR(Lib_Audio3d, "queue_depth too large for 0x100 granularity"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.granularity == 0x200 && effective.queue_depth > 0x1F) { LOG_ERROR(Lib_Audio3d, "queue_depth too large for 0x200 granularity"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.granularity == 0x300 && effective.queue_depth > 0x14) { LOG_ERROR(Lib_Audio3d, "queue_depth too large for 0x300 granularity"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.queue_depth > 0xF && effective.granularity > 0x3FF) { LOG_ERROR(Lib_Audio3d, "queue_depth invalid for large granularity"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (static_cast(effective.buffer_mode) > 2) { LOG_ERROR(Lib_Audio3d, "invalid buffer_mode"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if ((effective.num_beds & 0xfffffffe) != 2) { LOG_ERROR(Lib_Audio3d, "invalid num_beds"); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (effective.max_objects > 0x200) { LOG_WARNING(Lib_Audio3d, "max_objects {} exceeds limit, clamping to 512", effective.max_objects); effective.max_objects = 0x200; } std::scoped_lock lock{state->ports_mutex}; OrbisAudio3dPortId id = ORBIS_AUDIO3D_PORT_INVALID; for (OrbisAudio3dPortId i = 0; i < MaxPorts; i++) { if (!state->ports.contains(i)) { id = i; break; } } if (id == ORBIS_AUDIO3D_PORT_INVALID) { LOG_ERROR(Lib_Audio3d, "no free ports"); return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; } auto& port = state->ports.try_emplace(id).first->second; port.parameters = effective; *port_id = id; 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