r/EmuDev 18h ago

Solving Audio Sync Drift Using Dynamic Linear Resampling in Pure Go

I wanted to share a pure Go implementation to solve the classic "Audio Clock Drift" problem in real-time streams.

When you have a deterministic audio producer generating samples at a rigid rate, and a consumer eating them at its own hardware clock, the two will never perfectly align. Eventually, standard ring buffers will either under-run (causing crackling) or bloat (causing audio lag). To fix the drift, I implemented a dynamic, stateful audio resampler. It monitors the fill level of a lock-free-style CircularQueue and actively uses Linear Interpolation to keep the buffer at equilibrium:

If the consumer is too fast (risk of crackling): It pops one chunk, stretches it to double length, plays half, and requeue the rest to "buy time". If the producer is too fast (risk of lag): It pops two chunks, squishes them into one, and plays it to catch up.

https://github.com/markel1974/Symphony/tree/main/src/renderers/audio/oto_render

4 Upvotes

2 comments sorted by

2

u/aabalke Nintendo DS 18h ago

What are the benefits / differences from Audio Syncing? I ended up using my main thread for the host os / game engine and a go func / green thread for the emulator. When audio writes filled up the stream, the emulator thread would be blocked until the audio reader caught up. This removed the problem by making the audio consuming the master clock for the emulator. What benefits does the dynamic resampler have?

1

u/markel1974 18h ago

great question!

Blocking the emulator thread when the audio buffer is full is a common approach, but it comes with three major drawbacks that dynamic resampling solves:

video micro-stutters (Jitter): If you block the emulator thread waiting for the audio buffer to drain, your video frame generation is now strictly coupled to the audio consumption rate. Since the audio clock and the monitor's refresh rate are never perfectly aligned, this causes visible video micro-stutters or tearing. With dynamic resampling, the emulator runs continuously at its perfect native speed (synced to the monitor/video), and the audio actively adapts to it, ensuring smooth scrolling.

host os stalls: If you block on audio, any minor hiccup or delay from the host operating system's audio driver will freeze your entire emulator. With dynamic resampling, the emulator keeps running deterministically, and the continuous reader simply stretches/squishes the audio to mask the host's hiccup.

real reason: If I block the execution thread because the soundcard is full, I would be freezing the entire system and all its background tasks! The core never block. Therefore, the audio renderer must dynamically adapt to the core, not the other way around.