audio: Accurate audio output timing. (#1986)

* audio: Accurate audio output timing.

* audio: Handle SDL audio queue stalls.

* audio: Format info cleanup.
This commit is contained in:
squidbus 2024-12-31 02:38:52 -08:00 committed by GitHub
parent 927dc6d95c
commit 48c51bd9ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 170 additions and 821 deletions

View file

@ -9,6 +9,8 @@
#include "common/assert.h"
#include "common/config.h"
#include "common/logging/log.h"
#include "common/polyfill_thread.h"
#include "common/thread.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
#include "core/libraries/audio/audioout_error.h"
@ -21,111 +23,28 @@ std::array<PortOut, SCE_AUDIO_OUT_NUM_PORTS> ports_out{};
static std::unique_ptr<AudioOutBackend> audio;
static std::string_view GetAudioOutPort(OrbisAudioOutPort port) {
switch (port) {
case OrbisAudioOutPort::Main:
return "MAIN";
case OrbisAudioOutPort::Bgm:
return "BGM";
case OrbisAudioOutPort::Voice:
return "VOICE";
case OrbisAudioOutPort::Personal:
return "PERSONAL";
case OrbisAudioOutPort::Padspk:
return "PADSPK";
case OrbisAudioOutPort::Aux:
return "AUX";
default:
return "INVALID";
}
}
static std::string_view GetAudioOutParamFormat(OrbisAudioOutParamFormat param) {
switch (param) {
case OrbisAudioOutParamFormat::S16Mono:
return "S16_MONO";
case OrbisAudioOutParamFormat::S16Stereo:
return "S16_STEREO";
case OrbisAudioOutParamFormat::S16_8CH:
return "S16_8CH";
case OrbisAudioOutParamFormat::FloatMono:
return "FLOAT_MONO";
case OrbisAudioOutParamFormat::FloatStereo:
return "FLOAT_STEREO";
case OrbisAudioOutParamFormat::Float_8CH:
return "FLOAT_8CH";
case OrbisAudioOutParamFormat::S16_8CH_Std:
return "S16_8CH_STD";
case OrbisAudioOutParamFormat::Float_8CH_Std:
return "FLOAT_8CH_STD";
default:
return "INVALID";
}
}
static std::string_view GetAudioOutParamAttr(OrbisAudioOutParamAttr attr) {
switch (attr) {
case OrbisAudioOutParamAttr::None:
return "NONE";
case OrbisAudioOutParamAttr::Restricted:
return "RESTRICTED";
case OrbisAudioOutParamAttr::MixToMain:
return "MIX_TO_MAIN";
default:
return "INVALID";
}
}
static bool IsFormatFloat(const OrbisAudioOutParamFormat format) {
switch (format) {
case OrbisAudioOutParamFormat::S16Mono:
case OrbisAudioOutParamFormat::S16Stereo:
case OrbisAudioOutParamFormat::S16_8CH:
case OrbisAudioOutParamFormat::S16_8CH_Std:
return false;
case OrbisAudioOutParamFormat::FloatMono:
case OrbisAudioOutParamFormat::FloatStereo:
case OrbisAudioOutParamFormat::Float_8CH:
case OrbisAudioOutParamFormat::Float_8CH_Std:
return true;
default:
UNREACHABLE_MSG("Unknown format");
}
}
static u8 GetFormatNumChannels(const OrbisAudioOutParamFormat format) {
switch (format) {
case OrbisAudioOutParamFormat::S16Mono:
case OrbisAudioOutParamFormat::FloatMono:
return 1;
case OrbisAudioOutParamFormat::S16Stereo:
case OrbisAudioOutParamFormat::FloatStereo:
return 2;
case OrbisAudioOutParamFormat::S16_8CH:
case OrbisAudioOutParamFormat::Float_8CH:
case OrbisAudioOutParamFormat::S16_8CH_Std:
case OrbisAudioOutParamFormat::Float_8CH_Std:
return 8;
default:
UNREACHABLE_MSG("Unknown format");
}
}
static u8 GetFormatSampleSize(const OrbisAudioOutParamFormat format) {
switch (format) {
case OrbisAudioOutParamFormat::S16Mono:
case OrbisAudioOutParamFormat::S16Stereo:
case OrbisAudioOutParamFormat::S16_8CH:
case OrbisAudioOutParamFormat::S16_8CH_Std:
return 2;
case OrbisAudioOutParamFormat::FloatMono:
case OrbisAudioOutParamFormat::FloatStereo:
case OrbisAudioOutParamFormat::Float_8CH:
case OrbisAudioOutParamFormat::Float_8CH_Std:
return 4;
default:
UNREACHABLE_MSG("Unknown format");
}
static AudioFormatInfo GetFormatInfo(const OrbisAudioOutParamFormat format) {
static constexpr std::array<AudioFormatInfo, 8> format_infos = {{
// S16Mono
{false, 2, 1, {0}},
// S16Stereo
{false, 2, 2, {0, 1}},
// S16_8CH
{false, 2, 8, {0, 1, 2, 3, 4, 5, 6, 7}},
// FloatMono
{true, 4, 1, {0}},
// FloatStereo
{true, 4, 2, {0, 1}},
// Float_8CH
{true, 4, 8, {0, 1, 2, 3, 4, 5, 6, 7}},
// S16_8CH_Std
{false, 2, 8, {0, 1, 2, 3, 6, 7, 4, 5}},
// Float_8CH_Std
{true, 4, 8, {0, 1, 2, 3, 6, 7, 4, 5}},
}};
const auto index = static_cast<u32>(format);
ASSERT_MSG(index < format_infos.size(), "Unknown audio format {}", index);
return format_infos[index];
}
int PS4_SYSV_ABI sceAudioOutDeviceIdOpen() {
@ -180,6 +99,10 @@ int PS4_SYSV_ABI sceAudioOutClose(s32 handle) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
port.output_thread.Stop();
std::free(port.output_buffer);
port.output_buffer = nullptr;
port.output_ready = false;
port.impl = nullptr;
return ORBIS_OK;
}
@ -263,7 +186,7 @@ int PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* sta
case OrbisAudioOutPort::Bgm:
case OrbisAudioOutPort::Voice:
state->output = 1;
state->channel = port.channels_num > 2 ? 2 : port.channels_num;
state->channel = port.format_info.num_channels > 2 ? 2 : port.format_info.num_channels;
break;
case OrbisAudioOutPort::Personal:
case OrbisAudioOutPort::Padspk:
@ -311,16 +234,7 @@ int PS4_SYSV_ABI sceAudioOutInit() {
if (audio != nullptr) {
return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT;
}
const auto backend = Config::getAudioBackend();
if (backend == "cubeb") {
audio = std::make_unique<CubebAudioOut>();
} else if (backend == "sdl") {
audio = std::make_unique<SDLAudioOut>();
} else {
// Cubeb as a default fallback.
LOG_ERROR(Lib_AudioOut, "Invalid audio backend '{}', defaulting to cubeb.", backend);
audio = std::make_unique<CubebAudioOut>();
}
audio = std::make_unique<SDLAudioOut>();
return ORBIS_OK;
}
@ -354,6 +268,30 @@ int PS4_SYSV_ABI sceAudioOutMbusInit() {
return ORBIS_OK;
}
static void AudioOutputThread(PortOut* port, const std::stop_token& stop) {
{
const auto thread_name = fmt::format("shadPS4:AudioOutputThread:{}", fmt::ptr(port));
Common::SetCurrentThreadName(thread_name.c_str());
}
Common::AccurateTimer timer(
std::chrono::nanoseconds(1000000000ULL * port->buffer_frames / port->sample_rate));
while (true) {
timer.Start();
{
std::unique_lock lock{port->output_mutex};
Common::CondvarWait(port->output_cv, lock, stop, [&] { return port->output_ready; });
if (stop.stop_requested()) {
break;
}
port->impl->Output(port->output_buffer);
port->output_ready = false;
}
port->output_cv.notify_one();
timer.End();
}
}
s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
OrbisAudioOutPort port_type, s32 index, u32 length,
u32 sample_rate,
@ -361,9 +299,9 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
LOG_INFO(Lib_AudioOut,
"id = {} port_type = {} index = {} length = {} sample_rate = {} "
"param_type = {} attr = {}",
user_id, GetAudioOutPort(port_type), index, length, sample_rate,
GetAudioOutParamFormat(param_type.data_format),
GetAudioOutParamAttr(param_type.attributes));
user_id, magic_enum::enum_name(port_type), index, length, sample_rate,
magic_enum::enum_name(param_type.data_format.Value()),
magic_enum::enum_name(param_type.attributes.Value()));
if ((port_type < OrbisAudioOutPort::Main || port_type > OrbisAudioOutPort::Padspk) &&
(port_type != OrbisAudioOutPort::Aux)) {
LOG_ERROR(Lib_AudioOut, "Invalid port type");
@ -403,17 +341,18 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
}
port->type = port_type;
port->format = format;
port->is_float = IsFormatFloat(format);
port->sample_size = GetFormatSampleSize(format);
port->channels_num = GetFormatNumChannels(format);
port->samples_num = length;
port->frame_size = port->sample_size * port->channels_num;
port->buffer_size = port->frame_size * port->samples_num;
port->freq = sample_rate;
port->format_info = GetFormatInfo(format);
port->sample_rate = sample_rate;
port->buffer_frames = length;
port->volume.fill(SCE_AUDIO_OUT_VOLUME_0DB);
port->impl = audio->Open(*port);
port->output_buffer = std::malloc(port->BufferSize());
port->output_ready = false;
port->output_thread.Run(
[port](const std::stop_token& stop) { AudioOutputThread(&*port, stop); });
return std::distance(ports_out.begin(), port) + 1;
}
@ -426,24 +365,30 @@ s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, void* ptr) {
if (handle < 1 || handle > SCE_AUDIO_OUT_NUM_PORTS) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
if (ptr == nullptr) {
// Nothing to output
return ORBIS_OK;
}
auto& port = ports_out.at(handle - 1);
if (!port.impl) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
port.impl->Output(ptr, port.buffer_size);
{
std::unique_lock lock{port.output_mutex};
port.output_cv.wait(lock, [&] { return !port.output_ready; });
if (ptr != nullptr) {
std::memcpy(port.output_buffer, ptr, port.BufferSize());
port.output_ready = true;
}
}
port.output_cv.notify_one();
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num) {
for (u32 i = 0; i < num; i++) {
if (const auto err = sceAudioOutOutput(param[i].handle, param[i].ptr); err != 0)
return err;
const auto [handle, ptr] = param[i];
if (const auto ret = sceAudioOutOutput(handle, ptr); ret != ORBIS_OK) {
return ret;
}
}
return ORBIS_OK;
}
@ -549,30 +494,9 @@ s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
for (int i = 0; i < port.channels_num; i++, flag >>= 1u) {
auto bit = flag & 0x1u;
if (bit == 1) {
int src_index = i;
if (port.format == OrbisAudioOutParamFormat::Float_8CH_Std ||
port.format == OrbisAudioOutParamFormat::S16_8CH_Std) {
switch (i) {
case 4:
src_index = 6;
break;
case 5:
src_index = 7;
break;
case 6:
src_index = 4;
break;
case 7:
src_index = 5;
break;
default:
break;
}
}
port.volume[i] = vol[src_index];
for (int i = 0; i < port.format_info.num_channels; i++, flag >>= 1u) {
if (flag & 0x1u) {
port.volume[i] = vol[i];
}
}

View file

@ -6,6 +6,7 @@
#include <memory>
#include "common/bit_field.h"
#include "core/libraries/kernel/threads.h"
#include "core/libraries/system/userservice.h"
namespace Libraries::AudioOut {
@ -14,12 +15,12 @@ class PortBackend;
// Main up to 8 ports, BGM 1 port, voice up to 4 ports,
// personal up to 4 ports, padspk up to 5 ports, aux 1 port
constexpr int SCE_AUDIO_OUT_NUM_PORTS = 22;
constexpr int SCE_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value
constexpr s32 SCE_AUDIO_OUT_NUM_PORTS = 22;
constexpr s32 SCE_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value
enum class OrbisAudioOutPort { Main = 0, Bgm = 1, Voice = 2, Personal = 3, Padspk = 4, Aux = 127 };
enum class OrbisAudioOutParamFormat {
enum class OrbisAudioOutParamFormat : u32 {
S16Mono = 0,
S16Stereo = 1,
S16_8CH = 2,
@ -30,7 +31,7 @@ enum class OrbisAudioOutParamFormat {
Float_8CH_Std = 7
};
enum class OrbisAudioOutParamAttr {
enum class OrbisAudioOutParamAttr : u32 {
None = 0,
Restricted = 1,
MixToMain = 2,
@ -59,19 +60,37 @@ struct OrbisAudioOutPortState {
u64 reserved64[2];
};
struct AudioFormatInfo {
bool is_float;
u8 sample_size;
u8 num_channels;
/// Layout array remapping channel indices, specified in this order:
/// FL, FR, FC, LFE, BL, BR, SL, SR
std::array<int, 8> channel_layout;
[[nodiscard]] u16 FrameSize() const {
return sample_size * num_channels;
}
};
struct PortOut {
std::unique_ptr<PortBackend> impl{};
void* output_buffer;
std::mutex output_mutex;
std::condition_variable_any output_cv;
bool output_ready;
Kernel::Thread output_thread{};
OrbisAudioOutPort type;
OrbisAudioOutParamFormat format;
bool is_float;
u8 sample_size;
u8 channels_num;
u32 samples_num;
u32 frame_size;
u32 buffer_size;
u32 freq;
std::array<int, 8> volume;
AudioFormatInfo format_info;
u32 sample_rate;
u32 buffer_frames;
std::array<s32, 8> volume;
[[nodiscard]] u32 BufferSize() const {
return buffer_frames * format_info.FrameSize();
}
};
int PS4_SYSV_ABI sceAudioOutDeviceIdOpen();

View file

@ -3,8 +3,6 @@
#pragma once
typedef struct cubeb cubeb;
namespace Libraries::AudioOut {
struct PortOut;
@ -13,7 +11,10 @@ class PortBackend {
public:
virtual ~PortBackend() = default;
virtual void Output(void* ptr, size_t size) = 0;
/// Guaranteed to be called in intervals of at least port buffer time,
/// with size equal to port buffer size.
virtual void Output(void* ptr) = 0;
virtual void SetVolume(const std::array<int, 8>& ch_volumes) = 0;
};
@ -25,20 +26,6 @@ public:
virtual std::unique_ptr<PortBackend> Open(PortOut& port) = 0;
};
class CubebAudioOut final : public AudioOutBackend {
public:
CubebAudioOut();
~CubebAudioOut() override;
std::unique_ptr<PortBackend> Open(PortOut& port) override;
private:
cubeb* ctx = nullptr;
#ifdef _WIN32
bool owns_com = false;
#endif
};
class SDLAudioOut final : public AudioOutBackend {
public:
std::unique_ptr<PortBackend> Open(PortOut& port) override;

View file

@ -1,174 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <condition_variable>
#include <mutex>
#include <cubeb/cubeb.h>
#include "common/logging/log.h"
#include "common/ringbuffer.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
#ifdef _WIN32
#include <Objbase.h>
#endif
namespace Libraries::AudioOut {
class CubebPortBackend : public PortBackend {
public:
CubebPortBackend(cubeb* ctx, const PortOut& port)
: frame_size(port.frame_size), buffer(static_cast<int>(port.buffer_size) * 4) {
if (!ctx) {
return;
}
const auto get_channel_layout = [&port] -> cubeb_channel_layout {
switch (port.channels_num) {
case 1:
return CUBEB_LAYOUT_MONO;
case 2:
return CUBEB_LAYOUT_STEREO;
case 8:
return CUBEB_LAYOUT_3F4_LFE;
default:
UNREACHABLE();
}
};
cubeb_stream_params stream_params = {
.format = port.is_float ? CUBEB_SAMPLE_FLOAT32LE : CUBEB_SAMPLE_S16LE,
.rate = port.freq,
.channels = port.channels_num,
.layout = get_channel_layout(),
.prefs = CUBEB_STREAM_PREF_NONE,
};
u32 latency_frames = 512;
if (const auto ret = cubeb_get_min_latency(ctx, &stream_params, &latency_frames);
ret != CUBEB_OK) {
LOG_WARNING(Lib_AudioOut,
"Could not get minimum cubeb audio latency, falling back to default: {}",
ret);
}
char stream_name[64];
snprintf(stream_name, sizeof(stream_name), "shadPS4 stream %p", this);
if (const auto ret = cubeb_stream_init(ctx, &stream, stream_name, nullptr, nullptr, nullptr,
&stream_params, latency_frames, &DataCallback,
&StateCallback, this);
ret != CUBEB_OK) {
LOG_ERROR(Lib_AudioOut, "Failed to create cubeb stream: {}", ret);
return;
}
if (const auto ret = cubeb_stream_start(stream); ret != CUBEB_OK) {
LOG_ERROR(Lib_AudioOut, "Failed to start cubeb stream: {}", ret);
cubeb_stream_destroy(stream);
stream = nullptr;
return;
}
}
~CubebPortBackend() override {
if (!stream) {
return;
}
if (const auto ret = cubeb_stream_stop(stream); ret != CUBEB_OK) {
LOG_WARNING(Lib_AudioOut, "Failed to stop cubeb stream: {}", ret);
}
cubeb_stream_destroy(stream);
stream = nullptr;
}
void Output(void* ptr, size_t size) override {
if (!stream) {
return;
}
auto* data = static_cast<u8*>(ptr);
std::unique_lock lock{buffer_mutex};
buffer_cv.wait(lock, [&] { return buffer.available_write() >= size; });
buffer.enqueue(data, static_cast<int>(size));
}
void SetVolume(const std::array<int, 8>& ch_volumes) override {
if (!stream) {
return;
}
// Cubeb does not have per-channel volumes, for now just take the maximum of the channels.
const auto vol = *std::ranges::max_element(ch_volumes);
if (const auto ret =
cubeb_stream_set_volume(stream, static_cast<float>(vol) / SCE_AUDIO_OUT_VOLUME_0DB);
ret != CUBEB_OK) {
LOG_WARNING(Lib_AudioOut, "Failed to change cubeb stream volume: {}", ret);
}
}
private:
static long DataCallback(cubeb_stream* stream, void* user_data, const void* in, void* out,
long num_frames) {
auto* stream_data = static_cast<CubebPortBackend*>(user_data);
const auto out_data = static_cast<u8*>(out);
const auto requested_size = static_cast<int>(num_frames * stream_data->frame_size);
std::unique_lock lock{stream_data->buffer_mutex};
const auto dequeued_size = stream_data->buffer.dequeue(out_data, requested_size);
lock.unlock();
stream_data->buffer_cv.notify_one();
if (dequeued_size < requested_size) {
// Need to fill remaining space with silence.
std::memset(out_data + dequeued_size, 0, requested_size - dequeued_size);
}
return num_frames;
}
static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {
switch (state) {
case CUBEB_STATE_STARTED:
LOG_INFO(Lib_AudioOut, "Cubeb stream started");
break;
case CUBEB_STATE_STOPPED:
LOG_INFO(Lib_AudioOut, "Cubeb stream stopped");
break;
case CUBEB_STATE_DRAINED:
LOG_INFO(Lib_AudioOut, "Cubeb stream drained");
break;
case CUBEB_STATE_ERROR:
LOG_ERROR(Lib_AudioOut, "Cubeb stream encountered an error");
break;
}
}
size_t frame_size;
RingBuffer<u8> buffer;
std::mutex buffer_mutex;
std::condition_variable buffer_cv;
cubeb_stream* stream{};
};
CubebAudioOut::CubebAudioOut() {
#ifdef _WIN32
// Need to initialize COM for this thread on Windows, in case WASAPI backend is used.
owns_com = CoInitializeEx(nullptr, COINIT_MULTITHREADED) == S_OK;
#endif
if (const auto ret = cubeb_init(&ctx, "shadPS4", nullptr); ret != CUBEB_OK) {
LOG_CRITICAL(Lib_AudioOut, "Failed to create cubeb context: {}", ret);
}
}
CubebAudioOut::~CubebAudioOut() {
if (ctx) {
cubeb_destroy(ctx);
ctx = nullptr;
}
#ifdef _WIN32
if (owns_com) {
CoUninitialize();
owns_com = false;
}
#endif
}
std::unique_ptr<PortBackend> CubebAudioOut::Open(PortOut& port) {
return std::make_unique<CubebPortBackend>(ctx, port);
}
} // namespace Libraries::AudioOut

View file

@ -3,6 +3,7 @@
#include <thread>
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_hints.h>
#include "common/logging/log.h"
#include "core/libraries/audio/audioout.h"
@ -10,15 +11,21 @@
namespace Libraries::AudioOut {
constexpr int AUDIO_STREAM_BUFFER_THRESHOLD = 65536; // Define constant for buffer threshold
class SDLPortBackend : public PortBackend {
public:
explicit SDLPortBackend(const PortOut& port) {
explicit SDLPortBackend(const PortOut& port)
: frame_size(port.format_info.FrameSize()), buffer_size(port.BufferSize()) {
// We want the latency for delivering frames out to be as small as possible,
// so set the sample frames hint to the number of frames per buffer.
const auto samples_num_str = std::to_string(port.buffer_frames);
if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, samples_num_str.c_str())) {
LOG_WARNING(Lib_AudioOut, "Failed to set SDL audio sample frames hint to {}: {}",
samples_num_str, SDL_GetError());
}
const SDL_AudioSpec fmt = {
.format = port.is_float ? SDL_AUDIO_F32 : SDL_AUDIO_S16,
.channels = port.channels_num,
.freq = static_cast<int>(port.freq),
.format = port.format_info.is_float ? SDL_AUDIO_F32LE : SDL_AUDIO_S16LE,
.channels = port.format_info.num_channels,
.freq = static_cast<int>(port.sample_rate),
};
stream =
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &fmt, nullptr, nullptr);
@ -26,6 +33,15 @@ public:
LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError());
return;
}
queue_threshold = CalculateQueueThreshold();
if (!SDL_SetAudioStreamInputChannelMap(stream, port.format_info.channel_layout.data(),
port.format_info.num_channels)) {
LOG_ERROR(Lib_AudioOut, "Failed to configure SDL audio stream channel map: {}",
SDL_GetError());
SDL_DestroyAudioStream(stream);
stream = nullptr;
return;
}
if (!SDL_ResumeAudioStreamDevice(stream)) {
LOG_ERROR(Lib_AudioOut, "Failed to resume SDL audio stream: {}", SDL_GetError());
SDL_DestroyAudioStream(stream);
@ -42,14 +58,23 @@ public:
stream = nullptr;
}
void Output(void* ptr, size_t size) override {
void Output(void* ptr) override {
if (!stream) {
return;
}
SDL_PutAudioStreamData(stream, ptr, static_cast<int>(size));
while (SDL_GetAudioStreamAvailable(stream) > AUDIO_STREAM_BUFFER_THRESHOLD) {
// Yield to allow the stream to drain.
std::this_thread::yield();
// AudioOut library manages timing, but we still need to guard against the SDL
// audio queue stalling, which may happen during device changes, for example.
// Otherwise, latency may grow over time unbounded.
if (const auto queued = SDL_GetAudioStreamQueued(stream); queued >= queue_threshold) {
LOG_WARNING(Lib_AudioOut,
"SDL audio queue backed up ({} queued, {} threshold), clearing.", queued,
queue_threshold);
SDL_ClearAudioStream(stream);
// Recalculate the threshold in case this happened because of a device change.
queue_threshold = CalculateQueueThreshold();
}
if (!SDL_PutAudioStreamData(stream, ptr, static_cast<int>(buffer_size))) {
LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError());
}
}
@ -66,6 +91,21 @@ public:
}
private:
[[nodiscard]] u32 CalculateQueueThreshold() const {
SDL_AudioSpec discard;
int sdl_buffer_frames;
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard,
&sdl_buffer_frames)) {
LOG_WARNING(Lib_AudioOut, "Failed to get SDL audio stream buffer size: {}",
SDL_GetError());
sdl_buffer_frames = 0;
}
return std::max<u32>(buffer_size, sdl_buffer_frames * frame_size) * 4;
}
u32 frame_size;
u32 buffer_size;
u32 queue_threshold;
SDL_AudioStream* stream;
};