audio: keep blocking AudioOut writes on an even cadence (#822)

* audio: keep blocking AudioOut writes on an even cadence

The blocking queue check only enforced a minimum cushion, so the guest
could queue a burst of buffers and then be blocked for the whole cushion.
Measured on PPSA03647, intervals between guest writes alternated between
0 ms and ~44 ms instead of one 5.33 ms buffer period, while the average
rate stayed correct at 48000 frames/s. Mixers that run on a timer cannot
track that cadence and the music turns slow and warbling.

Keep the cushion as-is, but also hold writes to the buffer period, and
skip that wait when the stream is running low so it cannot underrun.

Intervals become ~5.4 ms with microsecond-level deviation and the menu
music plays correctly.

* audio: reuse output clock after SDL queue priming

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
6d4m
2026-09-25 11:33:01 +03:00
committed by GitHub
parent 5a705dd15f
commit 7855c980cb
+21 -2
View File
@@ -101,6 +101,7 @@ private:
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] = {};
@@ -360,20 +361,34 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
const auto prepared_size =
BytesPerSample(port->format) * output_channels * port->samples_num;
uint32_t min_queued_size = 0;
if (blocking) {
constexpr uint64_t target_latency_us = 40000;
const auto buffer_us = port->freq != 0 ? (1000000ULL * port->samples_num) / port->freq : 0;
const auto buffers =
buffer_us != 0 ? static_cast<uint32_t>((target_latency_us + buffer_us - 1) / buffer_us)
: 2u;
const auto 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();
while (SDL_GetAudioStreamQueued(port->stream) > static_cast<int>(min_queued_size)) {
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)) {
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
SDL_ClearAudioStream(port->stream);
port->queue_primed = false;
break;
}
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);
}
}
}
@@ -381,6 +396,10 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
LOGF("AudioOut: SDL_PutAudioStreamData failed: %s\n", SDL_GetError());
return false;
}
if (blocking && !port->queue_primed &&
SDL_GetAudioStreamQueued(port->stream) >= static_cast<int>(min_queued_size)) {
port->queue_primed = true;
}
return true;
}