Skip to content

ASIO, CoreAudio: read the input before invoking the user callback - #492

Merged
garyscavone merged 1 commit into
thestk:masterfrom
sharooms:asio-coreaudio-input-before-callback
Oct 27, 2026
Merged

garyscavone merged 1 commit into
thestk:masterfrom
sharooms:asio-coreaudio-input-before-callback

Conversation

@sharooms

Copy link
Copy Markdown
Contributor

Fixes #491.

RtApiAsio::callbackEvent and RtApiCore::callbackEvent invoked the user callback before copying the driver's input into stream_.userBuffer[1], so the callback always received the previous cycle's input: one buffer of extra round-trip latency that getStreamLatency() does not report. The issue has the measurements (macOS/CoreAudio and Windows/ASIO, cross-checked with RTL Utility) and how it came up; the analysis and this patch were worked out with Claude and tested on real hardware.

The change

In both functions the block that copies the input into stream_.userBuffer[1] moves in front of the user callback. The block itself is unchanged except for its condition, which now carries the existing "don't bother draining input" rule (handle->drainCounter == 0) instead of a goto unlock after the output. The drain counter is still advanced after the output, exactly as before.

  • ASIO: at bufferSwitch(index) the input half index has just been filled by the driver, so it can be read before the callback. Duplex and input-only streams both get the current input.
  • CoreAudio, one device (duplex or input-only): the callback now sees the input of the same IOProc call.
  • CoreAudio, two devices: unchanged. The input device's IOProc never invoked the callback and still only stores the input; the output device's IOProc never read input.
  • stream_.deviceBuffer is shared by the input and output conversion. They still run one after the other within one call, only in the other order.
  • Side effect: after a restart, a one-device CoreAudio duplex stream no longer hands its first callback the last input of the previous run.

WASAPI, JACK, ALSA, PulseAudio, OSS and DirectSound are not touched.

The open #411 also edits the CoreAudio input copy block that this change moves. Whichever of the two lands second needs a mechanical rebase; I am happy to do it on this side.

Tested

  • macOS, CoreAudio: Audient iD4 (duplex with a cable loopback, and output-only + input-only streams), MacBook speakers + microphone (two devices). Round trip one buffer shorter at 64 / 256 / 1024, every start identical, no xruns; the two-device path measures as before.
  • Windows, ASIO: RME Digiface USB with an external ADAT converter. Same result. Inside a DAW, buffer-size changes in the driver panel (ASIO reset), unplugging the interface, sample-rate changes and repeated start/stop ran through the patched callback and behaved as with the unpatched build.
  • Builds: Apple clang on macOS, clang-cl 20 with the MSVC 14.50 headers on Windows (ASIO + WASAPI).

RtApiAsio::callbackEvent and RtApiCore::callbackEvent invoked the user
callback before copying the driver's input into stream_.userBuffer[1],
so the callback always received the input of the previous cycle: one
buffer of round-trip latency that getStreamLatency() does not report.

The input block now runs before the callback, unchanged except that it
carries the existing "don't bother draining input" rule as its own
condition. The drain counter is still advanced after the output. With
two CoreAudio devices nothing changes: the input device's IOProc only
stores the input, and the output device's IOProc never reads it.
@garyscavone
garyscavone merged commit 8ce4f26 into thestk:master Oct 27, 2026
12 checks passed
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.

ASIO and CoreAudio hand the callback the previous cycle's input (one extra buffer of round-trip latency)

2 participants