audio_core: Add OpenAL audio backend support

- Add complete OpenAL sink implementation with robust error handling
- Support for device enumeration using ALC extensions
- Implement dummy streams for graceful degradation when OpenAL fails
- Add proper audio threading and buffer management
- Include comprehensive logging and diagnostic information
- Add stream limits and retry mechanisms for stability

Additional changes:
- Add ENABLE_OPENAL CMake option and OpenAL dependency management
- Include openal-soft in vcpkg dependencies
- Add OpenAL to audio engine settings enum

The OpenAL backend provides an alternative audio solution alongside
existing Cubeb and SDL2 backends, with enhanced device compatibility
and improved error recovery mechanisms.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-06-01 15:26:55 +10:00
parent 1238d37f27
commit abf5f6730b
7 changed files with 913 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
cmake_minimum_required(VERSION 3.22) cmake_minimum_required(VERSION 3.22)
@@ -44,6 +45,8 @@ option(CITRON_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation"
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF) option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
option(CITRON_TESTS "Compile tests" "${BUILD_TESTING}") option(CITRON_TESTS "Compile tests" "${BUILD_TESTING}")

View File

@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
add_library(audio_core STATIC add_library(audio_core STATIC
@@ -251,6 +252,17 @@ if (ENABLE_SDL2)
target_compile_definitions(audio_core PRIVATE HAVE_SDL2) target_compile_definitions(audio_core PRIVATE HAVE_SDL2)
endif() endif()
if (ENABLE_OPENAL)
target_sources(audio_core PRIVATE
sink/openal_sink.cpp
sink/openal_sink.h
)
find_package(OpenAL CONFIG REQUIRED)
target_link_libraries(audio_core PRIVATE OpenAL::OpenAL)
target_compile_definitions(audio_core PRIVATE HAVE_OPENAL)
endif()
if (ANDROID) if (ANDROID)
target_sources(audio_core PRIVATE target_sources(audio_core PRIVATE
sink/oboe_sink.cpp sink/oboe_sink.cpp

View File

@@ -0,0 +1,775 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <span>
#include <vector>
#include <thread>
#include <chrono>
#include <AL/al.h>
#include <AL/alc.h>
#include "audio_core/common/common.h"
#include "audio_core/sink/openal_sink.h"
#include "audio_core/sink/sink_stream.h"
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "core/core.h"
// Define missing ALC constants for device enumeration
#ifndef ALC_ALL_DEVICES_SPECIFIER
#define ALC_ALL_DEVICES_SPECIFIER 0x1013
#endif
#ifndef ALC_ENUMERATE_ALL_EXT
#define ALC_ENUMERATE_ALL_EXT 1
#endif
#ifndef ALC_DEFAULT_ALL_DEVICES_SPECIFIER
#define ALC_DEFAULT_ALL_DEVICES_SPECIFIER 0x1012
#endif
namespace AudioCore::Sink {
/**
* OpenAL sink stream, responsible for sinking samples to hardware.
*/
class OpenALSinkStream final : public SinkStream {
public:
/**
* Create a new sink stream.
*
* @param device_channels_ - Number of channels supported by the hardware.
* @param system_channels_ - Number of channels the audio systems expect.
* @param output_device - Name of the output device to use for this stream.
* @param input_device - Name of the input device to use for this stream.
* @param type_ - Type of this stream.
* @param system_ - Core system.
* @param al_device - OpenAL device.
* @param al_context - OpenAL context.
*/
OpenALSinkStream(u32 device_channels_, u32 system_channels_, const std::string& output_device,
const std::string& input_device, StreamType type_, Core::System& system_,
ALCdevice* al_device, ALCcontext* al_context)
: SinkStream{system_, type_}, device{al_device}, context{al_context} {
system_channels = system_channels_;
device_channels = device_channels_;
LOG_DEBUG(Audio_Sink, "Creating OpenAL stream: type={}, device_channels={}, system_channels={}",
static_cast<int>(type_), device_channels_, system_channels_);
if (type == StreamType::In) {
// For input streams, we need to create a capture device
const char* device_name = input_device.empty() ? nullptr : input_device.c_str();
capture_device = alcCaptureOpenDevice(device_name, TargetSampleRate, AL_FORMAT_STEREO16,
TargetSampleCount * 4);
if (!capture_device) {
LOG_CRITICAL(Audio_Sink, "Error opening OpenAL capture device: {}",
alcGetString(nullptr, alcGetError(nullptr)));
return;
}
} else {
// Ensure the context is current before creating OpenAL objects
if (!alcMakeContextCurrent(context)) {
LOG_CRITICAL(Audio_Sink, "Failed to make OpenAL context current for stream creation");
// Create a dummy stream that does nothing but allows the system to continue
is_dummy_stream = true;
LOG_WARNING(Audio_Sink, "Creating dummy audio stream to allow system to continue");
return;
}
// Clear any previous errors
alGetError();
// Verify the context is current and valid
ALCcontext* current_context = alcGetCurrentContext();
if (current_context != context) {
LOG_CRITICAL(Audio_Sink, "OpenAL context mismatch: expected {:p}, got {:p}",
static_cast<void*>(context), static_cast<void*>(current_context));
is_dummy_stream = true;
LOG_WARNING(Audio_Sink, "Creating dummy audio stream due to context mismatch");
return;
}
// Log diagnostic information
const char* renderer = reinterpret_cast<const char*>(alGetString(AL_RENDERER));
const char* vendor = reinterpret_cast<const char*>(alGetString(AL_VENDOR));
if (renderer && vendor) {
LOG_DEBUG(Audio_Sink, "OpenAL renderer: {}, vendor: {}", renderer, vendor);
}
// Attempt to create source with multiple retries and better error handling
bool source_created = false;
for (int attempt = 0; attempt < 3 && !source_created; ++attempt) {
if (attempt > 0) {
LOG_WARNING(Audio_Sink, "OpenAL source creation attempt {} of 3", attempt + 1);
// Wait longer between retries
std::this_thread::sleep_for(std::chrono::milliseconds(50 * attempt));
// Try to clear and reset the context
alGetError(); // Clear any existing errors
alcMakeContextCurrent(nullptr);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (!alcMakeContextCurrent(context)) {
LOG_ERROR(Audio_Sink, "Failed to restore OpenAL context on attempt {}", attempt + 1);
continue;
}
}
alGenSources(1, &source);
ALenum error = alGetError();
if (error == AL_NO_ERROR) {
source_created = true;
if (attempt > 0) {
LOG_INFO(Audio_Sink, "OpenAL source creation succeeded on attempt {}", attempt + 1);
}
} else {
const char* error_str = "";
switch (error) {
case AL_INVALID_VALUE:
error_str = "AL_INVALID_VALUE";
break;
case AL_INVALID_OPERATION:
error_str = "AL_INVALID_OPERATION";
break;
case AL_OUT_OF_MEMORY:
error_str = "AL_OUT_OF_MEMORY";
break;
default:
error_str = "Unknown error";
break;
}
if (attempt == 2) {
LOG_CRITICAL(Audio_Sink, "Final attempt failed - Error creating OpenAL source: {} ({})", error_str, error);
LOG_CRITICAL(Audio_Sink, "This may indicate OpenAL driver issues or resource exhaustion");
LOG_WARNING(Audio_Sink, "Creating dummy audio stream to allow system to continue");
is_dummy_stream = true;
return;
} else {
LOG_WARNING(Audio_Sink, "Attempt {} failed - Error creating OpenAL source: {} ({})", attempt + 1, error_str, error);
}
}
}
if (!source_created) {
LOG_CRITICAL(Audio_Sink, "Failed to create OpenAL source after all attempts");
LOG_WARNING(Audio_Sink, "Creating dummy audio stream to allow system to continue");
is_dummy_stream = true;
return;
}
alGenBuffers(num_buffers, buffers.data());
ALenum error = alGetError();
if (error != AL_NO_ERROR) {
const char* error_str = "";
switch (error) {
case AL_INVALID_VALUE:
error_str = "AL_INVALID_VALUE";
break;
case AL_INVALID_OPERATION:
error_str = "AL_INVALID_OPERATION";
break;
case AL_OUT_OF_MEMORY:
error_str = "AL_OUT_OF_MEMORY";
break;
default:
error_str = "Unknown error";
break;
}
LOG_CRITICAL(Audio_Sink, "Error creating OpenAL buffers: {} ({})", error_str, error);
// Clean up the source we created
alDeleteSources(1, &source);
source = 0;
return;
}
// Set 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);
// Initialize buffers with silence
std::vector<s16> silence(TargetSampleCount * device_channels, 0);
for (auto& buffer : buffers) {
alBufferData(buffer, device_channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16,
silence.data(), static_cast<ALsizei>(silence.size() * sizeof(s16)), TargetSampleRate);
}
// Queue all buffers
alSourceQueueBuffers(source, num_buffers, buffers.data());
}
LOG_INFO(Service_Audio,
"Opening OpenAL stream with: rate {} channels {} (system channels {})",
TargetSampleRate, device_channels, system_channels);
}
/**
* Destroy the sink stream.
*/
~OpenALSinkStream() override {
LOG_DEBUG(Service_Audio, "Destructing OpenAL stream");
Finalize();
}
/**
* Finalize the sink stream.
*/
void Finalize() override {
if (is_dummy_stream) {
LOG_DEBUG(Audio_Sink, "Finalize called on dummy stream - ignoring");
return;
}
StopAudioThread();
if (type == StreamType::In) {
if (capture_device) {
if (is_playing) {
alcCaptureStop(capture_device);
}
alcCaptureCloseDevice(capture_device);
capture_device = nullptr;
}
} else {
if (source != 0) {
Stop();
alDeleteSources(1, &source);
source = 0;
}
if (buffers[0] != 0) {
alDeleteBuffers(num_buffers, buffers.data());
buffers.fill(0);
}
}
}
/**
* Start the sink stream.
*
* @param resume - Set to true if this is resuming the stream a previously-active stream.
* Default false.
*/
void Start(bool resume = false) override {
if (is_dummy_stream) {
LOG_DEBUG(Audio_Sink, "Start called on dummy stream - ignoring");
return;
}
if (paused) {
paused = false;
if (type == StreamType::In) {
if (capture_device) {
alcCaptureStart(capture_device);
is_playing = true;
}
} else {
if (source != 0) {
alSourcePlay(source);
is_playing = true;
}
}
StartAudioThread();
}
}
/**
* Stop the sink stream.
*/
void Stop() override {
if (is_dummy_stream) {
LOG_DEBUG(Audio_Sink, "Stop called on dummy stream - ignoring");
return;
}
if (!paused) {
SignalPause();
StopAudioThread();
if (type == StreamType::In) {
if (capture_device && is_playing) {
alcCaptureStop(capture_device);
is_playing = false;
}
} else {
if (source != 0 && is_playing) {
alSourceStop(source);
is_playing = false;
}
}
}
}
private:
/**
* Start the audio processing thread.
*/
void StartAudioThread() {
if (!audio_thread.joinable()) {
audio_thread = std::thread(&OpenALSinkStream::AudioThreadFunc, this);
}
}
/**
* Stop the audio processing thread.
*/
void StopAudioThread() {
if (audio_thread.joinable()) {
audio_thread.join();
}
}
/**
* Audio processing thread function.
*/
void AudioThreadFunc() {
if (is_dummy_stream) {
return; // No-op for dummy streams
}
while (is_playing && !paused) {
if (type == StreamType::In) {
ProcessInputAudio();
} else {
ProcessOutputAudio();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
/**
* Process audio data for output streams.
*/
void ProcessOutputAudio() {
if (is_dummy_stream || (type != StreamType::Out && type != StreamType::Render)) {
return;
}
// Check if any buffers have finished playing
ALint processed = 0;
alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);
while (processed > 0) {
ALuint buffer;
alSourceUnqueueBuffers(source, 1, &buffer);
// Prepare output buffer
const std::size_t num_frames = TargetSampleCount;
std::vector<s16> output_buffer(num_frames * device_channels);
// Get audio data from the system
std::span<s16> output_span{output_buffer.data(), output_buffer.size()};
ProcessAudioOutAndRender(output_span, num_frames);
// Fill the buffer with new data
alBufferData(buffer, device_channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16,
output_buffer.data(), static_cast<ALsizei>(output_buffer.size() * sizeof(s16)), TargetSampleRate);
// Queue the buffer back
alSourceQueueBuffers(source, 1, &buffer);
processed--;
}
// Make sure the source is playing
ALint state;
alGetSourcei(source, AL_SOURCE_STATE, &state);
if (state != AL_PLAYING && is_playing) {
alSourcePlay(source);
}
}
/**
* Process audio data for input streams.
*/
void ProcessInputAudio() {
if (is_dummy_stream || type != StreamType::In || !capture_device) {
return;
}
// Check how many samples are available
ALint samples_available = 0;
alcGetIntegerv(capture_device, ALC_CAPTURE_SAMPLES, 1, &samples_available);
const std::size_t num_frames = TargetSampleCount;
if (samples_available >= static_cast<ALint>(num_frames)) {
// Capture the audio data
std::vector<s16> capture_buffer(num_frames * device_channels);
alcCaptureSamples(capture_device, capture_buffer.data(), static_cast<ALCsizei>(num_frames));
// Process the captured data
std::span<const s16> captured_span{capture_buffer.data(), capture_buffer.size()};
ProcessAudioIn(captured_span, num_frames);
}
}
/// OpenAL device
ALCdevice* device{};
/// OpenAL context
ALCcontext* context{};
/// OpenAL capture device (for input streams)
ALCdevice* capture_device{};
/// OpenAL source
ALuint source{0};
/// OpenAL buffers
static constexpr size_t num_buffers = 4;
std::array<ALuint, num_buffers> buffers{};
/// Whether the stream is currently playing
bool is_playing{false};
/// Audio processing thread
std::thread audio_thread;
/// Whether this is a dummy stream
bool is_dummy_stream{false};
};
OpenALSink::OpenALSink(std::string_view target_device_name) {
// Log OpenAL version and available extensions
LOG_INFO(Audio_Sink, "Initializing OpenAL sink...");
// Check for device enumeration extensions
if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) {
LOG_INFO(Audio_Sink, "OpenAL ALC_ENUMERATE_ALL_EXT extension available");
} else if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) {
LOG_INFO(Audio_Sink, "OpenAL ALC_ENUMERATION_EXT extension available");
} else {
LOG_WARNING(Audio_Sink, "OpenAL device enumeration extensions not available");
}
// Initialize OpenAL
const char* device_name = target_device_name.empty() ? nullptr : target_device_name.data();
// Try to get a better device name if using default
if (!device_name && alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) {
device_name = alcGetString(nullptr, ALC_DEFAULT_ALL_DEVICES_SPECIFIER);
if (device_name) {
LOG_INFO(Audio_Sink, "Using default device: {}", device_name);
}
}
device = alcOpenDevice(device_name);
if (!device) {
ALenum error = alcGetError(nullptr);
const char* error_str = "";
switch (error) {
case ALC_INVALID_DEVICE:
error_str = "ALC_INVALID_DEVICE";
break;
case ALC_INVALID_CONTEXT:
error_str = "ALC_INVALID_CONTEXT";
break;
case ALC_INVALID_VALUE:
error_str = "ALC_INVALID_VALUE";
break;
case ALC_OUT_OF_MEMORY:
error_str = "ALC_OUT_OF_MEMORY";
break;
default:
error_str = "Unknown error";
break;
}
LOG_CRITICAL(Audio_Sink, "Failed to open OpenAL device '{}': {} ({})",
target_device_name.empty() ? "default" : target_device_name, error_str, error);
return;
}
// Create context with attributes for better compatibility
ALCint context_attributes[] = {
ALC_FREQUENCY, TargetSampleRate,
ALC_REFRESH, 50, // 50Hz refresh rate
ALC_SYNC, ALC_FALSE,
0 // Null terminator
};
context = alcCreateContext(static_cast<ALCdevice*>(device), context_attributes);
if (!context) {
ALenum error = alcGetError(static_cast<ALCdevice*>(device));
const char* error_str = "";
switch (error) {
case ALC_INVALID_DEVICE:
error_str = "ALC_INVALID_DEVICE";
break;
case ALC_INVALID_CONTEXT:
error_str = "ALC_INVALID_CONTEXT";
break;
case ALC_INVALID_VALUE:
error_str = "ALC_INVALID_VALUE";
break;
case ALC_OUT_OF_MEMORY:
error_str = "ALC_OUT_OF_MEMORY";
break;
default:
error_str = "Unknown error";
break;
}
LOG_CRITICAL(Audio_Sink, "Failed to create OpenAL context: {} ({})", error_str, error);
alcCloseDevice(static_cast<ALCdevice*>(device));
device = nullptr;
return;
}
if (!alcMakeContextCurrent(static_cast<ALCcontext*>(context))) {
ALenum error = alcGetError(static_cast<ALCdevice*>(device));
const char* error_str = "";
switch (error) {
case ALC_INVALID_DEVICE:
error_str = "ALC_INVALID_DEVICE";
break;
case ALC_INVALID_CONTEXT:
error_str = "ALC_INVALID_CONTEXT";
break;
case ALC_INVALID_VALUE:
error_str = "ALC_INVALID_VALUE";
break;
default:
error_str = "Unknown error";
break;
}
LOG_CRITICAL(Audio_Sink, "Failed to make OpenAL context current: {} ({})", error_str, error);
alcDestroyContext(static_cast<ALCcontext*>(context));
alcCloseDevice(static_cast<ALCdevice*>(device));
context = nullptr;
device = nullptr;
return;
}
// Set device name
if (!target_device_name.empty()) {
output_device = target_device_name;
} else {
const char* default_device = alcGetString(static_cast<ALCdevice*>(device), ALC_DEVICE_SPECIFIER);
if (default_device) {
output_device = default_device;
}
}
// Get device capabilities
device_channels = 2; // OpenAL typically supports stereo output
// Log OpenAL implementation details
const char* al_version = reinterpret_cast<const char*>(alGetString(AL_VERSION));
const char* al_renderer = reinterpret_cast<const char*>(alGetString(AL_RENDERER));
const char* al_vendor = reinterpret_cast<const char*>(alGetString(AL_VENDOR));
const char* al_extensions = reinterpret_cast<const char*>(alGetString(AL_EXTENSIONS));
LOG_INFO(Audio_Sink, "OpenAL implementation details:");
LOG_INFO(Audio_Sink, " Version: {}", al_version ? al_version : "Unknown");
LOG_INFO(Audio_Sink, " Renderer: {}", al_renderer ? al_renderer : "Unknown");
LOG_INFO(Audio_Sink, " Vendor: {}", al_vendor ? al_vendor : "Unknown");
LOG_INFO(Audio_Sink, " Device: {}", output_device);
// Check for important extensions
if (al_extensions) {
std::string extensions_str(al_extensions);
LOG_DEBUG(Audio_Sink, " Extensions: {}", extensions_str);
if (extensions_str.find("AL_SOFT_direct_channels") != std::string::npos) {
LOG_INFO(Audio_Sink, " AL_SOFT_direct_channels extension available");
}
if (extensions_str.find("AL_SOFT_source_latency") != std::string::npos) {
LOG_INFO(Audio_Sink, " AL_SOFT_source_latency extension available");
}
}
LOG_INFO(Audio_Sink, "OpenAL sink initialized successfully with device: {}", output_device);
}
OpenALSink::~OpenALSink() {
CloseStreams();
if (context) {
alcMakeContextCurrent(nullptr);
alcDestroyContext(static_cast<ALCcontext*>(context));
}
if (device) {
alcCloseDevice(static_cast<ALCdevice*>(device));
}
}
SinkStream* OpenALSink::AcquireSinkStream(Core::System& system, u32 system_channels_,
const std::string&, StreamType type) {
if (!device || !context) {
LOG_ERROR(Audio_Sink, "Cannot create sink stream - OpenAL device or context is null");
return nullptr;
}
// Limit the number of concurrent streams to avoid resource exhaustion
constexpr size_t max_streams = 8;
if (sink_streams.size() >= max_streams) {
LOG_WARNING(Audio_Sink, "Maximum number of OpenAL streams ({}) reached, cannot create more", max_streams);
return nullptr;
}
// Ensure context is current before creating streams
if (!alcMakeContextCurrent(static_cast<ALCcontext*>(context))) {
LOG_ERROR(Audio_Sink, "Failed to make OpenAL context current before creating stream");
return nullptr;
}
system_channels = system_channels_;
// Add some delay between stream creations to avoid resource conflicts
if (!sink_streams.empty()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<OpenALSinkStream>(
device_channels, system_channels, output_device, input_device, type, system,
static_cast<ALCdevice*>(device), static_cast<ALCcontext*>(context)));
return stream.get();
}
void OpenALSink::CloseStream(SinkStream* stream) {
for (size_t i = 0; i < sink_streams.size(); i++) {
if (sink_streams[i].get() == stream) {
sink_streams[i].reset();
sink_streams.erase(sink_streams.begin() + i);
break;
}
}
}
void OpenALSink::CloseStreams() {
sink_streams.clear();
}
f32 OpenALSink::GetDeviceVolume() const {
if (sink_streams.empty()) {
return 1.0f;
}
return sink_streams[0]->GetDeviceVolume();
}
void OpenALSink::SetDeviceVolume(f32 volume) {
for (auto& stream : sink_streams) {
stream->SetDeviceVolume(volume);
}
}
void OpenALSink::SetSystemVolume(f32 volume) {
for (auto& stream : sink_streams) {
stream->SetSystemVolume(volume);
}
}
std::vector<std::string> ListOpenALSinkDevices(bool capture) {
std::vector<std::string> device_list;
if (capture) {
// List capture devices
if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) {
// Use the newer extension for better device names
const char* devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
if (devices) {
while (*devices) {
device_list.emplace_back(devices);
devices += strlen(devices) + 1;
}
}
}
// Fallback to older enumeration
if (device_list.empty()) {
const char* devices = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER);
if (devices) {
while (*devices) {
device_list.emplace_back(devices);
devices += strlen(devices) + 1;
}
}
}
} else {
// List playback devices
if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) {
// Use the newer extension for better device names
const char* devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
if (devices) {
while (*devices) {
device_list.emplace_back(devices);
devices += strlen(devices) + 1;
}
}
}
// Fallback to older enumeration if the extension isn't available
if (device_list.empty() && alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) {
const char* devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER);
if (devices) {
while (*devices) {
device_list.emplace_back(devices);
devices += strlen(devices) + 1;
}
}
}
// Last resort fallback
if (device_list.empty()) {
const char* devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER);
if (devices) {
while (*devices) {
device_list.emplace_back(devices);
devices += strlen(devices) + 1;
}
}
}
}
// Log the devices we found for debugging
LOG_INFO(Audio_Sink, "OpenAL {} devices found: {}", capture ? "capture" : "playback", device_list.size());
for (size_t i = 0; i < device_list.size(); ++i) {
LOG_INFO(Audio_Sink, " {}: {}", i, device_list[i]);
}
if (device_list.empty()) {
LOG_WARNING(Audio_Sink, "No OpenAL {} devices found, using default", capture ? "capture" : "playback");
device_list.emplace_back("Default");
}
return device_list;
}
bool IsOpenALSuitable() {
// Try to initialize OpenAL to check if it's available
ALCdevice* test_device = alcOpenDevice(nullptr);
if (!test_device) {
LOG_ERROR(Audio_Sink, "OpenAL not suitable - failed to open default device");
return false;
}
ALCcontext* test_context = alcCreateContext(test_device, nullptr);
if (!test_context) {
LOG_ERROR(Audio_Sink, "OpenAL not suitable - failed to create context");
alcCloseDevice(test_device);
return false;
}
// Try to make the context current
if (!alcMakeContextCurrent(test_context)) {
LOG_ERROR(Audio_Sink, "OpenAL not suitable - failed to make context current");
alcDestroyContext(test_context);
alcCloseDevice(test_device);
return false;
}
// Try to create a test source to verify functionality
ALuint test_source = 0;
alGenSources(1, &test_source);
ALenum error = alGetError();
bool suitable = (error == AL_NO_ERROR && test_source != 0);
if (suitable) {
alDeleteSources(1, &test_source);
LOG_INFO(Audio_Sink, "OpenAL is suitable for use");
} else {
LOG_ERROR(Audio_Sink, "OpenAL not suitable - failed to create test source (error: {})", error);
}
// Clean up
alcMakeContextCurrent(nullptr);
alcDestroyContext(test_context);
alcCloseDevice(test_device);
return suitable;
}
} // namespace AudioCore::Sink

