feat: add SDL audio backend for Linux runtime

Add a native SDL3 audio driver for non-Windows hosts and gate WASAPI to Windows-only builds, so Linux builds no longer depend on Windows headers while keeping runtime telemetry and queue behavior intact.

Made-with: Cursor
This commit is contained in:
salh
2026-04-17 22:39:55 +03:00
parent 0b14603b56
commit a96d70d51a
4 changed files with 380 additions and 4 deletions
@@ -0,0 +1,62 @@
// Native audio runtime
// Part of the AC6 Recompilation native foundation
#pragma once
#include <array>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <queue>
#include <stack>
#include <SDL3/SDL_audio.h>
#include <native/audio/audio_driver.h>
namespace rex::audio {
class AudioRuntime;
}
namespace rex::audio::sdl {
class SdlAudioDriver : public AudioDriver {
public:
SdlAudioDriver(memory::Memory* memory, AudioRuntime* runtime, size_t client_index);
~SdlAudioDriver() override;
bool Initialize() override;
void Shutdown() override;
void SubmitFrame(uint32_t frame_ptr) override;
void SubmitSilenceFrame() override;
AudioDriverTelemetry GetTelemetry() const override;
const char* backend_name() const override { return "sdl"; }
uint32_t queue_low_water_frames() const override { return 2; }
uint32_t queue_target_frames() const override { return 3; }
private:
static void SDLCALL StreamCallback(void* userdata, SDL_AudioStream* stream,
int additional_amount, int total_amount);
void FillStream(SDL_AudioStream* stream, int bytes_needed);
AudioRuntime* runtime_{nullptr};
size_t client_index_{0};
SDL_AudioStream* stream_{nullptr};
std::mutex frames_mutex_{};
std::queue<float*> frames_queued_{};
std::stack<float*> frames_unused_{};
std::array<float, kRenderDriverTicSamplesPerFrame * 2> pending_output_frame_{};
size_t pending_output_float_count_{0};
size_t pending_output_float_offset_{0};
std::atomic<bool> shutting_down_{false};
std::atomic<uint32_t> submitted_frames_{0};
std::atomic<uint32_t> consumed_frames_{0};
std::atomic<uint32_t> underrun_count_{0};
std::atomic<uint32_t> silence_injections_{0};
std::atomic<uint32_t> queued_depth_{0};
std::atomic<uint32_t> peak_queued_depth_{0};
};
} // namespace rex::audio::sdl
+18 -3
View File
@@ -1,4 +1,4 @@
# rexaudio - Native audio subsystem library (WASAPI-only, Windows target)
# rexaudio - Native audio subsystem library
add_library(rexaudio STATIC
audio_driver.cpp
@@ -6,7 +6,6 @@ add_library(rexaudio STATIC
audio_runtime.cpp
audio_system.cpp
audio_trace.cpp
wasapi/wasapi_audio_driver.cpp
xma/context.cpp
xma/decoder.cpp
xma/register_file.cpp
@@ -14,11 +13,27 @@ add_library(rexaudio STATIC
)
add_library(rex::audio ALIAS rexaudio)
if(WIN32)
target_sources(rexaudio PRIVATE
wasapi/wasapi_audio_driver.cpp
)
else()
target_sources(rexaudio PRIVATE
sdl/sdl_audio_driver.cpp
)
endif()
target_include_directories(rexaudio PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(rexaudio
PUBLIC rexcore libavcodec libavutil ole32 avrt uuid
PUBLIC rexcore libavcodec libavutil
)
if(WIN32)
target_link_libraries(rexaudio PUBLIC ole32 avrt uuid)
else()
target_link_libraries(rexaudio PUBLIC SDL3::SDL3)
endif()
+17 -1
View File
@@ -12,14 +12,23 @@
#include <native/audio/audio_runtime.h>
#if defined(_WIN32)
#include <native/audio/wasapi/wasapi_audio_driver.h>
#else
#include <native/audio/sdl/sdl_audio_driver.h>
#endif
#include <rex/cvar.h>
#include <rex/memory.h>
#include <rex/ppc/exceptions.h>
#include <native/stream.h>
#if defined(_WIN32)
REXCVAR_DEFINE_STRING(audio_backend, "wasapi", "Audio", "Audio backend: wasapi")
.allowed({"wasapi"})
.allowed({"wasapi", "sdl"})
#else
REXCVAR_DEFINE_STRING(audio_backend, "sdl", "Audio", "Audio backend: sdl")
.allowed({"sdl"})
#endif
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_INT32(audio_max_queue_depth, 8, "Audio",
"Maximum queued render-driver frames per client");
@@ -41,7 +50,14 @@ namespace {
std::unique_ptr<AudioDriver> CreateConfiguredDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index) {
#if defined(_WIN32)
if (REXCVAR_GET(audio_backend) == "sdl") {
return std::make_unique<sdl::SdlAudioDriver>(memory, runtime, client_index);
}
return std::make_unique<wasapi::WasapiAudioDriver>(memory, runtime, client_index);
#else
return std::make_unique<sdl::SdlAudioDriver>(memory, runtime, client_index);
#endif
}
uint32_t QueueDepthLimit() {
@@ -0,0 +1,283 @@
/**
******************************************************************************
* ReXGlue native SDL audio driver *
******************************************************************************
*/
#include <native/audio/sdl/sdl_audio_driver.h>
#include <algorithm>
#include <cstring>
#include <SDL3/SDL_init.h>
#include <native/audio/audio_runtime.h>
#include <native/audio/conversion.h>
#include <rex/cvar.h>
#include <rex/logging.h>
REXCVAR_DECLARE(bool, audio_mute);
REXCVAR_DECLARE(bool, audio_trace_render_driver_verbose);
namespace rex::audio::sdl {
namespace {
constexpr int kOutputChannels = 2;
constexpr int kOutputFrameBytes =
static_cast<int>(kRenderDriverTicSamplesPerFrame * kOutputChannels * sizeof(float));
} // namespace
SdlAudioDriver::SdlAudioDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index)
: AudioDriver(memory), runtime_(runtime), client_index_(client_index) {}
SdlAudioDriver::~SdlAudioDriver() {
Shutdown();
}
bool SdlAudioDriver::Initialize() {
shutting_down_.store(false, std::memory_order_release);
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
REXAPU_ERROR("SdlAudioDriver init failed for client {}: SDL_InitSubSystem: {}",
client_index_, SDL_GetError());
return false;
}
SDL_AudioSpec spec{};
spec.format = SDL_AUDIO_F32;
spec.channels = kOutputChannels;
spec.freq = kAudioFrameSampleRate;
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec,
&SdlAudioDriver::StreamCallback, this);
if (!stream_) {
REXAPU_ERROR("SdlAudioDriver init failed for client {}: SDL_OpenAudioDeviceStream: {}",
client_index_, SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
if (!SDL_ResumeAudioStreamDevice(stream_)) {
REXAPU_ERROR("SdlAudioDriver init failed for client {}: SDL_ResumeAudioStreamDevice: {}",
client_index_, SDL_GetError());
SDL_DestroyAudioStream(stream_);
stream_ = nullptr;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
REXAPU_INFO("SdlAudioDriver initialized: client={} channels={} freq={}", client_index_,
kOutputChannels, kAudioFrameSampleRate);
return true;
}
void SdlAudioDriver::Shutdown() {
const bool was_shutting_down = shutting_down_.exchange(true, std::memory_order_acq_rel);
if (was_shutting_down) {
return;
}
if (stream_) {
SDL_DestroyAudioStream(stream_);
stream_ = nullptr;
}
std::lock_guard<std::mutex> guard(frames_mutex_);
while (!frames_unused_.empty()) {
delete[] frames_unused_.top();
frames_unused_.pop();
}
while (!frames_queued_.empty()) {
delete[] frames_queued_.front();
frames_queued_.pop();
}
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
void SdlAudioDriver::SubmitFrame(const uint32_t frame_ptr) {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
const auto input_frame = memory_->TranslateVirtual<float*>(frame_ptr);
if (!input_frame) {
return;
}
float* output_frame = nullptr;
{
std::lock_guard<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[kAudioFrameTotalSamples];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::memcpy(output_frame, input_frame, sizeof(float) * kAudioFrameTotalSamples);
{
std::lock_guard<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"SdlAudioDriver::SubmitFrame frame_ptr={:08X} submitted={} consumed={} queued_depth={} peak={} underruns={}",
frame_ptr, submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed));
}
}
void SdlAudioDriver::SubmitSilenceFrame() {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
float* output_frame = nullptr;
{
std::lock_guard<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[kAudioFrameTotalSamples];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::fill_n(output_frame, kAudioFrameTotalSamples, 0.0f);
{
std::lock_guard<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
silence_injections_.fetch_add(1, std::memory_order_relaxed);
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"SdlAudioDriver::SubmitSilenceFrame submitted={} consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed));
}
}
AudioDriverTelemetry SdlAudioDriver::GetTelemetry() const {
return AudioDriverTelemetry{
submitted_frames_.load(std::memory_order_relaxed),
consumed_frames_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed),
queued_depth_.load(std::memory_order_relaxed),
peak_queued_depth_.load(std::memory_order_relaxed),
};
}
void SDLCALL SdlAudioDriver::StreamCallback(void* userdata, SDL_AudioStream* stream,
const int additional_amount, const int total_amount) {
(void)total_amount;
auto* driver = static_cast<SdlAudioDriver*>(userdata);
if (!driver || driver->shutting_down_.load(std::memory_order_acquire) || additional_amount <= 0) {
return;
}
driver->FillStream(stream, additional_amount);
}
void SdlAudioDriver::FillStream(SDL_AudioStream* stream, int bytes_needed) {
while (bytes_needed > 0 && !shutting_down_.load(std::memory_order_acquire)) {
if (pending_output_float_offset_ == pending_output_float_count_) {
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
float* buffer = nullptr;
{
std::lock_guard<std::mutex> guard(frames_mutex_);
if (!frames_queued_.empty()) {
buffer = frames_queued_.front();
frames_queued_.pop();
queued_depth_.fetch_sub(1, std::memory_order_relaxed);
}
}
if (buffer) {
if (!REXCVAR_GET(audio_mute)) {
conversion::sequential_6_BE_to_interleaved_2_LE(
pending_output_frame_.data(), buffer, kRenderDriverTicSamplesPerFrame);
} else {
std::memset(pending_output_frame_.data(), 0, sizeof(float) * pending_output_frame_.size());
}
pending_output_float_count_ = pending_output_frame_.size();
{
std::lock_guard<std::mutex> guard(frames_mutex_);
frames_unused_.push(buffer);
}
}
}
if (pending_output_float_count_ == 0) {
std::array<float, kRenderDriverTicSamplesPerFrame * kOutputChannels> silence{};
if (!SDL_PutAudioStreamData(stream, silence.data(), kOutputFrameBytes)) {
return;
}
++underrun_count_;
++silence_injections_;
bytes_needed -= std::min(bytes_needed, kOutputFrameBytes);
continue;
}
const size_t pending_bytes =
(pending_output_float_count_ - pending_output_float_offset_) * sizeof(float);
const int write_bytes = static_cast<int>(
std::min<size_t>(pending_bytes, static_cast<size_t>(std::max(bytes_needed, 0))));
if (write_bytes <= 0) {
break;
}
const auto* write_ptr = reinterpret_cast<const uint8_t*>(pending_output_frame_.data()) +
(pending_output_float_offset_ * sizeof(float));
if (!SDL_PutAudioStreamData(stream, write_ptr, write_bytes)) {
return;
}
pending_output_float_offset_ += static_cast<size_t>(write_bytes) / sizeof(float);
bytes_needed -= write_bytes;
if (pending_output_float_offset_ == pending_output_float_count_) {
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
consumed_frames_.fetch_add(1, std::memory_order_relaxed);
if (runtime_) {
runtime_->ReportSamplesConsumedForClient(client_index_, kRenderDriverTicSamplesPerFrame);
runtime_->ConsumeQueuedFramesForClient(client_index_, 1);
runtime_->WakeWorker();
}
}
}
}
} // namespace rex::audio::sdl