summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris <f1ana@users.noreply.github.com>2026-09-30 22:39:23 -0400
committerGitHub <noreply@github.com>2026-10-01 02:39:23 +0000
commitf4e698f4cc6906af2892c762fee115025d3863e6 (patch)
tree88ff9b1378e3e350ee8fede76ca6c5326885823e
parentad7d0a66d0d528cf315f024d6cb168b357031143 (diff)
Revert self-pumping audio thread (#7284)
* Revert "Fix audio regression playing audio too slowly (#6894)" This reverts commit db41e1b5f84ce8cf35635dac68ad818cbf70c453. * Revert "fix(audio): Fix audio stutter when gfx hitches (#6704)" This reverts commit 4415537deb9942ae7f2bb8d5fe458422b829bb1a.
-rw-r--r--soh/soh/OTRAudio.h6
-rw-r--r--soh/soh/OTRGlobals.cpp117
2 files changed, 34 insertions, 89 deletions
diff --git a/soh/soh/OTRAudio.h b/soh/soh/OTRAudio.h
index eaa0d1edc..5ba4b23de 100644
--- a/soh/soh/OTRAudio.h
+++ b/soh/soh/OTRAudio.h
@@ -4,8 +4,8 @@
static struct {
std::thread thread;
- std::condition_variable cv_to_thread;
+ std::condition_variable cv_to_thread, cv_from_thread;
std::mutex mutex;
- std::atomic_bool running;
- std::atomic_bool processing;
+ bool running;
+ bool processing;
} audio;
diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp
index 0341e4f36..1eb3d212e 100644
--- a/soh/soh/OTRGlobals.cpp
+++ b/soh/soh/OTRGlobals.cpp
@@ -849,11 +849,7 @@ void OTRGlobals::Initialize() {
CVarGetInteger(CVAR_SETTING("AutoCaptureMouse"), 1));
context->GetWindow()->SetForceCursorVisibility(CVarGetInteger(CVAR_SETTING("CursorVisibility"), 0));
- context->InitAudio({ .SampleRate = 32000,
- .SampleLength = 1024,
- // 4096 frames at 32 kHz (~128 ms) gives enough reservoir for frame
- // jitter and slow-frame spikes without perceptible audio latency.
- .DesiredBuffered = 4096 });
+ context->InitAudio({ .SampleRate = 32000, .SampleLength = 1024, .DesiredBuffered = 1680 });
// The menu is set up before audio is initialized, so its list of available audio backends has to be
// populated here rather than in Menu::InitElement (where the window backends are handled).
@@ -1042,100 +1038,42 @@ extern "C" int AudioPlayer_GetDesiredBuffered(void);
std::unordered_map<std::string, ExtensionEntry> ExtensionCache;
void OTRAudio_Thread() {
-#define SAMPLES_HIGH 560
-#define SAMPLES_MID 544
-#define SAMPLES_LOW 528
-#define AUDIO_FRAMES_PER_UPDATE (R_UPDATE_RATE > 0 ? R_UPDATE_RATE : 1)
-#define NUM_AUDIO_CHANNELS 2
-
- // The sequencer advances a fixed slice of musical time per engine update
- // (tempoInternalToExternal in audio_heap.c assumes 60 updates/sec), so with
- // production paced by backend buffer fill the sample count must average
- // exactly 32000/60 = 533.33 per update or tempo drifts.
- // Two thirds 528 one third 544 gives 533.33.
- int32_t sample_debt_thirds = 0;
-
- // Single producer routine used by both wake-driven and pre-buffer loops.
- // Picks per-iteration sample count itself, then produces and plays it.
- auto produce_next_batch = [&]() {
- u32 num_audio_samples = sample_debt_thirds > 0 ? SAMPLES_MID : SAMPLES_LOW;
- sample_debt_thirds += (1600 - 3 * (int32_t)num_audio_samples) * AUDIO_FRAMES_PER_UPDATE;
-
- const u32 total_frames = num_audio_samples * AUDIO_FRAMES_PER_UPDATE;
- const u32 total_samples = total_frames * NUM_AUDIO_CHANNELS;
-
- // 3 is the maximum authentic frame divisor.
- static thread_local s16 audio_buffer[SAMPLES_HIGH * NUM_AUDIO_CHANNELS * 3];
-
- for (int i = 0; i < AUDIO_FRAMES_PER_UPDATE; i++) {
- AudioMgr_CreateNextAudioBuffer(audio_buffer + i * (num_audio_samples * NUM_AUDIO_CHANNELS),
- num_audio_samples);
- }
-
- AudioPlayer_Play(reinterpret_cast<u8*>(audio_buffer), total_samples * sizeof(int16_t));
- };
-
- // Self-pump cadence. The gfx thread wakes us once per rendered frame
- // (Graph_ProcessGfxCommands sets audio.processing), but a single long
- // frame leave us asleep while the backend's queue drains to silence.
- // So we also wake on a short timeout, independent of the gfx frame rate.
- // Doing so is in fact closer to the console, where the audio task ran
- // off the scheduler rather than gated on rendering..
- constexpr auto kSelfPumpInterval = std::chrono::milliseconds(5);
-
- // The self-pump timeout must wait that the game has reached its render
- // loop, to avoid accessing uninitialized variables.
- bool primed = false;
-
while (audio.running) {
{
std::unique_lock<std::mutex> Lock(audio.mutex);
- if (!primed) {
- // Pre-init: block until the gfx thread drives the first buffer
- // (engine guaranteed ready by then), exactly as before.
- while (!audio.processing && audio.running) {
- audio.cv_to_thread.wait(Lock);
- }
- primed = true;
- } else if (!audio.processing && audio.running) {
- // Primed: wait for the next gfx wake, but no longer than
- // kSelfPumpInterval so a stalled gfx thread can't starve the
- // backend queue. A pending wake falls straight through.
- audio.cv_to_thread.wait_for(Lock, kSelfPumpInterval);
+ while (!audio.processing && audio.running) {
+ audio.cv_to_thread.wait(Lock);
}
if (!audio.running) {
break;
}
}
+ std::unique_lock<std::mutex> Lock(audio.mutex);
+// AudioMgr_ThreadEntry(&gAudioMgr);
+// 528 and 544 relate to 60 fps at 32 kHz 32000/60 = 533.333..
+// in an ideal world, one third of the calls should use num_samples=544 and two thirds num_samples=528
+#define SAMPLES_HIGH 560
+#define SAMPLES_LOW 528
- {
- std::unique_lock<std::mutex> Lock(audio.mutex);
+#define AUDIO_FRAMES_PER_UPDATE (R_UPDATE_RATE > 0 ? R_UPDATE_RATE : 1)
+#define NUM_AUDIO_CHANNELS 2
- // Producer guard (banteg/Shipwright#6594): skip advancing the audio
- // engine if the backend ring cannot accept the largest next burst.
- // Generating PCM that DoPlay() would refuse creates a discontinuity
- // audible as a click. The pre-buffer loop below will catch up once
- // the backend drains enough.
- if (AudioPlayer_Buffered() + SAMPLES_MID * AUDIO_FRAMES_PER_UPDATE > AudioPlayer_GetDesiredBuffered()) {
- audio.processing = false;
- } else {
- produce_next_batch();
- audio.processing = false;
- }
- }
+ int samples_left = AudioPlayer_Buffered();
+ u32 num_audio_samples = samples_left < AudioPlayer_GetDesiredBuffered() ? SAMPLES_HIGH : SAMPLES_LOW;
- // Pre-buffer: fill the reservoir while the backend can accept more,
- // without waiting for the next frame signal. This absorbs load spikes.
- // Safe for BGM — the N64 sequencer advances independently of gameplay.
- // The producer guard (same as above) prevents advancing the audio engine
- // when the backend ring is already at capacity.
- while (audio.running && AudioPlayer_Buffered() < AudioPlayer_GetDesiredBuffered()) {
- if (AudioPlayer_Buffered() + SAMPLES_MID * AUDIO_FRAMES_PER_UPDATE > AudioPlayer_GetDesiredBuffered()) {
- break;
- }
- produce_next_batch();
+ // 3 is the maximum authentic frame divisor.
+ s16 audio_buffer[SAMPLES_HIGH * NUM_AUDIO_CHANNELS * 3];
+ for (int i = 0; i < AUDIO_FRAMES_PER_UPDATE; i++) {
+ AudioMgr_CreateNextAudioBuffer(audio_buffer + i * (num_audio_samples * NUM_AUDIO_CHANNELS),
+ num_audio_samples);
}
+
+ AudioPlayer_Play((u8*)audio_buffer,
+ num_audio_samples * (sizeof(int16_t) * NUM_AUDIO_CHANNELS * AUDIO_FRAMES_PER_UPDATE));
+
+ audio.processing = false;
+ audio.cv_from_thread.notify_one();
}
}
@@ -1906,6 +1844,13 @@ extern "C" void Graph_ProcessGfxCommands(Gfx* commands) {
last_fps = fps;
last_update_rate = R_UPDATE_RATE;
+ {
+ std::unique_lock<std::mutex> Lock(audio.mutex);
+ while (audio.processing) {
+ audio.cv_from_thread.wait(Lock);
+ }
+ }
+
bool curAltAssets = CVarGetInteger(CVAR_SETTING("AltAssets"), 1);
if (prevAltAssets != curAltAssets) {
prevAltAssets = curAltAssets;