View File

@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include <vector>
#include "audio_core/sink/sink.h"
namespace Core {
class System;
}
namespace AudioCore::Sink {
class SinkStream;
/**
* OpenAL backend sink, holds multiple output streams and is responsible for sinking samples to
* hardware. Used by Audio Render, Audio In and Audio Out.
*/
class OpenALSink final : public Sink {
public:
explicit OpenALSink(std::string_view device_id);
~OpenALSink() override;
/**
* Create a new sink stream.
*
* @param system - Core system.
* @param system_channels - Number of channels the audio system expects.
* May differ from the device's channel count.
* @param name - Name of this stream.
* @param type - Type of this stream, render/in/out.
*
* @return A pointer to the created SinkStream
*/
SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels,
const std::string& name, StreamType type) override;
/**
* Close a given stream.
*
* @param stream - The stream to close.
*/
void CloseStream(SinkStream* stream) override;
/**
* Close all streams.
*/
void CloseStreams() override;
/**
* Get the device volume. Set from calls to the IAudioDevice service.
*
* @return Volume of the device.
*/
f32 GetDeviceVolume() const override;
/**
* Set the device volume. Set from calls to the IAudioDevice service.
*
* @param volume - New volume of the device.
*/
void SetDeviceVolume(f32 volume) override;
/**
* Set the system volume. Comes from the audio system using this stream.
*
* @param volume - New volume of the system.
*/
void SetSystemVolume(f32 volume) override;
private:
/// OpenAL device
void* device{};
/// OpenAL context
void* context{};
/// Device channels
u32 device_channels{2};
/// System channels
u32 system_channels{2};
/// Output device name
std::string output_device{};
/// Input device name
std::string input_device{};
/// Vector of streams managed by this sink
std::vector<SinkStreamPtr> sink_streams{};
};
/**
* Get a list of connected devices from OpenAL.
*
* @param capture - Return input (capture) devices if true, otherwise output devices.
*/
std::vector<std::string> ListOpenALSinkDevices(bool capture);
/**
* Check if this backend is suitable for use.
* Checks if enabled, its latency, whether it opens successfully, etc.
*
* @return True is this backend is suitable, false otherwise.
*/
bool IsOpenALSuitable();
} // namespace AudioCore::Sink

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@@ -16,6 +17,9 @@
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
#include "audio_core/sink/sdl2_sink.h" #include "audio_core/sink/sdl2_sink.h"
#endif #endif
#ifdef HAVE_OPENAL
#include "audio_core/sink/openal_sink.h"
#endif
#include "audio_core/sink/null_sink.h" #include "audio_core/sink/null_sink.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/settings_enums.h" #include "common/settings_enums.h"
@@ -68,6 +72,16 @@ constexpr SinkDetails sink_details[] = {
&ListSDLSinkDevices, &ListSDLSinkDevices,
&IsSDLSuitable, &IsSDLSuitable,
}, },
#endif
#ifdef HAVE_OPENAL
SinkDetails{
Settings::AudioEngine::OpenAL,
[](std::string_view device_id) -> std::unique_ptr<Sink> {
return std::make_unique<OpenALSink>(device_id);
},
&ListOpenALSinkDevices,
&IsOpenALSuitable,
},
#endif #endif
SinkDetails{ SinkDetails{
Settings::AudioEngine::Null, Settings::AudioEngine::Null,

View File

@@ -82,6 +82,7 @@ enum class AudioEngine : u32 {
Auto, Auto,
Cubeb, Cubeb,
Sdl2, Sdl2,
OpenAL,
Null, Null,
Oboe, Oboe,
}; };
@@ -91,7 +92,7 @@ inline std::vector<std::pair<std::string, AudioEngine>>
EnumMetadata<AudioEngine>::Canonicalizations() { EnumMetadata<AudioEngine>::Canonicalizations() {
return { return {
{"auto", AudioEngine::Auto}, {"cubeb", AudioEngine::Cubeb}, {"sdl2", AudioEngine::Sdl2}, {"auto", AudioEngine::Auto}, {"cubeb", AudioEngine::Cubeb}, {"sdl2", AudioEngine::Sdl2},
{"null", AudioEngine::Null}, {"oboe", AudioEngine::Oboe}, {"openal", AudioEngine::OpenAL}, {"null", AudioEngine::Null}, {"oboe", AudioEngine::Oboe},
}; };
} }

View File

@@ -28,6 +28,7 @@
"fmt", "fmt",
"lz4", "lz4",
"nlohmann-json", "nlohmann-json",
"openal-soft",
"zlib", "zlib",
"zstd" "zstd"
], ],