mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 10:56:58 +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:
@@ -481,6 +481,10 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
|
|||||||
target_link_libraries(audio_out2_port_tests common fmt::fmt)
|
target_link_libraries(audio_out2_port_tests common fmt::fmt)
|
||||||
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
|
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
|
||||||
|
|
||||||
|
add_executable(audio_out_timing_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AudioOutTimingTests.cpp")
|
||||||
|
target_link_libraries(audio_out_timing_tests common fmt::fmt SDL3::SDL3-static)
|
||||||
|
target_include_directories(audio_out_timing_tests PRIVATE ${inc_headers})
|
||||||
|
|
||||||
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
|
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
|
||||||
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
|
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
|
||||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||||
@@ -606,6 +610,7 @@ if(BUILD_TESTING)
|
|||||||
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
|
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
|
||||||
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
|
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
|
||||||
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
|
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
|
||||||
|
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
|
||||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||||
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||||
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||||
@@ -682,6 +687,7 @@ if(BUILD_TESTING)
|
|||||||
shader_recompiler_compute_tests
|
shader_recompiler_compute_tests
|
||||||
virtual_memory_allocation_tests
|
virtual_memory_allocation_tests
|
||||||
audio_out2_port_tests
|
audio_out2_port_tests
|
||||||
|
audio_out_timing_tests
|
||||||
ngs2_sampler_tests
|
ngs2_sampler_tests
|
||||||
pad_haptics_tests
|
pad_haptics_tests
|
||||||
save_data_memory_tests
|
save_data_memory_tests
|
||||||
|
|||||||
+80
-61
@@ -1,6 +1,5 @@
|
|||||||
#include "libs/audio.h"
|
#include "libs/audio.h"
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/emulatorConfig.h"
|
#include "common/emulatorConfig.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
@@ -12,7 +11,9 @@
|
|||||||
#include "libs/errno.h"
|
#include "libs/errno.h"
|
||||||
#include "libs/libs.h"
|
#include "libs/libs.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <magic_enum.hpp>
|
#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 int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
|
||||||
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
|
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) {
|
static bool audio_out_port_type_is_valid(int type) {
|
||||||
return (type >= AUDIO_OUT_PORT_TYPE_MAIN && type <= AUDIO_OUT_PORT_TYPE_PADSPK) ||
|
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 ||
|
type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_AUDIO3D ||
|
||||||
@@ -98,17 +117,17 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
struct PortOut {
|
struct PortOut {
|
||||||
bool used = false;
|
bool used = false;
|
||||||
int type = 0;
|
int type = 0;
|
||||||
uint32_t samples_num = 0;
|
uint32_t samples_num = 0;
|
||||||
uint32_t freq = 0;
|
uint32_t freq = 0;
|
||||||
Format format = Format::Unknown;
|
Format format = Format::Unknown;
|
||||||
uint64_t last_output_time = 0;
|
OutputClock clock;
|
||||||
bool queue_primed = false;
|
bool queue_primed = false;
|
||||||
int channels_num = 0;
|
int channels_num = 0;
|
||||||
int volume[12] = {};
|
int volume[12] = {};
|
||||||
|
|
||||||
SDL_AudioStream* stream = nullptr;
|
SDL_AudioStream* stream = nullptr;
|
||||||
Controller::DualSenseHaptics::Stream* haptics = nullptr;
|
Controller::DualSenseHaptics::Stream* haptics = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -376,10 +395,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
|||||||
: 2u;
|
: 2u;
|
||||||
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
|
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
|
||||||
const auto wait_start = LibKernel::KernelGetProcessTime();
|
const auto wait_start = LibKernel::KernelGetProcessTime();
|
||||||
auto queued = SDL_GetAudioStreamQueued(port->stream);
|
auto queued = SDL_GetAudioStreamQueued(port->stream);
|
||||||
if (queued < static_cast<int>(prepared_size)) {
|
|
||||||
port->queue_primed = false;
|
|
||||||
}
|
|
||||||
while (queued > static_cast<int>(min_queued_size)) {
|
while (queued > static_cast<int>(min_queued_size)) {
|
||||||
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
|
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
|
||||||
SDL_ClearAudioStream(port->stream);
|
SDL_ClearAudioStream(port->stream);
|
||||||
@@ -389,19 +405,20 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
|||||||
Common::Thread::SleepMicro(1000);
|
Common::Thread::SleepMicro(1000);
|
||||||
queued = SDL_GetAudioStreamQueued(port->stream);
|
queued = SDL_GetAudioStreamQueued(port->stream);
|
||||||
}
|
}
|
||||||
if (port->queue_primed) {
|
if (queued < static_cast<int>(prepared_size)) {
|
||||||
const auto next_time = port->last_output_time + buffer_us;
|
port->queue_primed = false;
|
||||||
const auto now = LibKernel::KernelGetProcessTime();
|
|
||||||
if (next_time > now) {
|
|
||||||
Common::Thread::SleepMicro(next_time - now);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SDL_PutAudioStreamData(port->stream, prepared_data, static_cast<int>(prepared_size))) {
|
if (!SDL_PutAudioStreamData(port->stream, prepared_data, static_cast<int>(prepared_size))) {
|
||||||
LOGF("AudioOut: SDL_PutAudioStreamData failed: %s\n", SDL_GetError());
|
LOGF("AudioOut: SDL_PutAudioStreamData failed: %s\n", SDL_GetError());
|
||||||
|
port->queue_primed = false;
|
||||||
return 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 &&
|
if (blocking && !port->queue_primed &&
|
||||||
SDL_GetAudioStreamQueued(port->stream) >= static_cast<int>(min_queued_size)) {
|
SDL_GetAudioStreamQueued(port->stream) >= static_cast<int>(min_queued_size)) {
|
||||||
port->queue_primed = true;
|
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) {
|
if (!m_out_ports[id].used) {
|
||||||
auto& port = m_out_ports[id];
|
auto& port = m_out_ports[id];
|
||||||
|
|
||||||
port.used = true;
|
port.used = true;
|
||||||
port.type = type;
|
port.type = type;
|
||||||
port.samples_num = samples_num;
|
port.samples_num = samples_num;
|
||||||
port.freq = freq;
|
port.freq = freq;
|
||||||
port.format = format;
|
port.format = format;
|
||||||
port.last_output_time = 0;
|
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case Format::Signed16bitMono:
|
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) {
|
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));
|
EXIT_NOT_IMPLEMENTED(!AudioOutValid(params[0].handle));
|
||||||
|
|
||||||
const auto& first_port = m_out_ports[params[0].handle.GetId()];
|
const auto& first_port = m_out_ports[params[0].handle.GetId()];
|
||||||
|
std::array<bool, OUT_PORTS_MAX> paced {};
|
||||||
uint64_t block_time = (1000000 * first_port.samples_num) / first_port.freq;
|
bool any_device = false;
|
||||||
uint64_t current_time = LibKernel::KernelGetProcessTime();
|
uint64_t deadline = 0;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint32_t i = 0; i < num; i++) {
|
for (uint32_t i = 0; i < num; i++) {
|
||||||
auto& port = m_out_ports[params[i].handle.GetId()];
|
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) {
|
if (controller_queued_us == 0) {
|
||||||
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
|
// 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.
|
// output instead, and a vibration port has none.
|
||||||
QueueSdlAudio(&port, params[i].data, blocking);
|
if (QueueSdlAudio(&port, params[i].data, blocking)) {
|
||||||
} else if (blocking && port.type == AUDIO_OUT_PORT_TYPE_PADSPK &&
|
any_device = true;
|
||||||
controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
|
paced[i] = port.queue_primed;
|
||||||
// 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);
|
} 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++) {
|
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;
|
return first_port.samples_num;
|
||||||
@@ -935,6 +951,9 @@ int KYTY_SYSV_ABI AudioOutOutputs(AudioOutOutputParam* param, uint32_t num) {
|
|||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
|
|
||||||
EXIT_NOT_IMPLEMENTED(param == nullptr);
|
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];
|
Audio::OutputParam params[Audio::OUT_PORTS_MAX];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,360 @@
|
|||||||
|
// Exercise production output scheduling with a deterministic clock and consuming SDL queues.
|
||||||
|
#include "common/threads.h"
|
||||||
|
#include "libs/dualSenseHaptics.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void Check(bool condition, const char* message) {
|
||||||
|
if (!condition) {
|
||||||
|
std::fprintf(stderr, "AudioOutTimingTests: %s\n", message);
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Stream {
|
||||||
|
SDL_AudioSpec spec;
|
||||||
|
uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption.
|
||||||
|
uint64_t updated;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint64_t now = 1000000;
|
||||||
|
uint32_t oversleep = 0, processing = 0;
|
||||||
|
std::vector<uint32_t> sleeps;
|
||||||
|
std::vector<std::unique_ptr<Stream>> streams;
|
||||||
|
bool fail_open = false, fail_put = false, stalled = false;
|
||||||
|
int clears = 0;
|
||||||
|
|
||||||
|
Stream& GetStream(SDL_AudioStream* stream) {
|
||||||
|
return *reinterpret_cast<Stream*>(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Drain(Stream& stream) {
|
||||||
|
if (!stalled) {
|
||||||
|
const auto consumed = (now - stream.updated) * stream.spec.freq;
|
||||||
|
stream.frames -= std::min(stream.frames, consumed);
|
||||||
|
}
|
||||||
|
stream.updated = now;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace Fake {
|
||||||
|
bool InitSubSystem(SDL_InitFlags) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void QuitSubSystem(SDL_InitFlags) {}
|
||||||
|
bool ResumeAudioStreamDevice(SDL_AudioStream*) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* spec,
|
||||||
|
SDL_AudioStreamCallback, void*) {
|
||||||
|
if (fail_open) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now}));
|
||||||
|
return reinterpret_cast<SDL_AudioStream*>(streams.back().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyAudioStream(SDL_AudioStream* stream) {
|
||||||
|
const auto it = std::find_if(streams.begin(), streams.end(), [stream](const auto& value) {
|
||||||
|
return value.get() == &GetStream(stream);
|
||||||
|
});
|
||||||
|
Check(it != streams.end(), "unknown stream destroyed");
|
||||||
|
streams.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetAudioStreamQueued(SDL_AudioStream* stream) {
|
||||||
|
auto& state = GetStream(stream);
|
||||||
|
Drain(state);
|
||||||
|
return static_cast<int>(state.frames / 1000000 * SDL_AUDIO_BYTESIZE(state.spec.format) *
|
||||||
|
state.spec.channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) {
|
||||||
|
now += processing;
|
||||||
|
if (fail_put) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto& state = GetStream(stream);
|
||||||
|
Drain(state);
|
||||||
|
state.frames += static_cast<uint64_t>(bytes) * 1000000 /
|
||||||
|
(SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClearAudioStream(SDL_AudioStream* stream) {
|
||||||
|
auto& state = GetStream(stream);
|
||||||
|
state.frames = 0;
|
||||||
|
state.updated = now;
|
||||||
|
clears++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace Fake
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
class TimingThread: public Thread {
|
||||||
|
public:
|
||||||
|
static void SleepMicro(uint32_t micros) {
|
||||||
|
sleeps.push_back(micros);
|
||||||
|
now += micros + oversleep;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace Common
|
||||||
|
|
||||||
|
#define Thread TimingThread
|
||||||
|
#define SDL_InitSubSystem Fake::InitSubSystem
|
||||||
|
#define SDL_QuitSubSystem Fake::QuitSubSystem
|
||||||
|
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
|
||||||
|
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
|
||||||
|
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
|
||||||
|
#define SDL_GetAudioStreamQueued Fake::GetAudioStreamQueued
|
||||||
|
#define SDL_PutAudioStreamData Fake::PutAudioStreamData
|
||||||
|
#define SDL_ClearAudioStream Fake::ClearAudioStream
|
||||||
|
#include "libs/audio.cpp"
|
||||||
|
#undef Thread
|
||||||
|
#undef SDL_InitSubSystem
|
||||||
|
#undef SDL_QuitSubSystem
|
||||||
|
#undef SDL_OpenAudioDeviceStream
|
||||||
|
#undef SDL_ResumeAudioStreamDevice
|
||||||
|
#undef SDL_DestroyAudioStream
|
||||||
|
#undef SDL_GetAudioStreamQueued
|
||||||
|
#undef SDL_PutAudioStreamData
|
||||||
|
#undef SDL_ClearAudioStream
|
||||||
|
|
||||||
|
namespace Libs::Controller {
|
||||||
|
int GetActiveControllerId() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace DualSenseHaptics {
|
||||||
|
Stream* Open(uint32_t, bool) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
void Close(Stream*) {}
|
||||||
|
uint64_t Queue(Stream*, int, const void*, uint32_t, uint32_t, bool, const int*) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} // namespace DualSenseHaptics
|
||||||
|
} // namespace Libs::Controller
|
||||||
|
|
||||||
|
namespace Libs::LibKernel {
|
||||||
|
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
} // namespace Libs::LibKernel
|
||||||
|
|
||||||
|
namespace Loader::Timer {
|
||||||
|
double GetTimeMs() {
|
||||||
|
return static_cast<double>(now) / 1000;
|
||||||
|
}
|
||||||
|
} // namespace Loader::Timer
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using Audio = Libs::Audio::Audio;
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
Audio audio;
|
||||||
|
std::array<float, 2048> pcm {};
|
||||||
|
|
||||||
|
Fixture() {
|
||||||
|
Check(streams.empty(), "stream leaked between tests");
|
||||||
|
now = 1000000;
|
||||||
|
processing = oversleep = 0;
|
||||||
|
clears = 0;
|
||||||
|
fail_open = fail_put = stalled = false;
|
||||||
|
sleeps.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::Id Open(uint32_t frames = 256, int type = 0) {
|
||||||
|
const auto id = audio.AudioOutOpen(type, frames, 48000, Audio::Format::FloatStereo);
|
||||||
|
Check(id.IsValid(), "port open failed");
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Output(Audio::Id port, bool blocking = true) {
|
||||||
|
Audio::OutputParam param {port, pcm.data()};
|
||||||
|
audio.AudioOutOutputs(¶m, 1, blocking);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Prime(Audio::Id port) {
|
||||||
|
const auto start = now;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
Output(port);
|
||||||
|
}
|
||||||
|
Check(now == start, "priming was paced before the 40 ms cushion filled");
|
||||||
|
Output(port);
|
||||||
|
Check(now - start >= 5333 && now - start <= 5334,
|
||||||
|
"priming accumulated a multi-block deadline");
|
||||||
|
Drain(*streams.front());
|
||||||
|
Check(streams.front()->frames >= uint64_t {7 * 256} * 1000000,
|
||||||
|
"first paced output consumed the priming cushion");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestPrimingAndSteadyCadence() {
|
||||||
|
Fixture f;
|
||||||
|
const auto port = f.Open();
|
||||||
|
f.Prime(port);
|
||||||
|
processing = 125;
|
||||||
|
oversleep = 175;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
const auto start = now;
|
||||||
|
for (int i = 0; i < 3000; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
Check(now - start >= 15999999 && now - start <= 16000001,
|
||||||
|
"processing, sleep overshoot, or fractional periods accumulated timing drift");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestUnderrunAndStalledQueueRecovery() {
|
||||||
|
Fixture f;
|
||||||
|
const auto port = f.Open();
|
||||||
|
f.Prime(port);
|
||||||
|
now += 1000000;
|
||||||
|
f.Prime(port);
|
||||||
|
|
||||||
|
// A stopped device must clear its stale queue and restart priming, not catch up old deadlines.
|
||||||
|
stalled = true;
|
||||||
|
for (int i = 0; i < 3 && clears == 0; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
Check(clears == 1, "stalled playback queue was not cleared");
|
||||||
|
stalled = false;
|
||||||
|
const auto start = now;
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
Check(now == start, "queue clear did not restart unpaced priming");
|
||||||
|
f.Output(port);
|
||||||
|
Check(now - start >= 5333 && now - start <= 5334,
|
||||||
|
"queue clear retained an old pacing deadline");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFallbackClockAndLongGap() {
|
||||||
|
Fixture f;
|
||||||
|
fail_open = true;
|
||||||
|
const auto port = f.Open();
|
||||||
|
f.Output(port);
|
||||||
|
const auto start = now;
|
||||||
|
oversleep = 175;
|
||||||
|
for (int i = 0; i < 3000; i++) {
|
||||||
|
now += 125;
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
Check(now - start >= 16000174 && now - start <= 16000176,
|
||||||
|
"device-free output accumulated timing drift");
|
||||||
|
now += 1000000;
|
||||||
|
f.Output(port);
|
||||||
|
const auto resumed = now;
|
||||||
|
f.Output(port);
|
||||||
|
Check(now - resumed >= 5333 && now - resumed <= 5509,
|
||||||
|
"long interruption caused a burst of device-free catch-up submissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAsyncDoesNotAccumulateDeadlines() {
|
||||||
|
Fixture f;
|
||||||
|
const auto port = f.Open();
|
||||||
|
const auto start = now;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
f.Output(port, false);
|
||||||
|
}
|
||||||
|
Check(now == start, "asynchronous output slept");
|
||||||
|
f.Output(port);
|
||||||
|
Check(now - start <= 5334, "asynchronous submissions accumulated a blocking deadline");
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
now += 1000000;
|
||||||
|
f.Output(port, false);
|
||||||
|
const auto resumed = now;
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
Check(now == resumed, "async-to-blocking transition did not reset an emptied queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestSynchronizedBatchAndInactivePorts() {
|
||||||
|
Fixture f;
|
||||||
|
const auto vibration = f.Open(256, 10);
|
||||||
|
const auto main = f.Open();
|
||||||
|
const auto background = f.Open();
|
||||||
|
Audio::OutputParam batch[] {
|
||||||
|
{vibration, f.pcm.data()}, {main, f.pcm.data()}, {background, f.pcm.data()}};
|
||||||
|
const auto start = now;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
f.audio.AudioOutOutputs(batch, 3);
|
||||||
|
}
|
||||||
|
Check(now == start, "vibration fallback slowed a device-backed batch's priming");
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
sleeps.clear();
|
||||||
|
f.audio.AudioOutOutputs(batch, 3);
|
||||||
|
Check(sleeps.size() == 1 && sleeps.front() >= 5333 && sleeps.front() <= 5334,
|
||||||
|
"synchronized streams accumulated separate pacing waits");
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::OutputParam inactive[] {{main, nullptr}, {vibration, f.pcm.data()}};
|
||||||
|
now += 1000000;
|
||||||
|
f.audio.AudioOutOutputs(inactive, 2);
|
||||||
|
const auto resumed = now;
|
||||||
|
f.audio.AudioOutOutputs(inactive, 2);
|
||||||
|
Check(now - resumed >= 5333 && now - resumed <= 5334,
|
||||||
|
"a device without PCM disabled fallback pacing");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFallbackUsesEachPortsPeriod() {
|
||||||
|
Fixture f;
|
||||||
|
fail_open = true;
|
||||||
|
const auto short_port = f.Open(128);
|
||||||
|
const auto long_port = f.Open(256);
|
||||||
|
Audio::OutputParam batch[] {{short_port, f.pcm.data()}, {long_port, f.pcm.data()}};
|
||||||
|
f.audio.AudioOutOutputs(batch, 2);
|
||||||
|
const auto start = now;
|
||||||
|
f.audio.AudioOutOutputs(batch, 2);
|
||||||
|
Check(now - start >= 5333 && now - start <= 5334,
|
||||||
|
"fallback batch used the first port's period for every port");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFailedQueueUsesFallbackClock() {
|
||||||
|
Fixture f;
|
||||||
|
const auto port = f.Open();
|
||||||
|
fail_put = true;
|
||||||
|
f.Output(port);
|
||||||
|
const auto start = now;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
f.Output(port);
|
||||||
|
}
|
||||||
|
Check(now - start == 16000, "repeated queue failures reset fallback pacing");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestInvalidBatchSize() {
|
||||||
|
Libs::Audio::AudioOut::AudioOutOutputParam param {};
|
||||||
|
for (const auto count: {0u, 33u}) {
|
||||||
|
Check(Libs::Audio::AudioOut::AudioOutOutputs(¶m, count) ==
|
||||||
|
Libs::Audio::AUDIO_OUT_ERROR_INVALID_SIZE,
|
||||||
|
"invalid batch size was not rejected before accessing ports");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
TestPrimingAndSteadyCadence();
|
||||||
|
TestUnderrunAndStalledQueueRecovery();
|
||||||
|
TestFallbackClockAndLongGap();
|
||||||
|
TestAsyncDoesNotAccumulateDeadlines();
|
||||||
|
TestSynchronizedBatchAndInactivePorts();
|
||||||
|
TestFallbackUsesEachPortsPeriod();
|
||||||
|
TestFailedQueueUsesFallbackClock();
|
||||||
|
TestInvalidBatchSize();
|
||||||
|
Check(streams.empty(), "output stream leaked");
|
||||||
|
std::puts("AudioOutTimingTests: all cases passed");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user