diff --git a/CMakeLists.txt b/CMakeLists.txt index 1de4416ee..9e77a037a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,6 +293,8 @@ 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 ) diff --git a/src/core/libraries/audio/audioout_backend.h b/src/core/libraries/audio/audioout_backend.h index 0f36f19c8..ce6e261ab 100644 --- a/src/core/libraries/audio/audioout_backend.h +++ b/src/core/libraries/audio/audioout_backend.h @@ -31,4 +31,9 @@ public: std::unique_ptr Open(PortOut& port) override; }; +class OpenALAudioOut final : public AudioOutBackend { +public: + std::unique_ptr Open(PortOut& port) override; +}; + } // namespace Libraries::AudioOut diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp new file mode 100644 index 000000000..9cf903d09 --- /dev/null +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -0,0 +1,662 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/config.h" +#include "common/logging/log.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_backend.h" +#include "core/libraries/audio/openal_manager.h" +#include "core/libraries/kernel/threads.h" + +// SIMD support detection +#if defined(__x86_64__) || defined(_M_X64) +#include +#define HAS_SSE2 +#endif + +namespace Libraries::AudioOut { + +// Volume constants +constexpr float VOLUME_0DB = 32768.0f; // 1 << 15 +constexpr float INV_VOLUME_0DB = 1.0f / VOLUME_0DB; +constexpr float VOLUME_EPSILON = 0.001f; +// Timing constants +constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms +constexpr u64 MIN_SLEEP_THRESHOLD_US = 10; +constexpr u64 TIMING_RESYNC_THRESHOLD_US = 100000; // Resync if >100ms behind + +// OpenAL constants +constexpr ALsizei NUM_BUFFERS = 4; // Triple buffering +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) { + + if (!Initialize(port.type)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL audio backend"); + } + } + + ~OpenALPortBackend() override { + Cleanup(); + } + + void Output(void* ptr) override { + if (!source || buffers.empty() || !convert) [[unlikely]] { + return; + } + + if (ptr == nullptr) [[unlikely]] { + return; + } + + // Make context current before any OpenAL operations + if (!device_context->MakeCurrent()) { + return; + } + + UpdateVolumeIfChanged(); + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + // Process audio data + convert(ptr, al_buffer.data(), buffer_frames, nullptr); + HandleTiming(current_time); + + // Manage buffer queue + while (!available_buffers.empty() && available_buffers.size() < NUM_BUFFERS) { + ALint processed = 0; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + + if (processed <= 0) + break; + + ALuint buffer_id; + alSourceUnqueueBuffers(source, 1, &buffer_id); // One at a time + + if (alGetError() != AL_NO_ERROR) { + break; // Stop on error + } + + available_buffers.push_back(buffer_id); + } + + // Check if we need to queue more buffers + ALint queued = 0; + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + + if (queued < BUFFER_QUEUE_THRESHOLD && !available_buffers.empty()) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + + alBufferData(buffer_id, format, al_buffer.data(), buffer_size_bytes, sample_rate); + + alSourceQueueBuffers(source, 1, &buffer_id); + queued++; + } + // Ensure the source is playing (OpenAL does NOT auto-start) + ALint state = 0; + alGetSourcei(source, AL_SOURCE_STATE, &state); + + if (state != AL_PLAYING && queued > 0) { + alSourcePlay(source); + } + // Update timing + last_output_time.store(current_time, std::memory_order_release); + output_count++; + } + + void SetVolume(const std::array& ch_volumes) override { + // Make context current before any OpenAL operations + if (!device_context->MakeCurrent()) { + return; + } + + if (!source) [[unlikely]] { + return; + } + + float max_channel_gain = 0.0f; + const u32 channels_to_check = std::min(num_channels, 8u); + + for (u32 i = 0; i < channels_to_check; i++) { + const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; + max_channel_gain = std::max(max_channel_gain, channel_gain); + } + + const float slider_gain = Config::getVolumeSlider() * 0.01f; + const float total_gain = max_channel_gain * slider_gain; + + // Only update if changed significantly + const float current = current_gain.load(std::memory_order_acquire); + if (std::abs(total_gain - current) < VOLUME_EPSILON) { + return; + } + + // Apply volume change to OpenAL source + 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) { + // Get OpenAL device and context + if (!OpenALDevice::GetInstance().IsInitialized()) { + LOG_ERROR(Lib_AudioOut, "OpenAL device not initialized"); + return false; + } + + device_context = &OpenALDevice::GetInstance(); + + // Make context current for initialization + if (!device_context->MakeCurrent()) { + LOG_ERROR(Lib_AudioOut, "Failed to make OpenAL context current"); + return false; + } + + // Calculate timing parameters + period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; + + // Determine OpenAL format + if (!DetermineOpenALFormat()) { + LOG_ERROR(Lib_AudioOut, "Unsupported audio format for OpenAL"); + return false; + } + + // Calculate buffer size + buffer_size_bytes = buffer_frames * frame_size; + + // Allocate current buffer + current_buffer.resize(buffer_frames * num_channels); // float staging + al_buffer.resize(buffer_frames * num_channels); // int16 upload + 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); + + // Apply initial volume + alSourcef(source, AL_GAIN, current_gain.load(std::memory_order_relaxed)); + + LOG_INFO(Lib_AudioOut, "Initialized OpenAL backend ({} Hz, {} ch, {} format)", sample_rate, + num_channels, is_float ? "float" : "int16"); + + return true; + } + + void Cleanup() { + if (!device_context->MakeCurrent()) { + return; + } + + if (source) { + alSourceStop(source); + + ALint queued = 0; + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + while (queued-- > 0) { + ALuint buf; + alSourceUnqueueBuffers(source, 1, &buf); + } + + alDeleteSources(1, &source); + source = 0; + } + + if (!buffers.empty()) { + alDeleteBuffers(static_cast(buffers.size()), buffers.data()); + buffers.clear(); + } + } + + 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); + + // Only update if the difference is significant + 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]] { + // First output - set initial timing + next_output_time = current_time + period_us; + return; + } + + const s64 time_diff = static_cast(current_time - next_output_time); + + if (time_diff > static_cast(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] { + // We're far behind - resync + next_output_time = current_time + period_us; + } else if (time_diff < 0) { + // We're ahead of schedule - wait + const u64 time_to_wait = static_cast(-time_diff); + next_output_time += period_us; + + if (time_to_wait > MIN_SLEEP_THRESHOLD_US) { + // Sleep for most of the wait period + const u64 sleep_duration = time_to_wait - MIN_SLEEP_THRESHOLD_US; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration)); + } + } else { + // Slightly behind or on time - just advance + next_output_time += period_us; + } + } + + bool DetermineOpenALFormat() { + if (is_float) { + // OpenAL doesn't natively support float formats, we need to use AL_EXT_FLOAT32 + // extension For simplicity, we'll convert to int16 in our converter functions + format = AL_FORMAT_MONO16; // Default, will be overridden + + 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 { + // 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() { + // Generate source + alGenSources(1, &source); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL source"); + return false; + } + + // Generate buffers + buffers.resize(NUM_BUFFERS); + alGenBuffers(static_cast(buffers.size()), buffers.data()); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL buffers"); + alDeleteSources(1, &source); + source = 0; + return false; + } + + // All buffers are initially available + available_buffers = buffers; + + // Configure source properties + 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) { + // For OpenAL, we need to convert float to int16 + switch (num_channels) { + case 1: + convert = &ConvertF32ToS16Mono; + break; + case 2: + convert = &ConvertF32ToS16Stereo; + break; + case 8: + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + switch (num_channels) { + case 1: + convert = &ConvertS16Mono; + break; + case 2: +#if defined(HAS_SSE2) + convert = &ConvertS16StereoSIMD; +#else + convert = &ConvertS16Stereo; +#endif + break; + case 8: +#if defined(HAS_SSE2) + convert = &ConvertS16_8CH_SIMD; +#else + convert = &ConvertS16_8CH; +#endif + 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); + + // S16 converters (same as SDL backend) + static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } + + static void ConvertS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + const u32 num_samples = frames << 1; + for (u32 i = 0; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } + +#ifdef HAS_SSE2 + static void ConvertS16StereoSIMD(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(INV_VOLUME_0DB); + const u32 num_samples = frames << 1; + u32 i = 0; + + for (; i + 8 <= num_samples; i += 8) { + __m128i s16_vals = _mm_loadu_si128(reinterpret_cast(&s[i])); + __m128i s32_lo = _mm_cvtepi16_epi32(s16_vals); + __m128i s32_hi = _mm_cvtepi16_epi32(_mm_srli_si128(s16_vals, 8)); + __m128 f_lo = _mm_mul_ps(_mm_cvtepi32_ps(s32_lo), scale); + __m128 f_hi = _mm_mul_ps(_mm_cvtepi32_ps(s32_hi), scale); + _mm_storeu_ps(&d[i], f_lo); + _mm_storeu_ps(&d[i + 4], f_hi); + } + + for (; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } +#endif + + static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + const u32 num_samples = frames << 3; + for (u32 i = 0; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } + +#ifdef HAS_SSE2 + static void ConvertS16_8CH_SIMD(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(INV_VOLUME_0DB); + const u32 num_samples = frames << 3; + u32 i = 0; + + for (; i + 8 <= num_samples; i += 8) { + __m128i s16_vals = _mm_loadu_si128(reinterpret_cast(&s[i])); + __m128i s32_lo = _mm_cvtepi16_epi32(s16_vals); + __m128i s32_hi = _mm_cvtepi16_epi32(_mm_srli_si128(s16_vals, 8)); + __m128 f_lo = _mm_mul_ps(_mm_cvtepi32_ps(s32_lo), scale); + __m128 f_hi = _mm_mul_ps(_mm_cvtepi32_ps(s32_hi), scale); + _mm_storeu_ps(&d[i], f_lo); + _mm_storeu_ps(&d[i + 4], f_hi); + } + + for (; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } +#endif + + // Float to S16 converters for OpenAL + static void ConvertF32ToS16Mono(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const float sample = s[i] * VOLUME_0DB; + d[i] = static_cast(std::clamp(sample, -32768.0f, 32767.0f)); + } + } + + static void ConvertF32ToS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + for (u32 i = 0; i < num_samples; i++) { + const float sample = s[i] * VOLUME_0DB; + d[i] = static_cast(std::clamp(sample, -32768.0f, 32767.0f)); + } + } + + static void ConvertF32ToS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + for (u32 i = 0; i < num_samples; i++) { + const float sample = s[i] * VOLUME_0DB; + d[i] = static_cast(std::clamp(sample, -32768.0f, 32767.0f)); + } + } + + static void ConvertF32ToS16Std8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const u32 offset = i << 3; + + d[offset + FL] = + static_cast(std::clamp(s[offset + FL] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + FR] = + static_cast(std::clamp(s[offset + FR] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + FC] = + static_cast(std::clamp(s[offset + FC] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + LF] = + static_cast(std::clamp(s[offset + LF] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + SL] = + static_cast(std::clamp(s[offset + STD_SL] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + SR] = + static_cast(std::clamp(s[offset + STD_SR] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + BL] = + static_cast(std::clamp(s[offset + STD_BL] * VOLUME_0DB, -32768.0f, 32767.0f)); + d[offset + BR] = + static_cast(std::clamp(s[offset + STD_BR] * VOLUME_0DB, -32768.0f, 32767.0f)); + } + } + + // Audio format parameters + const u32 frame_size; + const u32 guest_buffer_size; + const u32 buffer_frames; + const u32 sample_rate; + const u32 num_channels; + const bool is_float; + const bool is_std; + const std::array channel_layout; + + alignas(64) u64 period_us{0}; + alignas(64) std::atomic last_output_time{0}; + u64 next_output_time{0}; + u64 last_volume_check_time{0}; + u32 output_count{0}; + + // OpenAL objects + OpenALDevice* device_context{nullptr}; + ALuint source{0}; + std::vector buffers; + std::vector available_buffers; + ALenum format{AL_FORMAT_STEREO16}; + + // Buffer management + u32 buffer_size_bytes{0}; + std::vector current_buffer; + std::vector al_buffer; + + // Converter function pointer + ConverterFunc convert{nullptr}; + + // Volume management + alignas(64) std::atomic current_gain{1.0f}; +}; + +std::unique_ptr OpenALAudioOut::Open(PortOut& port) { + return std::make_unique(port); +} + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio/openal_manager.h b/src/core/libraries/audio/openal_manager.h new file mode 100644 index 000000000..4f58fca18 --- /dev/null +++ b/src/core/libraries/audio/openal_manager.h @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace Libraries::AudioOut { + +class OpenALDevice { +public: + static OpenALDevice& GetInstance() { + static OpenALDevice instance; + return instance; + } + + bool IsInitialized() const { + return initialized; + } + + ALCdevice* GetDevice() const { + return device; + } + + ALCcontext* GetContext() const { + return context; + } + + bool MakeCurrent() { + std::lock_guard lock(mutex); + + if (!initialized) { + return false; + } + + return alcMakeContextCurrent(context); + } + + void ReleaseContext() { + std::lock_guard lock(mutex); + alcMakeContextCurrent(nullptr); + } + +private: + OpenALDevice() { + Initialize(); + } + + ~OpenALDevice() { + Cleanup(); + } + + OpenALDevice(const OpenALDevice&) = delete; + OpenALDevice& operator=(const OpenALDevice&) = delete; + + void Initialize() { + std::lock_guard lock(mutex); + + if (initialized) { + return; + } + + // Open default device + device = alcOpenDevice(nullptr); + if (!device) { + return; + } + + // Create context + context = alcCreateContext(device, nullptr); + if (!context) { + alcCloseDevice(device); + device = nullptr; + return; + } + + initialized = true; + } + + void Cleanup() { + std::lock_guard lock(mutex); + + if (!initialized) { + return; + } + + ReleaseContext(); + + if (context) { + alcDestroyContext(context); + context = nullptr; + } + + if (device) { + alcCloseDevice(device); + device = nullptr; + } + + initialized = false; + } + + ALCdevice* device{nullptr}; + ALCcontext* context{nullptr}; + bool initialized{false}; + mutable std::mutex mutex; +}; + +} // namespace Libraries::AudioOut \ No newline at end of file