mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 18:53:32 +00:00
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:
@@ -1,4 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||
# SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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_OPENAL "Enables the OpenAL audio backend" ON)
|
||||
|
||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||
|
||||
option(CITRON_TESTS "Compile tests" "${BUILD_TESTING}")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||
# SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
add_library(audio_core STATIC
|
||||
@@ -251,6 +252,17 @@ if (ENABLE_SDL2)
|
||||
target_compile_definitions(audio_core PRIVATE HAVE_SDL2)
|
||||
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)
|
||||
target_sources(audio_core PRIVATE
|
||||
sink/oboe_sink.cpp
|
||||
|
||||
775
src/audio_core/sink/openal_sink.cpp
Normal file
775
src/audio_core/sink/openal_sink.cpp
Normal 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
|
||||
106
src/audio_core/sink/openal_sink.h
Normal file
106
src/audio_core/sink/openal_sink.h
Normal 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
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
@@ -16,6 +17,9 @@
|
||||
#ifdef HAVE_SDL2
|
||||
#include "audio_core/sink/sdl2_sink.h"
|
||||
#endif
|
||||
#ifdef HAVE_OPENAL
|
||||
#include "audio_core/sink/openal_sink.h"
|
||||
#endif
|
||||
#include "audio_core/sink/null_sink.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings_enums.h"
|
||||
@@ -68,6 +72,16 @@ constexpr SinkDetails sink_details[] = {
|
||||
&ListSDLSinkDevices,
|
||||
&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
|
||||
SinkDetails{
|
||||
Settings::AudioEngine::Null,
|
||||
|
||||
@@ -82,6 +82,7 @@ enum class AudioEngine : u32 {
|
||||
Auto,
|
||||
Cubeb,
|
||||
Sdl2,
|
||||
OpenAL,
|
||||
Null,
|
||||
Oboe,
|
||||
};
|
||||
@@ -91,7 +92,7 @@ inline std::vector<std::pair<std::string, AudioEngine>>
|
||||
EnumMetadata<AudioEngine>::Canonicalizations() {
|
||||
return {
|
||||
{"auto", AudioEngine::Auto}, {"cubeb", AudioEngine::Cubeb}, {"sdl2", AudioEngine::Sdl2},
|
||||
{"null", AudioEngine::Null}, {"oboe", AudioEngine::Oboe},
|
||||
{"openal", AudioEngine::OpenAL}, {"null", AudioEngine::Null}, {"oboe", AudioEngine::Oboe},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"fmt",
|
||||
"lz4",
|
||||
"nlohmann-json",
|
||||
"openal-soft",
|
||||
"zlib",
|
||||
"zstd"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user