From 5f349fd603f3eab03b770a78fbdca0cdb55bfd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 9 Nov 2026 21:00:38 +0100 Subject: [PATCH] Audio: Size the granular queue from the classic resampler's target The queue cap was four output callbacks plus a frame of audio. Bluetooth and other outputs without a fast path call back with large chunks, so that came to about 100 ms there. Now it's the classic target (which also honors Extra audio buffering), raised for slow games' per-frame bursts and to keep half the queue above one callback's read, plus a granule for the overlap. Co-Authored-By: Claude Opus 5.5 --- Core/HW/GranularMixer.cpp | 19 ++++++++++++------- Core/HW/StereoResampler.cpp | 23 +++++++++++++---------- Core/HW/StereoResampler.h | 3 +++ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Core/HW/GranularMixer.cpp b/Core/HW/GranularMixer.cpp index 2bf1404a02..0636995593 100644 --- a/Core/HW/GranularMixer.cpp +++ b/Core/HW/GranularMixer.cpp @@ -14,6 +14,7 @@ #include "Common/Swap.h" #include "Core/Config.h" #include "Core/HW/Display.h" +#include "Core/HW/StereoResampler.h" #include "Core/Core.h" #include "Core/System.h" #include "Core/Util/AudioFormat.h" // for clamp_u16 @@ -102,13 +103,17 @@ void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate, float const float fade_in_mul = -std::expm1(-1.0 / (out_sample_rate * FADE_IN_RC)); const float fade_out_mul = -std::expm1(-1.0 / (out_sample_rate * FADE_OUT_RC)); - // Calculate the ideal length of the granule queue. - // NOTE: We must have enough room here for 20fps games, generating all their audio - // in a burst each frame (since we can't force real clock sync). That means 16*3 = 48 or rather 50ms. - // However, in case of faster framerates, we should apply some pressure to reduce this. And if real clock sync - // is on, we should also be able to get away with a shorter buffer here. - // const u32 buffer_size_ms = frameTimeEstimate_ * 44100.0f; - const u32 buffer_size_samples = smoothedReadSize_ * 4 + std::llround(frameTimeEstimate_ * inSampleRate); + // Calculate the length of the granule queue. Dequeue() cuts the queue back to half when it + // goes over, so this is a cap rather than a fill level. + // We aim for the same latency as StereoResampler, but must also have room for slow (like 20fps) + // games, which generate all their audio in a burst each frame (since we can't force real + // clock sync), and keep half the queue above what one output callback reads, or each callback + // runs dry. Bluetooth and other non-fast outputs read in large chunks. + // One more granule on top, for the two half-overlapping granules being played. + const int targetSamples = StereoResampler::TargetBufferSize(); + const int frameSamples = (int)std::llround(frameTimeEstimate_ * inSampleRate); + const int readSamples = (int)(smoothedReadSize_ * inSampleRate / out_sample_rate); + const u32 buffer_size_samples = std::max({ targetSamples, frameSamples + targetSamples / 2, readSamples * 2 }) + GRANULE_SIZE; queuedSamplesTarget_ = buffer_size_samples; // Limit the possible queue sizes to any number between 4 and 64. diff --git a/Core/HW/StereoResampler.cpp b/Core/HW/StereoResampler.cpp index c03926b952..13e00d56cf 100644 --- a/Core/HW/StereoResampler.cpp +++ b/Core/HW/StereoResampler.cpp @@ -74,20 +74,23 @@ StereoResampler::~StereoResampler() { buffer_ = nullptr; } -void StereoResampler::UpdateBufferSize() { +int StereoResampler::TargetBufferSize() { if (g_Config.bExtraAudioBuffering) { + return TARGET_BUFSIZE_EXTRA; + } + int systemBufsize = System_GetPropertyInt(SYSPROP_AUDIO_FRAMES_PER_BUFFER); + if (systemBufsize > 0 && TARGET_BUFSIZE_DEFAULT < systemBufsize + TARGET_BUFSIZE_MARGIN) { + return std::min(4096, systemBufsize + TARGET_BUFSIZE_MARGIN); + } + return TARGET_BUFSIZE_DEFAULT; +} + +void StereoResampler::UpdateBufferSize() { + targetBufsize_ = TargetBufferSize(); + if (g_Config.bExtraAudioBuffering || targetBufsize_ * 2 > MAX_BUFSIZE_DEFAULT) { maxBufsize_ = MAX_BUFSIZE_EXTRA; - targetBufsize_ = TARGET_BUFSIZE_EXTRA; } else { maxBufsize_ = MAX_BUFSIZE_DEFAULT; - targetBufsize_ = TARGET_BUFSIZE_DEFAULT; - - int systemBufsize = System_GetPropertyInt(SYSPROP_AUDIO_FRAMES_PER_BUFFER); - if (systemBufsize > 0 && targetBufsize_ < systemBufsize + TARGET_BUFSIZE_MARGIN) { - targetBufsize_ = std::min(4096, systemBufsize + TARGET_BUFSIZE_MARGIN); - if (targetBufsize_ * 2 > MAX_BUFSIZE_DEFAULT) - maxBufsize_ = MAX_BUFSIZE_EXTRA; - } } } diff --git a/Core/HW/StereoResampler.h b/Core/HW/StereoResampler.h index f12401715f..f7031b43f2 100644 --- a/Core/HW/StereoResampler.h +++ b/Core/HW/StereoResampler.h @@ -44,6 +44,9 @@ public: void GetAudioDebugStats(char *buf, size_t bufSize); void ResetStatCounters(); + // The buffer fill to aim for, in 44.1 kHz stereo frames. GranularMixer sizes its queue from this too. + static int TargetBufferSize(); + private: void UpdateBufferSize();