audio: drain the AudioOut2 queue at an exact 48 kHz grain rate

This commit is contained in:
nmzik committed 2026-10-27 21:15:03 +01:00
1 parent 64a554dea7
commit 7ff8aff5e6
2 files changed
+37 -11

No files matched your search

+12 -9
View File
@@ -262,6 +262,11 @@ static AudioOut2Submission& audioout2_submission(const AudioOut2ContextState& co
return g_audioout2_submissions[&context - g_audioout2_contexts.data()]; return g_audioout2_submissions[&context - g_audioout2_contexts.data()];
} }
// Ticks of a sixth of a microsecond: one 48 kHz sample lasts exactly 125 of them.
static uint64_t audioout2_clock() {
return LibKernel::KernelGetProcessTime() * 6;
}
static uint32_t audioout2_grain_micros(uint32_t grains) { static uint32_t audioout2_grain_micros(uint32_t grains) {
const auto sample_count = (grains == 0 ? 512u : grains); const auto sample_count = (grains == 0 ? 512u : grains);
return std::max<uint32_t>((sample_count * 1000000u) / 48000u, 1000u); return std::max<uint32_t>((sample_count * 1000000u) / 48000u, 1000u);
@@ -331,25 +336,23 @@ static void audioout2_update_context_locked(AudioOut2ContextState* state) {
return; return;
} }
const auto now = LibKernel::KernelGetProcessTime(); const auto now = audioout2_clock();
if (state->last_update == 0 || state->queued == 0) { if (state->last_update == 0 || state->queued == 0) {
state->last_update = now; state->last_update = now;
return; return;
} }
if (now <= state->last_update) {
const auto grain_micros = static_cast<uint64_t>(audioout2_grain_micros(state->num_grains));
if (now <= state->last_update || grain_micros == 0) {
return; return;
} }
const auto elapsed = now - state->last_update; const auto grain = uint64_t {state->num_grains} * 125;
const auto drained = std::min<uint64_t>(state->queued, elapsed / grain_micros); const auto drained = std::min<uint64_t>(state->queued, (now - state->last_update) / grain);
if (drained == 0) { if (drained == 0) {
return; return;
} }
state->queued -= static_cast<uint32_t>(drained); state->queued -= static_cast<uint32_t>(drained);
state->last_update += drained * grain_micros; state->last_update += drained * grain;
if (state->queued == 0) { if (state->queued == 0) {
state->last_update = now; state->last_update = now;
} }
@@ -527,7 +530,7 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
state->handle = *ctx; state->handle = *ctx;
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth; state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
state->num_grains = num_grains; state->num_grains = num_grains;
state->last_update = LibKernel::KernelGetProcessTime(); state->last_update = audioout2_clock();
state->object_audio_handle = object_audio_handle; state->object_audio_handle = object_audio_handle;
g_audioout2_state_mutex.Unlock(); g_audioout2_state_mutex.Unlock();
@@ -615,7 +618,7 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
sleep_micros = audioout2_grain_micros(state->num_grains); sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) { if (state->queued < state->queue_depth || use_device_clock) {
if (state->queued == 0) { if (state->queued == 0) {
state->last_update = LibKernel::KernelGetProcessTime(); state->last_update = audioout2_clock();
} }
if (state->queued < state->queue_depth) { if (state->queued < state->queue_depth) {
state->queued++; state->queued++;
+25 -2
View File
@@ -35,6 +35,8 @@ int g_open_waiters = 0;
bool g_block_opens = false; bool g_block_opens = false;
bool g_block_outputs = false; bool g_block_outputs = false;
std::vector<int> g_blocked_output; std::vector<int> g_blocked_output;
std::atomic_uint64_t g_process_time {0};
std::atomic_bool g_process_time_frozen {false};
void Check(bool value, const char* text) { void Check(bool value, const char* text) {
if (!value) { if (!value) {
@@ -465,6 +467,27 @@ void TestPushConsumesPcm() {
AudioOut2::AudioOut2ContextDestroy(context); AudioOut2::AudioOut2ContextDestroy(context);
} }
void TestQueueDrainsAtExactGrainRate() {
// Three 512-sample grains play in exactly 32000 us.
const auto context = CreateContext();
g_process_time_frozen = true;
const auto start = g_process_time.load();
for (int push = 0; push < 3; push++) {
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "async push failed");
}
const auto queued_after = [&](uint64_t micros) {
g_process_time = start + micros;
uint32_t queued = 0;
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, nullptr) == OK,
"queue-level query failed");
return queued;
};
Check(queued_after(31999) == 1, "the third grain drained before it finished playing");
Check(queued_after(32000) == 0, "three grains did not drain in their playing time");
g_process_time_frozen = false;
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPcmCopiedBeforeScratchBufferReuse() { void TestPcmCopiedBeforeScratchBufferReuse() {
const auto context = CreateContext(); const auto context = CreateContext();
const auto param = MakeParam(); const auto param = MakeParam();
@@ -746,8 +769,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
namespace Libs::LibKernel { namespace Libs::LibKernel {
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() { uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
static std::atomic_uint64_t now {0}; return g_process_time_frozen ? g_process_time.load() : g_process_time.fetch_add(1000);
return now.fetch_add(1000);
} }
} // namespace Libs::LibKernel } // namespace Libs::LibKernel
@@ -764,6 +786,7 @@ int main() {
TestAsynchronousDevicePushKeepsQueueBounded(); TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue(); TestHandleWithoutPcmDoesNotBypassQueue();
TestPushConsumesPcm(); TestPushConsumesPcm();
TestQueueDrainsAtExactGrainRate();
TestPcmCopiedBeforeScratchBufferReuse(); TestPcmCopiedBeforeScratchBufferReuse();
TestPortGainAndValidation(); TestPortGainAndValidation();
TestObjectGainAndAmbisonics(); TestObjectGainAndAmbisonics();