OpenAL sound backened (#4030)

* code cleanup

* simd implementation and better buffer support

* reduce logging in audioout

* added openal libs

* added sceAudioOutGetSystemState

* fixed backend

* initial audio out openal support , still not working properly

* cleanups

* fixed converters for openal

* basic work for openal and audio3d

* clang

* better conversion?

* trying to fix conversions

* better conversions?

* more safe format conversions

* improved audioout conversions

* seems better

* smoother

* increased buffers

* reduce spamming log

* added SIMD version of convertors

* some corrections to audio3d

* support for AL_EXT_FLOAT32 extension and fallbacks if not available

* added device selection

* use ALC_ALL_DEVICES_SPECIFIER

* no it will not work this way

* improved device detection

* fixed audio3d again

* proper device register in openal

* some openal audio3d improvements based on the sdl one

* clang
This commit is contained in:
georgemoralis 2026-03-10 11:00:21 +02:00 committed by GitHub
parent c9595a4288
commit 3157382d8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 2253 additions and 7 deletions

1
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -205,7 +205,7 @@ s32 PS4_SYSV_ABI sceAudioOutInit() {
return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT;
}
audio = std::make_unique<SDLAudioOut>();
audio = std::make_unique<OpenALAudioOut>();
LOG_INFO(Lib_AudioOut, "Audio system initialized");
return ORBIS_OK;

View File

@ -31,4 +31,9 @@ public:
std::unique_ptr<PortBackend> Open(PortOut& port) override;
};
class OpenALAudioOut final : public AudioOutBackend {
public:
std::unique_ptr<PortBackend> Open(PortOut& port) override;
};
} // namespace Libraries::AudioOut

View File

@ -0,0 +1,833 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <atomic>
#include <cstring>
#include <memory>
#include <thread>
#include <vector>
#include <AL/al.h>
#include <AL/alc.h>
#include <alext.h>
#include <queue>
#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 <immintrin.h>
#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<int, 8>& 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<float>(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<int>(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<int>(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<float> 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<s16> 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<ALsizei>(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<s64>(current_time - next_output_time);
if (time_diff > static_cast<s64>(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] {
next_output_time = current_time + period_us;
} else if (time_diff < 0) {
const u64 time_to_wait = static_cast<u64>(-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<ALsizei>(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<s16>(scaled + (scaled >= 0 ? 0.5f : -0.5f));
}
static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
s16* d = static_cast<s16*>(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<const s16*>(src);
s16* d = static_cast<s16*>(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<const s16*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<int, 8> channel_layout;
alignas(64) u64 period_us{0};
alignas(64) std::atomic<u64> 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<ALuint> buffers;
std::vector<ALuint> available_buffers;
ALenum format{AL_FORMAT_STEREO16};
// Buffer management
u32 buffer_size_bytes{0};
std::vector<s16> al_buffer_s16; // For S16 formats
std::vector<float> 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<float> current_gain{1.0f};
std::string device_name;
bool device_registered;
};
std::unique_ptr<PortBackend> OpenALAudioOut::Open(PortOut& port) {
return std::make_unique<OpenALPortBackend>(port);
}
} // namespace Libraries::AudioOut

View File

@ -0,0 +1,226 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <AL/al.h>
#include <AL/alc.h>
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::string> GetAvailableDevices() {
std::vector<std::string> 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<std::mutex> 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<std::string, DeviceContext> devices;
mutable std::mutex mutex;
ALCcontext* current_context{nullptr}; // For thread-local tracking
};
} // namespace Libraries::AudioOut

View File

@ -0,0 +1,997 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#include <magic_enum/magic_enum.hpp>
#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<Audio3dState> 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<AudioOut::OrbisAudioOutPort>(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<void*>(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<AudioData>& 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<u8*>(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<uintptr_t>(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<uintptr_t>(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<Audio3dState>();
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<void*>(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<u32>(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<u32>(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<const u8*>(attribute);
obj.persistent_attributes[static_cast<u32>(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<OrbisAudio3dPcm*>(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<const u8*>(attr.value);
obj.persistent_attributes[static_cast<u32>(attr.attribute_id)].assign(
src, src + attr.value_size);
}
LOG_DEBUG(Lib_Audio3d, "Stored attribute {:#x} for object {}",
static_cast<u32>(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<float*>(std::calloc(out_samples, sizeof(float)));
if (!mix_float)
return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY;
auto mix_in = [&](std::deque<AudioData>& 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<const s16*>(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<const float*>(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<u32>(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<s16*>(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<s16>(v * 32767.0f);
}
std::free(mix_float);
port.mixed_queue.push_back(AudioData{.sample_buffer = reinterpret_cast<u8*>(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<void*>(queue_level), static_cast<void*>(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<u32>(size);
}
if (queue_available) {
const u32 depth = port.parameters.queue_depth;
*queue_available = (size < depth) ? static_cast<u32>(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<const void*>(parameters), static_cast<void*>(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<int>(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<u64>(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<u32>(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<OrbisAudio3dPortId> 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

View File

@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <optional>
#include <vector>
#include <queue>
#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<AudioData> pcm_queue;
std::unordered_map<u32, std::vector<u8>> 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<s32> audioout_handles;
// Reserved objects and their state.
std::unordered_map<OrbisAudio3dObjectId, ObjectState> objects;
// Increasing counter for generating unique object IDs within this port.
OrbisAudio3dObjectId next_object_id{0};
// Bed audio queue.
std::deque<AudioData> bed_queue;
// Mixed stereo frames ready to be consumed by sceAudio3dPortPush.
std::deque<AudioData> mixed_queue;
};
struct Audio3dState {
std::unordered_map<OrbisAudio3dPortId, Port> 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

View File

@ -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);