audio: Rewrite SDL backend to use condvar wait

modules/SceAudio: fix crackling audio, if empty buffer is used too often

Co-authored-by: eagleflo <aku.kotkavuo@gmail.com>
This commit is contained in:
bookmist
2026-02-07 16:20:29 +03:00
committed by Zangetsu
parent e9769da5e8
commit 68220f6711
9 changed files with 44 additions and 66 deletions
+1 -8
View File
@@ -425,14 +425,7 @@ bool late_init(EmuEnvState &state) {
return false;
}
const ResumeAudioThread resume_thread = [&state](SceUID thread_id) {
const auto thread = state.kernel.get_thread(thread_id);
const std::lock_guard<std::mutex> lock(thread->mutex);
if (thread->status == ThreadStatus::wait) {
thread->update_status(ThreadStatus::run);
}
};
if (!state.audio.init(resume_thread, state.cfg.audio_backend)) {
if (!state.audio.init(state.cfg.audio_backend)) {
LOG_WARN("Failed to initialize audio! Audio will not work.");
}
+1 -2
View File
@@ -6,5 +6,4 @@ add_library(
src/impl/cubeb_audio.cpp)
target_include_directories(audio PUBLIC include)
target_link_libraries(audio PUBLIC SDL3::SDL3)
target_link_libraries(audio PRIVATE util cubeb kernel)
target_link_libraries(audio PRIVATE util cubeb SDL3::SDL3)
@@ -53,7 +53,7 @@ public:
bool init() override;
AudioOutPortPtr open_port(int nb_channels, int freq, int nb_sample) override;
void audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer) override;
void audio_output(AudioOutPort &out_port, const void *buffer) override;
void set_volume(AudioOutPort &out_port, float volume) override;
void switch_state(const bool pause) override;
};
+7 -6
View File
@@ -18,36 +18,37 @@
#pragma once
#include "../state.h"
#include <SDL3/SDL_audio.h>
#include <condition_variable>
class SDLAudioAdapter : public AudioAdapter {
private:
SDL_AudioDeviceID device_id = 0;
int device_buffer_samples = 0;
SDL_AudioSpec dst_spec;
static void SDLCALL thread_wakeup_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount);
public:
SDLAudioAdapter(AudioState &audio_state);
explicit SDLAudioAdapter(AudioState &audio_state);
~SDLAudioAdapter() override;
bool init() override;
void switch_state(const bool pause) override;
AudioOutPortPtr open_port(int nb_channels, int freq, int nb_sample) override;
void audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer) override;
void audio_output(AudioOutPort &out_port, const void *buffer) override;
void set_volume(AudioOutPort &out_port, float volume) override;
int get_rest_sample(AudioOutPort &out_port) override;
};
typedef std::shared_ptr<SDL_AudioStream> AudioStreamPtr;
using AudioStreamPtr = std::shared_ptr<SDL_AudioStream>;
struct SDLAudioOutPort : public AudioOutPort {
// thread currently waiting for the audio to be processed
SceUID thread = -1;
int channels = 2;
AudioStreamPtr stream;
SDLAudioAdapter &adapter;
std::mutex mutex;
std::condition_variable cond_var;
SDLAudioOutPort(AudioStreamPtr stream, AudioAdapter &adapter)
: stream(std::move(stream))
, adapter(dynamic_cast<SDLAudioAdapter &>(adapter)) {}
+3 -6
View File
@@ -28,8 +28,6 @@
#define SCE_AUDIO_OUT_MAX_VOL 32768 //!< Maximum output port volume
#define SCE_AUDIO_VOLUME_0DB SCE_AUDIO_OUT_MAX_VOL //!< Maximum output port volume
typedef std::function<void(SceUID)> ResumeAudioThread;
struct AudioOutPort {
// Channel range from 0 - 32768
int left_channel_volume = SCE_AUDIO_VOLUME_0DB;
@@ -73,7 +71,7 @@ public:
virtual bool init() = 0;
virtual AudioOutPortPtr open_port(int nb_channels, int freq, int nb_sample) { return nullptr; }
virtual void audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer) {}
virtual void audio_output(AudioOutPort &out_port, const void *buffer) {}
virtual void set_volume(AudioOutPort &out_port, float volume) {}
virtual void switch_state(const bool pause) {}
virtual int get_rest_sample(AudioOutPort &out_port) { return 0; };
@@ -87,14 +85,13 @@ struct AudioState {
int next_port_id = 1;
AudioOutPortPtrs out_ports;
AudioInPort in_port;
ResumeAudioThread resume_thread;
std::string audio_backend;
float global_volume = 1;
bool init(const ResumeAudioThread &resume_thread, const std::string &adapter_name);
bool init(const std::string &adapter_name);
void set_backend(const std::string &adapter_name);
AudioOutPortPtr open_port(int nb_channels, int freq, int nb_sample);
void audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer);
void audio_output(AudioOutPort &out_port, const void *buffer);
void set_volume(AudioOutPort &out_port, float volume);
void set_global_volume(float volume);
void switch_state(const bool pause);
+3 -10
View File
@@ -20,17 +20,12 @@
#include <audio/impl/cubeb_audio.h>
#include <audio/impl/sdl_audio.h>
#include <kernel/thread/thread_state.h>
#include <util/log.h>
#include <algorithm>
#include <cassert>
#include <cstring>
bool AudioState::init(const ResumeAudioThread &resume_thread, const std::string &adapter_name) {
this->resume_thread = resume_thread;
bool AudioState::init(const std::string &adapter_name) {
set_backend(adapter_name);
if (!adapter)
return false;
@@ -68,10 +63,8 @@ AudioOutPortPtr AudioState::open_port(int nb_channels, int freq, int nb_sample)
return port;
}
void AudioState::audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer) {
if (!buffer)
return;
adapter->audio_output(thread, out_port, buffer);
void AudioState::audio_output(AudioOutPort &out_port, const void *buffer) {
adapter->audio_output(out_port, buffer);
uint64_t now = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t diff = now - out_port.last_output;
+1 -7
View File
@@ -16,9 +16,6 @@
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include "audio/impl/cubeb_audio.h"
#include "kernel/thread/thread_state.h"
#include "util/log.h"
static long impl_cubeb_audio_callback(cubeb_stream *stream, void *user_data, const void *input, void *output, long nframes) {
@@ -122,15 +119,12 @@ AudioOutPortPtr CubebAudioAdapter::open_port(int nb_channels, int freq, int nb_s
return port;
}
void CubebAudioAdapter::audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer) {
void CubebAudioAdapter::audio_output(AudioOutPort &out_port, const void *buffer) {
CubebAudioOutPort &port = static_cast<CubebAudioOutPort &>(out_port);
std::unique_lock<std::mutex> lock(port.mutex);
if (port.nb_buffers_ready == port.audio_buffers.size()) {
// is it really useful to update the thread status?
thread.update_status(ThreadStatus::wait);
port.cond_var.wait(lock);
thread.update_status(ThreadStatus::run);
}
assert(port.nb_buffers_ready < port.audio_buffers.size());
+16 -24
View File
@@ -16,12 +16,9 @@
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include "audio/impl/sdl_audio.h"
#include "kernel/thread/thread_state.h"
#include <SDL3/SDL_audio.h>
#include "util/log.h"
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_hints.h>
#define SDL_CHECK_EXT(condition, ret) \
do { \
@@ -38,17 +35,10 @@
void SDLCALL SDLAudioAdapter::thread_wakeup_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) {
assert(userdata != nullptr);
assert(stream != nullptr);
SDLAudioOutPort *port = static_cast<SDLAudioOutPort *>(userdata);
// Is there a thread waiting for playback to finish?
if (port->thread >= 0) {
const int samples_available = port->adapter.get_rest_sample(*port);
assert(samples_available >= 0);
// Running out of data?
if (samples_available < (4 * port->adapter.device_buffer_samples) + port->len) {
// Wake the thread up.
port->adapter.state.resume_thread(port->thread);
port->thread = -1;
}
const int bytes_available = SDL_GetAudioStreamAvailable(stream);
if (bytes_available < 2 * total_amount || additional_amount > 0) {
SDLAudioOutPort *port = static_cast<SDLAudioOutPort *>(userdata);
port->cond_var.notify_one();
}
}
@@ -62,6 +52,10 @@ SDLAudioAdapter::~SDLAudioAdapter() {
}
bool SDLAudioAdapter::init() {
// SDL3 default is 1024 sample frames for 48kHz audio, which is higher than cubeb.
// Request smaller device buffer for lower latency callbacks.
// 512 sample frames = 2048 bytes for stereo 16-bit, matching cubeb's callback size.
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, "512");
device_id = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
SDL_CHECK_EXT(device_id > 0, false);
return true;
@@ -92,19 +86,17 @@ AudioOutPortPtr SDLAudioAdapter::open_port(int nb_channels, int freq, int nb_sam
switch_state(false);
return port;
}
void SDLAudioAdapter::audio_output(ThreadState &thread, AudioOutPort &out_port, const void *buffer) {
void SDLAudioAdapter::audio_output(AudioOutPort &out_port, const void *buffer) {
// Put audio to the port's stream and see how much is left to play.
SDLAudioOutPort &port = static_cast<SDLAudioOutPort &>(out_port);
SDL_CHECK_VOID(SDL_PutAudioStreamData(port.stream.get(), buffer, out_port.len_bytes));
const int samples_available = get_rest_sample(port);
// If there's lots of audio left to play, stop this thread.
// The audio callback will wake it up later when it's running out of data.
if (samples_available >= (4 * device_buffer_samples) + port.len) {
port.thread = thread.id;
std::unique_lock<std::mutex> mlock(thread.mutex);
thread.update_status(ThreadStatus::wait);
thread.status_cond.wait(mlock, [&]() { return thread.status == ThreadStatus::run; });
const int samples_available = get_rest_sample(port);
if (samples_available > device_buffer_samples) {
std::unique_lock<std::mutex> lock(port.mutex);
port.cond_var.wait_for(lock, std::chrono::microseconds(port.len_microseconds * 2));
}
SDL_CHECK_VOID(SDL_PutAudioStreamData(port.stream.get(), buffer, out_port.len_bytes));
}
void SDLAudioAdapter::set_volume(AudioOutPort &out_port, float volume) {
+11 -2
View File
@@ -19,6 +19,7 @@
#include <audio/state.h>
#include <kernel/state.h>
#include <kernel/thread/thread_state.h>
#include <util/lock_and_find.h>
#include <util/tracy.h>
@@ -194,12 +195,20 @@ EXPORT(int, sceAudioOutOutput, int port, const void *buf) {
return RET_ERROR(SCE_AUDIO_OUT_ERROR_INVALID_PORT);
}
// Empty "buf" variable is valid. It mean wait until sound output is completed.
// Because this function always returns when all sound is out, then on empty buf it returns immediately.
// Return value is the number of samples (value of 0 or greater) registered to the audio driver for normal termination.
if (!buf)
return 0;
const ThreadStatePtr thread = emuenv.kernel.get_thread(thread_id);
if (!thread) {
return RET_ERROR(SCE_AUDIO_OUT_ERROR_INVALID_PORT);
}
emuenv.audio.audio_output(*thread, *prt, buf);
// is it really useful to update the thread status?
thread->update_status(ThreadStatus::wait);
emuenv.audio.audio_output(*prt, buf);
thread->update_status(ThreadStatus::run);
return prt->len;
}