mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-04 09:28:34 -04:00
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:
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user