mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 19:07:01 +02:00
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:
+80
-61
@@ -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];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user