mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-04-17 18:20:45 -04:00
Merge branch 'openal-audio-implementation' into 'master'
audio_core: Add OpenAL audio backend support See merge request citron/rewrite!22
This commit is contained in:
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
@@ -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},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"fmt",
|
"fmt",
|
||||||
"lz4",
|
"lz4",
|
||||||
"nlohmann-json",
|
"nlohmann-json",
|
||||||
|
"openal-soft",
|
||||||
"zlib",
|
"zlib",
|
||||||
"zstd"
|
"zstd"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user