diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index 59da5f3..de77a4b 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -139,6 +139,9 @@ public static class AudioOut2Exports return ahead <= 0 ? 0 : (uint)Math.Min((ahead + GrainTicks - 1) / GrainTicks, QueueDepth); } + public double DeviceSlotFreeMilliseconds => + DeviceSlotFreeThreshold(GrainSamples, Frequency, QueueDepth); + // Grains the device holds beyond the cushion, at most QueueDepth. public uint DeviceQueuedGrains(int queuedMilliseconds) { @@ -430,6 +433,14 @@ public static class AudioOut2Exports { context.DevicePaced = true; } + + if (submitted || advancedGrain == 1) + { + if (blocking != 0) + { + WaitForDeviceSlot(context); + } + } else { // Push is the blocking point of a grain: it waits for the grain an Advance claimed, or @@ -1096,6 +1107,28 @@ public static class AudioOut2Exports } } + internal static double DeviceSlotFreeThreshold(uint grainSamples, uint frequency, uint queueDepth) => + DeviceCushionMilliseconds + (grainSamples * 1000.0 / frequency * Math.Max((long)queueDepth - 1, 0)); + + private static void WaitForDeviceSlot(ContextState context) + { + var backend = ResolveContextBackend(context, out _); + if (backend is null) + { + return; + } + + var limit = context.DeviceSlotFreeMilliseconds; + var deadline = Stopwatch.GetTimestamp() + (Stopwatch.Frequency / 4); + int queued; + while ((queued = backend.QueuedMilliseconds) > limit && + Stopwatch.GetTimestamp() < deadline && + !HostSessionControl.IsShutdownRequested) + { + Thread.Sleep((int)Math.Clamp(Math.Ceiling(queued - limit), 1, 50)); + } + } + private static uint QueuedGrains(ContextState context) { if (context.DevicePaced) diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PacingTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PacingTests.cs new file mode 100644 index 0000000..e3dce5a --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PacingTests.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +public sealed class AudioOut2PacingTests +{ + [Theory] + [InlineData(256u, 48000u, 1u, 40.0)] + [InlineData(256u, 48000u, 2u, 45.333)] + [InlineData(256u, 48000u, 4u, 56.0)] + [InlineData(1024u, 48000u, 4u, 104.0)] + public void ABlockingPushWaitsUntilTheDeviceHoldsTheCushionAndOneSlotLessThanTheQueue( + uint grainSamples, uint frequency, uint queueDepth, double expected) + { + Assert.Equal(expected, AudioOut2Exports.DeviceSlotFreeThreshold(grainSamples, frequency, queueDepth), 3); + } + + [Fact] + public void TheThresholdStaysFarBelowTheHostQueueCap() + { + const double hostCapMilliseconds = 128 * 1024 * 1000.0 / (48000 * 4); + Assert.True(AudioOut2Exports.DeviceSlotFreeThreshold(256, 48000, 2) < hostCapMilliseconds / 10); + } +}