Affected platform: macOS
Problem
While the player is refueling, the refuel beep should fire at a steady cadence. Instead, the beeps are heard in an irregular rhythm, perceived as a "triplet" pattern — some pairs of beeps sound closer together, others further apart.
The trigger itself is regular: log timestamps confirm rewindAndPlay is called at consistent ~83 ms intervals. The irregularity is in playback, not in triggering.
The issue reproduces with a minimal Ebiten program that simply calls Rewind()+Play() on aт *audio.Player every Update() at 12 TPS.
Research
Audio pipeline on macOS:
rewindAndPlay()
→ mux pre-buffer filled (~14.6 ms beep + EOF)
→ next AudioQueue render callback (fires every 34.83 ms, independent clock)
→ buffer enqueued at tail of 4-deep hardware pipeline
→ plays 3 × 34.83 ms = 104.5 ms later
The Darwin AudioQueue driver in ebitengine/oto uses 4 ring buffers of 12 288 bytes each.
At 44 100 Hz stereo float32:
12288 bytes / (2ch × 4 bytes/sample) / 44100 Hz ≈ 34.83 ms per buffer
The render callback fires every ~34.83 ms on the hardware clock, independently of the game clock.
When rewindAndPlay is called, the beep data enters the pipeline at the next render callback —
anywhere from 0 to 34.83 ms later. That callback's output is then 3 buffers deep in the hardware
queue, so actual playback happens 104.5 ms to 139.3 ms after the trigger.
Why the rhythm is uneven:
The game fires at 83.33 ms (12 TPS); the render callback fires at 34.83 ms. Their ratio is
irrational (≈ 2.392), so the phase between the two clocks drifts by:
83.33 mod 34.83 ≈ 13.67 ms per frame
This means the "wait until next render callback" after each game trigger cycles through different
values each frame. The resulting playback spacings alternate between approximately:
- ~69.6 ms (2 render periods) — two beeps sound close together
- ~104.4 ms (3 render periods) — two beeps sound far apart
The pattern repeats approximately every 5 game frames / 12 render callbacks (~415 ms), which the
ear perceives as a triplet-like rhythm.
Why Rewind() does not help:
Rewind() calls mux Seek(), which clears the in-memory pre-buffer (p.buf). It cannot touch
AudioQueue buffers already committed to hardware via _AudioQueueEnqueueBuffer. The jitter is
introduced by the hardware buffer granularity, not by the pre-buffer.
Affected platform: macOS
Problem
While the player is refueling, the refuel beep should fire at a steady cadence. Instead, the beeps are heard in an irregular rhythm, perceived as a "triplet" pattern — some pairs of beeps sound closer together, others further apart.
The trigger itself is regular: log timestamps confirm
rewindAndPlayis called at consistent ~83 ms intervals. The irregularity is in playback, not in triggering.The issue reproduces with a minimal Ebiten program that simply calls
Rewind()+Play()on aт*audio.PlayereveryUpdate()at 12 TPS.Research
Audio pipeline on macOS:
The Darwin AudioQueue driver in
ebitengine/otouses 4 ring buffers of 12 288 bytes each.At 44 100 Hz stereo float32:
The render callback fires every ~34.83 ms on the hardware clock, independently of the game clock.
When
rewindAndPlayis called, the beep data enters the pipeline at the next render callback —anywhere from 0 to 34.83 ms later. That callback's output is then 3 buffers deep in the hardware
queue, so actual playback happens 104.5 ms to 139.3 ms after the trigger.
Why the rhythm is uneven:
The game fires at 83.33 ms (12 TPS); the render callback fires at 34.83 ms. Their ratio is
irrational (≈ 2.392), so the phase between the two clocks drifts by:
This means the "wait until next render callback" after each game trigger cycles through different
values each frame. The resulting playback spacings alternate between approximately:
The pattern repeats approximately every 5 game frames / 12 render callbacks (~415 ms), which the
ear perceives as a triplet-like rhythm.
Why
Rewind()does not help:Rewind()calls muxSeek(), which clears the in-memory pre-buffer (p.buf). It cannot touchAudioQueue buffers already committed to hardware via
_AudioQueueEnqueueBuffer. The jitter isintroduced by the hardware buffer granularity, not by the pre-buffer.