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:
parent
b3e419ff11
commit
70a475ec8c
|
|
@ -481,6 +481,10 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
|
|||
target_link_libraries(audio_out2_port_tests common fmt::fmt)
|
||||
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
|
||||
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.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 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_out_timing COMMAND $<TARGET_FILE:audio_out_timing_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 save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||
|
|
@ -682,6 +687,7 @@ if(BUILD_TESTING)
|
|||
shader_recompiler_compute_tests
|
||||
virtual_memory_allocation_tests
|
||||
audio_out2_port_tests
|
||||
audio_out_timing_tests
|
||||
ngs2_sampler_tests
|
||||
pad_haptics_tests
|
||||
save_data_memory_tests
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
Loading…
Reference in New Issue