Granular queue: Use the current FPS to get an estimate for how long the maximum queue size should be.

This commit is contained in:
Henrik Rydgård committed 2025-08-22 21:21:20 +02:00
1 parent 3b683a8e43
commit e00ef04792
4 files changed
+26 -9

No files matched your search

+10 -6
View File
@@ -75,11 +75,12 @@ GranularMixer::GranularMixer() {
}
// Executed from sound stream thread
void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate) {
void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate, float fpsEstimate) {
_dbg_assert_(samples);
if (!samples)
return;
memset(samples, 0, num_samples * 2 * sizeof(s16));
frameTimeEstimate_ = 1.0f / fpsEstimate;
smoothedReadSize_ = smoothedReadSize_ == 0 ? num_samples : (smoothedReadSize_ * 0.95f + num_samples * 0.05f);
@@ -89,14 +90,14 @@ void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate) {
// We need at least a double because the index jump has 24 bits of fractional precision.
const double out_sample_rate = outSampleRate;
double in_sample_rate = 44100;
double inSampleRate = 44100;
const double emulation_speed = 1.0f; // TODO: Change when we're in slow-motion mode etc.
if (0 < emulation_speed && emulation_speed != 1.0)
in_sample_rate *= emulation_speed;
inSampleRate *= emulation_speed;
const double base = static_cast<double>(1 << GRANULE_FRAC_BITS);
const u32 index_jump = std::lround(base * in_sample_rate / out_sample_rate);
const u32 index_jump = std::lround(base * inSampleRate / out_sample_rate);
// These fade in / out multiplier are tuned to match a constant
// fade speed regardless of the input or the output sample rate.
@@ -108,8 +109,9 @@ void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate) {
// 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 = 100;
const u32 buffer_size_samples = std::llround(buffer_size_ms * in_sample_rate / 1000.0);
// const u32 buffer_size_ms = frameTimeEstimate_ * 44100.0f;
const u32 buffer_size_samples = smoothedReadSize_ * 4 + std::llround(frameTimeEstimate_ * inSampleRate);
queuedSamplesTarget_ = buffer_size_samples;
// Limit the possible queue sizes to any number between 4 and 64.
const u32 buffer_size_granules =
@@ -298,6 +300,8 @@ void GranularMixer::GetStats(GranularStats *stats) {
stats->overruns = overruns_;
stats->underruns = underruns_;
stats->smoothedReadSize = smoothedReadSize_;
stats->frameTimeEstimate = frameTimeEstimate_;
stats->queuedSamplesTarget = queuedSamplesTarget_;
queuedGranulesMin_ = 10000;
queuedGranulesMax_ = 0;
}
+6 -2
View File
@@ -26,6 +26,7 @@ constexpr u32 countr_one_replacement(u32 x) {
struct GranularStats {
int queuedGranulesMin;
int queuedGranulesMax;
int queuedSamplesTarget;
float smoothedQueuedGranules;
int targetQueueSize;
int maxQueuedGranules;
@@ -34,6 +35,7 @@ struct GranularStats {
int overruns;
int underruns;
float smoothedReadSize;
float frameTimeEstimate;
};
class GranularMixer final {
@@ -41,10 +43,10 @@ public:
explicit GranularMixer();
// Called from audio threads
void Mix(s16* samples, u32 numSamples, int outSampleRate);
void Mix(s16 *samples, u32 numSamples, int outSampleRate, float vpsEstimate);
// Called from emulation thread
void PushSamples(const s32* samples, u32 num_samples, float volume);
void PushSamples(const s32 *samples, u32 num_samples, float volume);
void GetStats(GranularStats *stats);
@@ -105,7 +107,9 @@ private:
int overruns_ = 0;
int queuedGranulesMin_ = 10000;
int queuedGranulesMax_ = 0;
int queuedSamplesTarget_ = 0;
float smoothedReadSize_ = 0.0f;
float frameTimeEstimate_ = 0.0f;
void Enqueue();
void Dequeue(Granule* granule);
+8 -1
View File
@@ -21,6 +21,7 @@
#include "Core/HW/GranularMixer.h"
#include "UI/AudioCommon.h"
#include "UI/BackgroundAudio.h"
#include "Core/HW/Display.h"
StereoResampler g_resampler;
GranularMixer g_granular;
@@ -30,7 +31,13 @@ GranularMixer g_granular;
void NativeMix(int16_t *outStereo, int numFrames, int sampleRateHz, void *userdata) {
// Mix UI sound effects on top.
if (g_Config.iAudioSyncMode == (int)AudioSyncMode::GRANULAR) {
g_granular.Mix(outStereo, numFrames, sampleRateHz);
// We use the FPS estimate, because to maintain smooth audio even though our
// frame execution is very front (or back) heavy (as we can't count on "real time clock sync"
// to be enabled), we need at least one whole frame buffered, plus a bit of extra.
float fpsEstimate, vps, actualFps;
__DisplayGetFPS(&vps, &fpsEstimate, &actualFps);
g_granular.Mix(outStereo, numFrames, sampleRateHz, fpsEstimate);
} else {
g_resampler.Mix(outStereo, numFrames, false, sampleRateHz);
}
+2
View File
@@ -1353,6 +1353,8 @@ void DrawAudioOut(ImConfig &cfg, ImControl &control) {
g_granular.GetStats(&stats);
ImGui::Text("Granules");
ImGui::Text("Read size: %0.2f", stats.smoothedReadSize);
ImGui::Text("Frame time estimate: %0.1f ms", stats.frameTimeEstimate * 1000.0f);
ImGui::Text("Queued samples target: %d", stats.queuedSamplesTarget);
ImGui::Text("Queued min/max: %d / %d", stats.queuedGranulesMin, stats.queuedGranulesMax);
ImGui::Text("Queued (smooth): %0.3f", stats.smoothedQueuedGranules);
ImGui::Text("Target: %d", stats.targetQueueSize);