Skip to content

Cut the native read-ahead to 24 ms on macOS - #78

Merged
mparrett merged 7 commits into
mainfrom
wt/native-readahead-24ms
Oct 27, 2026
Merged

mparrett merged 7 commits into
mainfrom
wt/native-readahead-24ms

Conversation

@mparrett

@mparrett mparrett commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

On macOS, playerBuffer goes from 40 ms to 24 ms. The device buffer stays at 1024 frames, and other platforms stay at 40 ms.

Evidence, all on a MacBook Air, 2026-10-26:

  • Latency: a speaker-to-mic loopback measured a trigger-to-sound median of 119–128 ms over three runs, down from 150–158 ms at 40 ms.
  • Clicks, by ear: a few minutes of tesserae at 24 ms / 1024 played clean, and so did 40 ms / 1024. At 24 ms / 512 it crackled the whole time, even though the loopback showed clean onsets at 512 frames. latency.go records this, along with the reason onset timing can't catch underruns, and the stall budget: about 47 ms at 24 ms, against 63 ms at 40 ms.

Changes:

  • playerBuffer is set per platform (playerbuffer_darwin.go and playerbuffer_other.go). Only macOS was heard at 24 ms. There oto drains the device a quarter-buffer at a time; a backend that drains the whole buffer per callback (WASAPI may) would leave 24 ms almost no margin, so other platforms keep 40 ms.
  • A missing Options.Queue is now resolved in overrides. When one device buffer outlasts playerBuffer, at a low rate or with more frames, the default stretches to that one buffer, which is continuous in rate and frames. Before, that case was an error.
  • cmd/undertone still asks for 500 ms for a bed with no one-shots, unchanged. Browsers aren't affected, since they play through webaudio.

🤖 Generated with Claude Code

mparrett and others added 4 commits October 26, 2026 23:33
Trigger-to-sound on a MacBook Air (speaker-to-mic loopback, 2026-10-26)
drops from a 150-158 ms median to 119-126 ms. 24 ms is the shortest
read-ahead the 1024-frame device allows at 44.1 kHz. A few minutes of
tesserae at 24 ms / 1024 played clean by ear; 24 ms / 512 crackled the
whole time despite clean loopback onsets, so the device buffer stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… short

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the default

Only macOS was heard at 24 ms. oto drains it a quarter-buffer at a time;
a backend that drains the whole buffer per callback would leave 24 ms
almost nothing after each drain. playerBuffer now lives per platform: 24 ms
on darwin, 40 ms elsewhere.

Where one device buffer outlasts playerBuffer (a low rate, or more
frames), the default read-ahead is that buffer plus playerBuffer rather
than exactly one buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mparrett mparrett changed the title Cut the native read-ahead to 24 ms Cut the native read-ahead to 24 ms on macOS Oct 27, 2026
mparrett and others added 3 commits October 26, 2026 23:40
…uffer if longer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e long comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mparrett
mparrett merged commit 59bd2d8 into main Oct 27, 2026
4 checks passed
@mparrett
mparrett deleted the wt/native-readahead-24ms branch October 27, 2026 04:45
mparrett added a commit that referenced this pull request Oct 31, 2026
* Put the macOS native read-ahead back to 40 ms

#78 cut it to 24 ms on macOS after tesserae played clean there. xsofy, a
heavier lg process, crackled at 24 ms on the same kind of Mac, and
UNDERTONE_PLAYER_BUFFER=40ms fixed it (2026-10-31). Every platform now
defaults to 40 ms, about 30 ms more trigger latency on macOS (150-158 ms
against 119-128 ms) for a stall budget of ~63 ms instead of ~47. A host
that wants 24 ms asks for it through Options.Queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fold playerBuffer into one constant, and name the lg host's override

From review: with both platforms at 40 ms the per-platform files were
the same constant twice, and latency.go still said it varied. An lg host
opens through the letgo binding with defaults, so it can't reach
Options.Queue; UNDERTONE_PLAYER_BUFFER is its route back to 24 ms, and
the README and notes now say so. The buffer margin is dated again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant