audio: fix popping and time-stretching in output (#966)

fix audio accumulated timing drift

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Mr.Hakkuh
2026-10-02 11:22:38 +02:00
committed by GitHub
parent b3e419ff11
commit 70a475ec8c
3 changed files with 446 additions and 61 deletions
+80 -61
View File
@@ -1,6 +1,5 @@
#include "libs/audio.h"
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
@@ -12,7 +11,9 @@
#include "libs/errno.h"
#include "libs/libs.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <magic_enum.hpp>
@@ -37,6 +38,24 @@ constexpr uint64_t AUDIO_OUT_TARGET_LATENCY_US = 40000;
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
struct OutputClock {
uint64_t next_time = 0;
uint64_t remainder = 0;
uint64_t Advance(uint64_t now, uint32_t frames, uint32_t rate) {
// Preserve small scheduling errors; discard timing debt after a discontinuity.
if (next_time == 0 || (now > next_time && now - next_time > 100000)) {
next_time = now;
remainder = 0;
}
const auto deadline = next_time;
const auto duration = 1000000ULL * frames + remainder;
next_time += duration / rate;
remainder = duration % rate;
return deadline;
}
};
static bool audio_out_port_type_is_valid(int type) {
return (type >= AUDIO_OUT_PORT_TYPE_MAIN && type <= AUDIO_OUT_PORT_TYPE_PADSPK) ||
type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_AUDIO3D ||
@@ -98,17 +117,17 @@ public:
private:
struct PortOut {
bool used = false;
int type = 0;
uint32_t samples_num = 0;
uint32_t freq = 0;
Format format = Format::Unknown;
uint64_t last_output_time = 0;
bool queue_primed = false;
int channels_num = 0;
int volume[12] = {};
bool used = false;
int type = 0;
uint32_t samples_num = 0;
uint32_t freq = 0;
Format format = Format::Unknown;
OutputClock clock;
bool queue_primed = false;
int channels_num = 0;
int volume[12] = {};
SDL_AudioStream* stream = nullptr;
SDL_AudioStream* stream = nullptr;
Controller::DualSenseHaptics::Stream* haptics = nullptr;
};
@@ -376,10 +395,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
: 2u;
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
const auto wait_start = LibKernel::KernelGetProcessTime();
auto queued = SDL_GetAudioStreamQueued(port->stream);
if (queued < static_cast<int>(prepared_size)) {
port->queue_primed = false;
}
auto queued = SDL_GetAudioStreamQueued(port->stream);
while (queued > static_cast<int>(min_queued_size)) {
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
SDL_ClearAudioStream(port->stream);
@@ -389,19 +405,20 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
Common::Thread::SleepMicro(1000);
queued = SDL_GetAudioStreamQueued(port->stream);
}
if (port->queue_primed) {
const auto next_time = port->last_output_time + buffer_us;
const auto now = LibKernel::KernelGetProcessTime();
if (next_time > now) {
Common::Thread::SleepMicro(next_time - now);
}
if (queued < static_cast<int>(prepared_size)) {
port->queue_primed = false;
}
}
if (!SDL_PutAudioStreamData(port->stream, prepared_data, static_cast<int>(prepared_size))) {
LOGF("AudioOut: SDL_PutAudioStreamData failed: %s\n", SDL_GetError());
port->queue_primed = false;
return false;
}
// Unpaced priming must not advance the playback deadline into the future.
if (!port->queue_primed) {
port->clock = {};
}
if (blocking && !port->queue_primed &&
SDL_GetAudioStreamQueued(port->stream) >= static_cast<int>(min_queued_size)) {
port->queue_primed = true;
@@ -417,12 +434,11 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For
if (!m_out_ports[id].used) {
auto& port = m_out_ports[id];
port.used = true;
port.type = type;
port.samples_num = samples_num;
port.freq = freq;
port.format = format;
port.last_output_time = 0;
port.used = true;
port.type = type;
port.samples_num = samples_num;
port.freq = freq;
port.format = format;
switch (format) {
case Format::Signed16bitMono:
@@ -524,36 +540,13 @@ bool Audio::AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume) {
}
uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking) {
EXIT_NOT_IMPLEMENTED(num == 0);
EXIT_NOT_IMPLEMENTED(num == 0 || num > OUT_PORTS_MAX);
EXIT_NOT_IMPLEMENTED(!AudioOutValid(params[0].handle));
const auto& first_port = m_out_ports[params[0].handle.GetId()];
uint64_t block_time = (1000000 * first_port.samples_num) / first_port.freq;
uint64_t current_time = LibKernel::KernelGetProcessTime();
uint64_t max_wait_time = 0;
for (uint32_t i = 0; i < num; i++) {
uint64_t next_time = m_out_ports[params[i].handle.GetId()].last_output_time + block_time;
uint64_t wait_time = (next_time > current_time ? next_time - current_time : 0);
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
}
bool any_port_has_device = false;
for (uint32_t i = 0; i < num; i++) {
if (m_out_ports[params[i].handle.GetId()].stream != nullptr) {
any_port_has_device = true;
break;
}
}
// One real output device is enough to pace the whole synchronized batch. Applying the fallback
// when a vibration port is present would rate-limit the device-backed ports to exactly 1x and
// prevent their SDL queues from building an underrun cushion.
if (blocking && max_wait_time != 0 && !any_port_has_device) {
Common::Thread::SleepMicro(max_wait_time);
}
const auto& first_port = m_out_ports[params[0].handle.GetId()];
std::array<bool, OUT_PORTS_MAX> paced {};
bool any_device = false;
uint64_t deadline = 0;
for (uint32_t i = 0; i < num; i++) {
auto& port = m_out_ports[params[i].handle.GetId()];
@@ -569,16 +562,39 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
// output instead, and a vibration port has none.
QueueSdlAudio(&port, params[i].data, blocking);
} else if (blocking && port.type == AUDIO_OUT_PORT_TYPE_PADSPK &&
controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
// Vibration never paces output; the speaker is audible, so pace it like other ports.
Common::Thread::SleepMicro(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
if (QueueSdlAudio(&port, params[i].data, blocking)) {
any_device = true;
paced[i] = port.queue_primed;
}
} else {
port.queue_primed = false;
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK) {
any_device = true;
if (blocking && controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
deadline =
std::max(deadline, LibKernel::KernelGetProcessTime() +
controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
}
}
}
}
// Submit the whole batch before pacing it. Audible devices own its cadence while
// priming; silent/vibration-only batches use the same sample clock as SDL output.
const auto now = LibKernel::KernelGetProcessTime();
for (uint32_t i = 0; i < num; i++) {
m_out_ports[params[i].handle.GetId()].last_output_time = LibKernel::KernelGetProcessTime();
auto& port = m_out_ports[params[i].handle.GetId()];
if (blocking && (paced[i] || !any_device)) {
deadline = std::max(deadline, port.clock.Advance(now, port.samples_num, port.freq));
} else {
port.clock = {};
}
if (!blocking) {
port.queue_primed = false;
}
}
if (deadline > now) {
Common::Thread::SleepMicro(deadline - now);
}
return first_port.samples_num;
@@ -935,6 +951,9 @@ int KYTY_SYSV_ABI AudioOutOutputs(AudioOutOutputParam* param, uint32_t num) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(param == nullptr);
if (num == 0 || num > Audio::OUT_PORTS_MAX) {
return AUDIO_OUT_ERROR_INVALID_SIZE;
}
Audio::OutputParam params[Audio::OUT_PORTS_MAX];