diff --git a/src/core/libraries/audio3d/audio3d.cpp b/src/core/libraries/audio3d/audio3d.cpp index 00576088b..f50ebfdd7 100644 --- a/src/core/libraries/audio3d/audio3d.cpp +++ b/src/core/libraries/audio3d/audio3d.cpp @@ -1,9 +1,12 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include +#include +#include #include +#include +#include #include "common/assert.h" #include "common/logging/log.h" #include "core/libraries/audio/audioout.h" @@ -17,7 +20,6 @@ namespace Libraries::Audio3d { 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; @@ -30,6 +32,280 @@ static bool Audio3dHRTFActive() { return al.IsInitialized() && al.IsHRTFEnabled(); } +// Convert PS4 3D coordinate system to OpenAL +static void ConvertPositionToOpenAL(const float* ps4_pos, float* al_pos) { + al_pos[0] = ps4_pos[0]; // X same + al_pos[1] = ps4_pos[1]; // Y same + al_pos[2] = -ps4_pos[2]; // Z inverted +} + +// Helper function to downmix 8-channel audio to stereo S16 +static std::vector Downmix8ChannelToStereoS16(const OrbisAudio3dPcm& pcm) { + std::vector converted_data; + + const bool is_float = (pcm.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT); + const u32 input_channels = 8; + + // Output will be stereo S16 + const size_t output_size = pcm.num_samples * 2 * sizeof(int16_t); + converted_data.resize(output_size); + + // Initialize with zeros + std::memset(converted_data.data(), 0, output_size); + + if (!pcm.sample_buffer || pcm.num_samples == 0) { + LOG_WARNING(Lib_Audio3d, "Empty audio buffer for downmixing"); + return converted_data; + } + + int16_t* output = reinterpret_cast(converted_data.data()); + + if (is_float) { + const float* input = static_cast(pcm.sample_buffer); + + for (u32 i = 0; i < pcm.num_samples; i++) { + size_t base_idx = i * input_channels; + + float fl = input[base_idx + 0]; + float fr = input[base_idx + 1]; + float fc = input[base_idx + 2]; + float lfe = input[base_idx + 3]; + float bl = input[base_idx + 4]; + float br = input[base_idx + 5]; + float sl = input[base_idx + 6]; + float sr = input[base_idx + 7]; + + // 7.1 to stereo downmix with volume reduction + float left = fl * 0.7f + fc * 0.5f + sl * 0.5f + bl * 0.3f + lfe * 0.1f; + float right = fr * 0.7f + fc * 0.5f + sr * 0.5f + br * 0.3f + lfe * 0.1f; + + // Clamp and convert to int16 + left = std::clamp(left, -1.0f, 1.0f); + right = std::clamp(right, -1.0f, 1.0f); + + output[i * 2 + 0] = static_cast(left * 32767.0f); + output[i * 2 + 1] = static_cast(right * 32767.0f); + } + } else { + const int16_t* input = static_cast(pcm.sample_buffer); + + for (u32 i = 0; i < pcm.num_samples; i++) { + size_t base_idx = i * input_channels; + + // Convert to float for processing + float fl = input[base_idx + 0] / 32768.0f; + float fr = input[base_idx + 1] / 32768.0f; + float fc = input[base_idx + 2] / 32768.0f; + float lfe = input[base_idx + 3] / 32768.0f; + float bl = input[base_idx + 4] / 32768.0f; + float br = input[base_idx + 5] / 32768.0f; + float sl = input[base_idx + 6] / 32768.0f; + float sr = input[base_idx + 7] / 32768.0f; + + // Downmix with volume reduction + float left = fl * 0.7f + fc * 0.5f + sl * 0.5f + bl * 0.3f + lfe * 0.1f; + float right = fr * 0.7f + fc * 0.5f + sr * 0.5f + br * 0.3f + lfe * 0.1f; + + // Clamp and convert back to int16 + left = std::clamp(left, -1.0f, 1.0f); + right = std::clamp(right, -1.0f, 1.0f); + + output[i * 2 + 0] = static_cast(left * 32767.0f); + output[i * 2 + 1] = static_cast(right * 32767.0f); + } + } + + LOG_DEBUG(Lib_Audio3d, "Downmixed 8-channel to stereo S16: {} samples", pcm.num_samples); + return converted_data; +} + +// Convert any input to stereo S16 format +static std::vector ConvertToStereoS16(const OrbisAudio3dPcm& pcm, u32 num_channels) { + std::vector converted_data; + + if (!pcm.sample_buffer || pcm.num_samples == 0) { + LOG_ERROR(Lib_Audio3d, "Empty or invalid PCM data"); + return converted_data; + } + + // We only support S16 format + if (pcm.format != OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + LOG_ERROR(Lib_Audio3d, "Only S16 format supported, got: {}", + magic_enum::enum_name(pcm.format)); + return converted_data; + } + + if (num_channels == 2) { + // Direct copy for stereo S16 + const size_t total_size = pcm.num_samples * 2 * sizeof(int16_t); + converted_data.resize(total_size); + std::memcpy(converted_data.data(), pcm.sample_buffer, total_size); + } else if (num_channels == 8) { + // Downmix 8-channel to stereo S16 + converted_data = Downmix8ChannelToStereoS16(pcm); + } else { + LOG_ERROR(Lib_Audio3d, "Unsupported channel count: {} (only 2 or 8 supported)", + num_channels); + return converted_data; + } + + LOG_DEBUG(Lib_Audio3d, "Converted to stereo S16: {} bytes, {} samples", converted_data.size(), + pcm.num_samples); + + return converted_data; +} + +// Queue audio data (always converted to stereo S16) +static s32 PortQueueAudio(Port& port, const OrbisAudio3dPcm& pcm, const u32 num_channels) { + LOG_DEBUG(Lib_Audio3d, "PortQueueAudio: channels={}, samples={}, format={}", num_channels, + pcm.num_samples, magic_enum::enum_name(pcm.format)); + + if (!pcm.sample_buffer || pcm.num_samples == 0) { + LOG_ERROR(Lib_Audio3d, "Invalid PCM data for queue"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + // Convert to stereo S16 + auto converted_data = ConvertToStereoS16(pcm, num_channels); + + if (converted_data.empty()) { + LOG_ERROR(Lib_Audio3d, "Failed to convert audio to stereo S16"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + // Verify size matches expected stereo S16 + const size_t expected_size = pcm.num_samples * 2 * sizeof(int16_t); + if (converted_data.size() != expected_size) { + LOG_ERROR(Lib_Audio3d, "Converted size mismatch: got {} bytes, expected {} bytes", + converted_data.size(), expected_size); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + AudioData audio_data{ + .sample_buffer = std::move(converted_data), + .num_samples = pcm.num_samples, + }; + + std::lock_guard lock(port.lock); + port.queue.emplace_back(std::move(audio_data)); + + LOG_DEBUG(Lib_Audio3d, "Queued stereo S16 audio: {} bytes, {} samples", + audio_data.sample_buffer.size(), audio_data.num_samples); + + return ORBIS_OK; +} + +// Clean up processed OpenAL buffers +static void CleanupProcessedBuffers(Audio3dObject& obj) { + if (obj.al_source.source_id == 0) + return; + + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + + ALint processed = 0; + alGetSourcei(obj.al_source.source_id, AL_BUFFERS_PROCESSED, &processed); + + if (processed > 0) { + std::vector buffers(processed); + alSourceUnqueueBuffers(obj.al_source.source_id, processed, buffers.data()); + alDeleteBuffers(processed, buffers.data()); + } +} + +// Stream object audio (always stereo S16) +static void StreamObjectAudio(Audio3dObject& obj) { + if (!obj.in_use || !obj.active) + return; + + // Clean up processed buffers first + CleanupProcessedBuffers(obj); + + // Ensure OpenAL source exists + if (obj.al_source.source_id == 0) { + alGenSources(1, &obj.al_source.source_id); + if (obj.al_source.source_id == 0) { + LOG_ERROR(Lib_Audio3d, "Failed to generate OpenAL source"); + return; + } + + // Set default OpenAL source properties + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + alSourcef(obj.al_source.source_id, AL_PITCH, obj.al_source.pitch); + alSourcef(obj.al_source.source_id, AL_GAIN, obj.al_source.gain); + alSource3f(obj.al_source.source_id, AL_POSITION, obj.al_source.position[0], + obj.al_source.position[1], obj.al_source.position[2]); + alSource3f(obj.al_source.source_id, AL_VELOCITY, obj.al_source.velocity[0], + obj.al_source.velocity[1], obj.al_source.velocity[2]); + alSourcef(obj.al_source.source_id, AL_REFERENCE_DISTANCE, obj.al_source.reference_distance); + alSourcef(obj.al_source.source_id, AL_MAX_DISTANCE, obj.al_source.max_distance); + alSourcef(obj.al_source.source_id, AL_ROLLOFF_FACTOR, obj.al_source.rolloff_factor); + alSourcei(obj.al_source.source_id, AL_LOOPING, obj.al_source.looping ? AL_TRUE : AL_FALSE); + alSourcei(obj.al_source.source_id, AL_SOURCE_RELATIVE, AL_FALSE); + } + + // Check if there is audio to stream + if (obj.pcm_queue.empty()) + return; + + auto& data = obj.pcm_queue.front(); + + if (data.sample_buffer.empty()) { + LOG_WARNING(Lib_Audio3d, "Empty audio buffer in queue"); + obj.pcm_queue.pop_front(); + return; + } + + // Generate OpenAL buffer + ALuint buffer = 0; + alGenBuffers(1, &buffer); + + if (buffer == 0) { + LOG_ERROR(Lib_Audio3d, "Failed to generate OpenAL buffer"); + obj.pcm_queue.pop_front(); + return; + } + + // Always use stereo S16 format (AL_FORMAT_STEREO16 = 0x1103) + ALenum al_format = AL_FORMAT_STEREO16; + + // Upload data to buffer + alBufferData(buffer, al_format, data.sample_buffer.data(), + static_cast(data.sample_buffer.size()), AUDIO3D_SAMPLE_RATE); + + ALenum error = alGetError(); + if (error != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to upload buffer data: {}", error); + alDeleteBuffers(1, &buffer); + obj.pcm_queue.pop_front(); + return; + } + + // Queue buffer to source + alSourceQueueBuffers(obj.al_source.source_id, 1, &buffer); + + error = alGetError(); + if (error != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to queue buffer: {}", error); + alDeleteBuffers(1, &buffer); + obj.pcm_queue.pop_front(); + return; + } + + // Check if source is playing + ALint state; + alGetSourcei(obj.al_source.source_id, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING) { + alSourcePlay(obj.al_source.source_id); + error = alGetError(); + if (error != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to play source: {}", error); + } + } + + // Remove from queue + obj.pcm_queue.pop_front(); +} + s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { LOG_DEBUG(Lib_Audio3d, "called, handle = {}, ptr = {}", handle, ptr); @@ -58,38 +334,6 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* p return AudioOut::sceAudioOutOutputs(param, num); } -static s32 PortQueueAudio(Port& port, const OrbisAudio3dPcm& pcm, const u32 num_channels) { - // Audio3d output is configured for stereo signed 16-bit PCM. Convert the data to match. - const SDL_AudioSpec src_spec = { - .format = pcm.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16 ? SDL_AUDIO_S16LE - : SDL_AUDIO_F32LE, - .channels = static_cast(num_channels), - .freq = AUDIO3D_SAMPLE_RATE, - }; - constexpr SDL_AudioSpec dst_spec = { - .format = SDL_AUDIO_S16LE, - .channels = AUDIO3D_OUTPUT_NUM_CHANNELS, - .freq = AUDIO3D_SAMPLE_RATE, - }; - const auto src_size = pcm.num_samples * - (pcm.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16 ? 2 : 4) * - num_channels; - - u8* dst_data; - int dst_len; - if (!SDL_ConvertAudioSamples(&src_spec, static_cast(pcm.sample_buffer), - static_cast(src_size), &dst_spec, &dst_data, &dst_len)) { - LOG_ERROR(Lib_Audio3d, "SDL_ConvertAudioSamples failed: {}", SDL_GetError()); - return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; - } - - port.queue.emplace_back(AudioData{ - .sample_buffer = dst_data, - .num_samples = pcm.num_samples, - }); - 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) { @@ -114,18 +358,14 @@ s32 PS4_SYSV_ABI sceAudio3dBedWrite2(const OrbisAudio3dPortId port_id, const u32 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"); + // We only support stereo (2 channels) or 8-channel (for downmixing) S16 format + if (format != OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + LOG_ERROR(Lib_Audio3d, "Only S16 format supported, got: {}", magic_enum::enum_name(format)); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } if (num_channels != 2 && num_channels != 8) { - LOG_ERROR(Lib_Audio3d, "num_channels != 2 && num_channels != 8"); + LOG_ERROR(Lib_Audio3d, "Only 2 or 8 channels supported, got: {}", num_channels); return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } @@ -134,16 +374,10 @@ s32 PS4_SYSV_ABI sceAudio3dBedWrite2(const OrbisAudio3dPortId port_id, const u32 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; - } + // Verify buffer alignment for S16 (2-byte alignment) + if ((reinterpret_cast(buffer) & 1) != 0) { + LOG_ERROR(Lib_Audio3d, "s16 buffer & 1 != 0 (not 2-byte aligned)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } return PortQueueAudio(state->ports[port_id], @@ -175,16 +409,64 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, switch (attribute.attribute_id) { case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_PCM: { - const auto pcm = static_cast(attribute.value); - // Object audio has 1 channel. - if (const auto ret = PortQueueAudio(port, *pcm, 1); ret != ORBIS_OK) { - return ret; + if (object_id >= port.objects.size()) + return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; + + auto& obj = port.objects[object_id]; + const auto* pcm = static_cast(attribute.value); + + // Convert to stereo S16 (objects are mono, but we'll handle as stereo) + auto converted_data = ConvertToStereoS16(*pcm, 1); + + if (converted_data.empty()) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + AudioData audio_data{ + .sample_buffer = std::move(converted_data), + .num_samples = pcm->num_samples, + }; + + obj.pcm_queue.push_back(std::move(audio_data)); + break; + } + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_POSITION: { + if (object_id >= port.objects.size()) { + return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; + } + + auto& obj = port.objects[object_id]; + if (attribute.value_size >= sizeof(float) * 3) { + const float* position = static_cast(attribute.value); + ConvertPositionToOpenAL(position, obj.al_source.position); + + if (obj.al_source.source_id != 0) { + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + alSource3f(obj.al_source.source_id, AL_POSITION, obj.al_source.position[0], + obj.al_source.position[1], obj.al_source.position[2]); + } + } + break; + } + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_GAIN: { + if (object_id >= port.objects.size()) { + return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; + } + + auto& obj = port.objects[object_id]; + if (attribute.value_size >= sizeof(float)) { + obj.al_source.gain = *static_cast(attribute.value); + + if (obj.al_source.source_id != 0) { + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + alSourcef(obj.al_source.source_id, AL_GAIN, obj.al_source.gain); + } } break; } default: - LOG_ERROR(Lib_Audio3d, "Unsupported attribute ID: {:#x}", - static_cast(attribute.attribute_id)); + LOG_INFO(Lib_Audio3d, "Processing attribute ID: {:#x}", + static_cast(attribute.attribute_id)); break; } } @@ -193,32 +475,23 @@ s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, } s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { - LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + LOG_DEBUG(Lib_Audio3d, "sceAudio3dPortAdvance(port_id={})", port_id); - if (!state->ports.contains(port_id)) { - LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + auto it = state->ports.find(port_id); + if (it == state->ports.end()) return ORBIS_AUDIO3D_ERROR_INVALID_PORT; - } - if (state->ports[port_id].parameters.buffer_mode == - OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_NO_ADVANCE) { - LOG_ERROR(Lib_Audio3d, "port doesn't have advance capability"); + Port& port = it->second; + + if (port.parameters.buffer_mode == OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_NO_ADVANCE) { return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; } - auto& port = state->ports[port_id]; - if (port.current_buffer.has_value()) { - // Free existing buffer before replacing. - SDL_free(port.current_buffer->sample_buffer); - } - if (!port.queue.empty()) { - port.current_buffer = port.queue.front(); + port.current_buffer = std::move(port.queue.front()); port.queue.pop_front(); } else { - // Nothing to advance to. - LOG_DEBUG(Lib_Audio3d, "Port advance with no buffer queued"); - port.current_buffer = std::nullopt; + port.current_buffer.reset(); } return ORBIS_OK; @@ -226,28 +499,22 @@ s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { 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)"); + auto it = state->ports.find(port_id); + if (it == state->ports.end()) return ORBIS_AUDIO3D_ERROR_INVALID_PORT; - } - if (!queue_level && !queue_available) { + if (!queue_level && !queue_available) return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; - } - auto& port = state->ports[port_id]; - const size_t size = port.queue.size(); + const Port& port = it->second; + const u32 depth = port.parameters.queue_depth; + const u32 level = static_cast(port.queue.size()); - if (queue_level) { - *queue_level = size; - } + if (queue_level) + *queue_level = level; - if (queue_available) { - *queue_available = port.parameters.queue_depth - size; - } + if (queue_available) + *queue_available = (level >= depth) ? 0 : (depth - level); return ORBIS_OK; } @@ -262,7 +529,9 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_PORT; } - const auto& port = state->ports[port_id]; + auto& port = state->ports[port_id]; + std::lock_guard lock(port.lock); + if (port.parameters.buffer_mode != OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH) { LOG_ERROR(Lib_Audio3d, "port doesn't have push capability"); @@ -270,14 +539,37 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, } if (!port.current_buffer.has_value()) { - // Nothing to push. - LOG_DEBUG(Lib_Audio3d, "Port push with no buffer ready"); - return ORBIS_OK; + // Generate silent audio if nothing to push + LOG_DEBUG(Lib_Audio3d, "Port push with no buffer ready - generating silence"); + + // Create silent buffer (stereo S16, granularity samples) + const size_t silent_size = port.parameters.granularity * 2 * sizeof(int16_t); + std::vector silent_buffer(silent_size, std::byte{0}); + + return AudioOut::sceAudioOutOutput(state->audio_out_handle, silent_buffer.data()); } - // TODO: Implement asynchronous blocking mode. - const auto& [sample_buffer, num_samples] = port.current_buffer.value(); - return AudioOut::sceAudioOutOutput(state->audio_out_handle, sample_buffer); + auto& audio_data = port.current_buffer.value(); + + // Stream object audio + for (auto& obj : port.objects) { + StreamObjectAudio(obj); + } + + // Output bed audio + if (!audio_data.sample_buffer.empty()) { + LOG_DEBUG(Lib_Audio3d, "Pushing bed audio: {} bytes", audio_data.sample_buffer.size()); + + return AudioOut::sceAudioOutOutput( + state->audio_out_handle, + const_cast(static_cast(audio_data.sample_buffer.data()))); + } else { + // Generate silent audio + const size_t silent_size = port.parameters.granularity * 2 * sizeof(int16_t); + std::vector silent_buffer(silent_size, std::byte{0}); + + return AudioOut::sceAudioOutOutput(state->audio_out_handle, silent_buffer.data()); + } } s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, @@ -297,103 +589,128 @@ s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; } - // TODO + auto& port = state->ports[port_id]; + + switch (attribute_id) { + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_PORT_ATTRIBUTE_LATE_REVERB_LEVEL: + LOG_INFO(Lib_Audio3d, "Setting late reverb level (not implemented)"); + break; + + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_POSITION: + // Update listener position + if (attribute_size >= sizeof(float) * 3) { + const float* position = static_cast(attribute); + ConvertPositionToOpenAL(position, port.listener_position); + + if (Libraries::AudioOut::OpenALManager::Instance().IsInitialized()) { + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + alListener3f(AL_POSITION, port.listener_position[0], port.listener_position[1], + port.listener_position[2]); + } + } + break; + + default: + LOG_INFO(Lib_Audio3d, "Setting port attribute {:#x} (not fully implemented)", + static_cast(attribute_id)); + break; + } return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortClose() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortCreate() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortDestroy() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortFlush() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortFreeState() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortGetList() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortGetParameters() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortGetState() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudio3dStrError() { - LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + LOG_INFO(Lib_Audio3d, "(STUBBED) called"); return ORBIS_OK; } @@ -433,6 +750,24 @@ s32 PS4_SYSV_ABI sceAudio3dInitialize(const s64 reserved) { return init_ret; } + // Initialize OpenAL for 3D audio + if (!Libraries::AudioOut::OpenALManager::Instance().Initialize(AUDIO3D_SAMPLE_RATE)) { + LOG_ERROR(Lib_Audio3d, "Failed to initialize OpenAL for 3D audio"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + // Enable HRTF if available + if (Libraries::AudioOut::OpenALManager::Instance().HasHRTF()) { + LOG_INFO(Lib_Audio3d, "HRTF is available"); + } + + // Set default OpenAL listener parameters + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); + alListener3f(AL_VELOCITY, 0.0f, 0.0f, 0.0f); + float listener_orientation[] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; + alListenerfv(AL_ORIENTATION, listener_orientation); + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); state->audio_out_handle = @@ -451,9 +786,32 @@ s32 PS4_SYSV_ABI sceAudio3dTerminate() { return ORBIS_AUDIO3D_ERROR_NOT_READY; } + // Clean up OpenAL resources for each port + for (auto& [port_id, port] : state->ports) { + std::lock_guard lock(port.lock); + + // Clean up OpenAL sources for objects + for (auto& obj : port.objects) { + if (obj.al_source.source_id != 0) { + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + if (obj.al_source.buffer_id != 0) { + alDeleteBuffers(1, &obj.al_source.buffer_id); + } + alDeleteSources(1, &obj.al_source.source_id); + obj.al_source.source_id = 0; + obj.al_source.buffer_id = 0; + } + } + port.objects.clear(); + port.queue.clear(); + port.current_buffer.reset(); + } + state->ports.clear(); + AudioOut::sceAudioOutOutput(state->audio_out_handle, nullptr); AudioOut::sceAudioOutClose(state->audio_out_handle); - state.release(); + state.reset(); + return ORBIS_OK; } @@ -613,7 +971,7 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( return ORBIS_AUDIO3D_ERROR_NOT_READY; } - return audio_out_handle; // TODO missing more stuff but let's keep it simple for now + return audio_out_handle; } s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { @@ -643,6 +1001,36 @@ s32 PS4_SYSV_ABI sceAudio3dObjectReserve(const OrbisAudio3dPortId port_id, auto& obj = port.objects[i]; if (!obj.in_use) { obj = {}; // reset + + // Create OpenAL source for this object + if (Libraries::AudioOut::OpenALManager::Instance().IsInitialized()) { + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + + alGenSources(1, &obj.al_source.source_id); + ALenum error = alGetError(); + if (error != AL_NO_ERROR) { + LOG_ERROR(Lib_Audio3d, "Failed to generate OpenAL source: {}", error); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + // Set default OpenAL source properties for stereo S16 + alSourcef(obj.al_source.source_id, AL_PITCH, obj.al_source.pitch); + alSourcef(obj.al_source.source_id, AL_GAIN, obj.al_source.gain); + alSource3f(obj.al_source.source_id, AL_POSITION, obj.al_source.position[0], + obj.al_source.position[1], obj.al_source.position[2]); + alSource3f(obj.al_source.source_id, AL_VELOCITY, obj.al_source.velocity[0], + obj.al_source.velocity[1], obj.al_source.velocity[2]); + alSourcef(obj.al_source.source_id, AL_REFERENCE_DISTANCE, + obj.al_source.reference_distance); + alSourcef(obj.al_source.source_id, AL_MAX_DISTANCE, obj.al_source.max_distance); + alSourcef(obj.al_source.source_id, AL_ROLLOFF_FACTOR, obj.al_source.rolloff_factor); + alSourcei(obj.al_source.source_id, AL_LOOPING, + obj.al_source.looping ? AL_TRUE : AL_FALSE); + + // Enable 3D positioning + alSourcei(obj.al_source.source_id, AL_SOURCE_RELATIVE, AL_FALSE); + } + obj.in_use = true; obj.active = true; *object_id = i; @@ -676,9 +1064,19 @@ s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(OrbisAudio3dPortId port_id, if (!obj.in_use) return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; + // Clean up OpenAL resources + if (obj.al_source.source_id != 0) { + Libraries::AudioOut::OpenALManager::Instance().MakeContextCurrent(); + if (obj.al_source.buffer_id != 0) { + alDeleteBuffers(1, &obj.al_source.buffer_id); + } + alDeleteSources(1, &obj.al_source.source_id); + } + // Mark free obj.in_use = false; obj.active = false; + obj.al_source = {}; // Reset OpenAL source return ORBIS_OK; } @@ -700,7 +1098,7 @@ s32 sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId portId, OrbisAudio3d std::vector supported; - // Always supported + // Always supported for S16 stereo supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_PCM); supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_GAIN); supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_PRIORITY); @@ -710,7 +1108,7 @@ s32 sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId portId, OrbisAudio3d supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_RESTRICTED); if (hrtf) { - // Object-based renderer + // Object-based renderer with HRTF supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_POSITION); supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_SPREAD); supported.push_back(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_OBJECT_ATTRIBUTE_OUTPUT_ROUTE); @@ -787,4 +1185,4 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("WW1TS2iz5yc", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dTerminate); }; -} // namespace Libraries::Audio3d +} // namespace Libraries::Audio3d \ No newline at end of file diff --git a/src/core/libraries/audio3d/audio3d.h b/src/core/libraries/audio3d/audio3d.h index a11d52c10..75e3fdad9 100644 --- a/src/core/libraries/audio3d/audio3d.h +++ b/src/core/libraries/audio3d/audio3d.h @@ -3,9 +3,14 @@ #pragma once +#include +#include #include +#include +#include #include +#include #include "common/types.h" #include "core/libraries/audio/audioout.h" @@ -87,22 +92,48 @@ struct OrbisAudio3dAttribute { u64 value_size; }; +// OpenAL-specific structures for 3D audio +struct OpenAL3dSource { + ALuint source_id = 0; + ALuint buffer_id = 0; + bool active = false; + float position[3] = {0.0f, 0.0f, 0.0f}; + float velocity[3] = {0.0f, 0.0f, 0.0f}; + float gain = 1.0f; + float pitch = 1.0f; + float reference_distance = 1.0f; + float max_distance = 100.0f; + float rolloff_factor = 1.0f; + float cone_inner_angle = 360.0f; + float cone_outer_angle = 360.0f; + float cone_outer_gain = 0.0f; + bool looping = false; +}; + +// Simplified audio data - always stereo S16 struct AudioData { - u8* sample_buffer; - u32 num_samples; + std::vector sample_buffer; // Always stereo S16 format + u32 num_samples; // Number of stereo samples }; struct Audio3dObject { bool in_use = false; bool active = false; + OpenAL3dSource al_source; + std::deque pcm_queue; // Stereo S16 audio data }; struct Port { OrbisAudio3dOpenParameters parameters{}; - std::deque queue; // Only stores PCM buffers for now + std::deque queue; // Stereo S16 audio data std::optional current_buffer{}; std::vector objects; std::mutex lock; + + // OpenAL listener for this port + float listener_position[3] = {0.0f, 0.0f, 0.0f}; + float listener_velocity[3] = {0.0f, 0.0f, 0.0f}; + float listener_orientation[6] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; }; struct Audio3dState { @@ -110,6 +141,7 @@ struct Audio3dState { s32 audio_out_handle; }; +// Function declarations s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle); s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, Libraries::UserService::OrbisUserServiceUserId user_id, @@ -165,4 +197,4 @@ s32 PS4_SYSV_ABI sceAudio3dStrError(); s32 PS4_SYSV_ABI sceAudio3dTerminate(); void RegisterLib(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::Audio3d +} // namespace Libraries::Audio3d \ No newline at end of file