diff --git a/Core/Config.cpp b/Core/Config.cpp index d65ed02c6a..eda707c945 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1130,7 +1130,7 @@ static constexpr ConfigSetting soundSettings[] = { ConfigSetting("FillAudioGaps", SETTING(g_Config, bFillAudioGaps), true, CfgFlag::DEFAULT) .Label(I18NCat::AUDIO, "Fill audio gaps"), - ConfigSetting("AudioSyncMode", SETTING(g_Config, iAudioPlaybackMode), (int)AudioSyncMode::CLASSIC_PITCH, CfgFlag::DEFAULT) + ConfigSetting("AudioSyncMode", SETTING(g_Config, iAudioPlaybackMode), (int)AudioSyncMode::GRANULAR, CfgFlag::DEFAULT) .Label(I18NCat::AUDIO, "Playback mode").Choices(audioPlaybackModes), // Legacy volume settings, these get auto upgraded through default handlers on the new settings. NOTE: Must be before the new ones in the order here. diff --git a/Core/HW/GranularMixer.cpp b/Core/HW/GranularMixer.cpp index 2bf1404a02..847f1394aa 100644 --- a/Core/HW/GranularMixer.cpp +++ b/Core/HW/GranularMixer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "Common/CommonTypes.h" @@ -14,6 +15,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 @@ -79,6 +81,7 @@ void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate, float memset(samples, 0, num_samples * 2 * sizeof(s16)); // The estimate is 0 until a second has gone by. frameTimeEstimate_ = 1.0f / std::max(fpsEstimate, 1.0f); + outSampleRate_ = outSampleRate; smoothedReadSize_ = smoothedReadSize_ == 0 ? num_samples : (smoothedReadSize_ * 0.95f + num_samples * 0.05f); @@ -102,13 +105,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. @@ -304,3 +311,35 @@ void GranularMixer::GetStats(GranularStats *stats) { queuedGranulesMin_ = 10000; queuedGranulesMax_ = 0; } + +void GranularMixer::GetAudioDebugStats(char *buf, size_t bufSize) { + GranularStats stats; + GetStats(&stats); + // Each queued granule holds GRANULE_OVERLAP new samples. + const double msPerGranule = GRANULE_OVERLAP * 1000.0 / 44100.0; + snprintf(buf, bufSize, + "Granular mixer\n" + "Queued: %0.1f granules (%0.1fms)\n" + "Queued min/max: %d / %d\n" + "Cap: %d granules (%0.1fms, %d samples)\n" + "Underruns: %d\n" + "Overruns: %d\n" + "Output sample rate: %d\n" + "Read size: %0.1f\n" + "Frame time estimate: %0.1fms\n" + "Fade volume: %0.2f%s\n", + stats.smoothedQueuedGranules, stats.smoothedQueuedGranules * msPerGranule, + stats.queuedGranulesMin, stats.queuedGranulesMax, + stats.targetQueueSize, stats.targetQueueSize * msPerGranule, stats.queuedSamplesTarget, + stats.underruns, + stats.overruns, + outSampleRate_, + stats.smoothedReadSize, + stats.frameTimeEstimate * 1000.0f, + stats.fadeVolume, stats.looping ? " (looping)" : ""); +} + +void GranularMixer::ResetStatCounters() { + underruns_ = 0; + overruns_ = 0; +} diff --git a/Core/HW/GranularMixer.h b/Core/HW/GranularMixer.h index 0010d6afd6..c64d393ddb 100644 --- a/Core/HW/GranularMixer.h +++ b/Core/HW/GranularMixer.h @@ -46,6 +46,9 @@ public: void PushSamples(const s32 *samples, u32 num_samples, float volume); void GetStats(GranularStats *stats); + // Text for the audio debug overlay. + void GetAudioDebugStats(char *buf, size_t bufSize); + void ResetStatCounters(); static constexpr u32 GRANULE_SIZE = 256; @@ -107,6 +110,7 @@ private: int queuedSamplesTarget_ = 0; float smoothedReadSize_ = 0.0f; float frameTimeEstimate_ = 0.0f; + int outSampleRate_ = 0; void Enqueue(); void Dequeue(Granule* granule); 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(); diff --git a/UI/AudioCommon.cpp b/UI/AudioCommon.cpp index 69e329f5e9..bbc0d1f0e8 100644 --- a/UI/AudioCommon.cpp +++ b/UI/AudioCommon.cpp @@ -47,12 +47,13 @@ void NativeMix(int16_t *outStereo, int numFrames, int sampleRateHz, void *userda void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) { if (g_Config.iAudioPlaybackMode == (int)AudioSyncMode::GRANULAR) { - snprintf(buf, bufSize, "(No stats available for granular yet)"); + g_granular.GetAudioDebugStats(buf, bufSize); } else { g_resampler.GetAudioDebugStats(buf, bufSize); } } else { g_resampler.ResetStatCounters(); + g_granular.ResetStatCounters(); } } diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index ec3e60b063..24b83a0db1 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -1717,7 +1717,7 @@ void DrawAudioOut(ImConfig &cfg, ImControl &control) { ImGui::Text("Queued (smooth): %0.3f", stats.smoothedQueuedGranules); ImGui::Text("Target: %d", stats.targetQueueSize); ImGui::Text("Max: %d", stats.maxQueuedGranules); - ImGui::Text("Computed queue latency: %d ms", (int)(stats.smoothedQueuedGranules * (GranularMixer::GRANULE_SIZE * 1000.0 / 44100.0))); + ImGui::Text("Computed queue latency: %d ms", (int)(stats.smoothedQueuedGranules * (GranularMixer::GRANULE_SIZE / 2 * 1000.0 / 44100.0))); ImGui::Text("Fade volume: %0.2f", stats.fadeVolume); ImGui::Text("Looping: %s", BoolStr(stats.looping)); ImGui::Text("Under/Over: %d / %d", stats.underruns, stats.overruns);