Measure macOS audio output latency properly — per device, at both the AudioUnit and AudioQueue layers, and all the way out to the actual sound.
If your Mac's audio starts late, drops the first syllable of a notification, or your USB DAC
feels sluggish, the usual next step is to time it with a stopwatch or afplay and guess.
That number is almost always wrong, and it sends you chasing the wrong layer.
audiolat is a small, dependency-free CLI that tells you where the time actually goes.
$ audiolat --all --reps 10 --cold 2
DEVICE TRANSPORT | AUHAL cn st pr TOTAL | QUEUE cr pp st TOTAL
─────────────────────────────────────────────────────────────────────────────────────────────────────
USB Audio USB | 2.6 14.3 11.7 29.1 | 0.1 0.0 47.5 47.6
HyperX Quadcast USB | 3.1 57.3 11.7 71.9 | 0.0 0.0 88.4 88.4
MacBook Pro Speakers built-in | 3.0 14.0 11.7 29.3 | 0.1 0.0 45.3 45.4Two USB devices on one machine, 40 ms apart. Invisible until you measure it.
Every other way of measuring macOS audio latency answers a different question than the one you're asking:
- Stopwatching
afplaymeasures the whole process — start-up plus the audio queue draining at the end plus the process exiting. It routinely reads ~20× high. - Device properties (
kAudioDevicePropertyLatency, safety offset, buffer size) describe steady state, not start-up. They say nothing about how long it takes to begin. - DAW round-trip tools measure a running signal chain, not what a cold app pays on its first sound.
- None of them can see a DAC that soft-mutes its analog output while its clock locks — during which CoreAudio reports perfect health and you hear silence.
audiolat covers all four gaps.
Requires macOS 13+ and a Swift toolchain (Xcode or Command Line Tools). No dependencies.
git clone https://github.com/abd3lraouf-studios/audiolat.git
cd audiolat
swift build -c release
./.build/release/audiolat --listOptionally put it on your PATH:
cp .build/release/audiolat /usr/local/bin/audiolat --list # every output + input device, with UIDs
audiolat --all --reps 10 # measure every output device
audiolat --device "USB Audio" --reps 20 --cold 5 # one device, idling between reps
audiolat --all --json # machine-readableTwo API layers, because they differ by an order of magnitude and only one of them is what your app actually pays.
AUHAL — the low-level path: an AudioUnit pinned to a specific device. What
low-latency and pro-audio code uses.
| column | span |
|---|---|
cn connect |
AudioComponentInstanceNew → AudioUnitInitialize returns |
st start |
AudioOutputUnitStart → first render callback |
pr present |
first callback → host time those samples reach the hardware |
QUEUE — the AudioQueue path. What AVAudioPlayer, afplay, and most "just play a
sound" code uses, whether the author knows it or not.
| column | span |
|---|---|
cr create |
AudioQueueNewOutput |
pp prepare |
pin to device, allocate and enqueue buffers |
st start |
AudioQueueStart → first buffer callback |
--list also reports each device's static latency —
(device latency + safety offset + stream latency + buffer) / sample rate — read from
device properties rather than measured. That's what the hardware adds once audio is
already flowing.
Three flags exist purely so you can kill a hypothesis before you build something on it:
| flag | question it answers |
|---|---|
--cold N |
"Is macOS powering the device down when idle?" Compare cold reps against warm ones. |
--keep-open |
"Would a keep-alive daemon help?" Holds a client with IO running, so you measure joining a live device instead of starting one. |
--fresh |
"What does a short-lived player really pay?" Runs each rep in a new process, including dyld and that process's first connection to coreaudiod. |
The hard case. Many inexpensive DAC chips soft-mute their analog output while the clock locks and unmute on a firmware timer. CoreAudio thinks audio is flowing; you hear nothing. No host-side instrumentation can see it — you have to listen.
audiolat --loopback --out "USB Audio" --in "HyperX Quadcast" --reps 5 --cold 75Plays a tone burst and reports how much later it was actually heard than the moment CoreAudio promised it would reach the hardware. Each rep fires twice — cold after a long silence, warm immediately after:
n median min p95 max
cold 3 84.2 82.4 89.4 89.4
warm 3 88.3 82.5 89.0 89.0
cold − warm = -4.1 ms
The absolute figure carries the capture path's fixed latency. The cold-minus-warm difference doesn't, and that's where a mute timer would appear. Above: none — that DAC isn't withholding audio at start-up.
Wire it up either way:
- Electrical — a 3.5 mm cable from the output jack into a capture device's input. ~1 ms precision, no room involved.
- Acoustic — speakers to a microphone. Adds ~3 ms per metre of air, irrelevant against a 100–500 ms mute.
Onset detection is a Goertzel filter at the burst frequency, not a level threshold. In a real room a level detector is hopeless: measured here, ambient noise sat at −22 dBFS with the burst arriving at −13 dBFS — 9 dB of headroom, on which the detector fires on the room, not the tone. Detecting pitch instead of loudness dropped the effective noise floor to −35 dBFS and took detection from 0 of 6 bursts to 6 of 6. The threshold is derived from a noise-floor measurement taken before each run, because no fixed default suits both a silent cable loopback and a live room.
This tool exists because a USB DAC appeared to take about a second before any sound came out. Every intuitive explanation was tested, and every one was wrong:
| Hypothesis | Test | Verdict |
|---|---|---|
| macOS tears the stream down when idle | 100 s of silence vs an immediate repeat | Identical. Idle duration is irrelevant |
| USB selective suspend / re-enumeration | ioreg power state polled across idle → play |
Pinned at state 2. Never suspends |
| A third-party audio plug-in is intercepting | Quit it; separately removed another vendor's HAL driver | Unchanged both times |
| A keep-alive daemon would fix it | 180 s of silence provably holding device IO open | Worth 9 ms |
A keep-alive daemon had already been designed. --keep-open killed it in one run, before a
line of it shipped.
And the headline number itself was wrong. Timing afplay as wall time − file duration
suggested ~1050 ms. Instrumented properly, time-to-first-audio was ~30–50 ms on every
device tested — about 4% of the apparent figure. The rest was the queue draining and the
process exiting.
That's the whole pitch: measure the layer you think is slow, before you fix the layer you think is slow.
Apple's TN2321
is blunt about what dominates: "the I/O buffer size will nearly always be the dominant
factor affecting audio stack power usage" — it sets how often the audio stack wakes the
CPU. To use less power, raise kAudioDevicePropertyBufferFrameSize, or set
kAudioHardwarePropertyPowerHint to kAudioHardwarePowerHintFavorSavingPower (or the
AudioHardwarePowerHint Info.plist key), which raises the default buffer from 512 to 4096
frames.
audiolat reports both and writes neither. Buffer frame size is device-wide, so changing
it would add latency for every other client of that device; the power hint would change the
very buffer size being measured. A measurement tool must not move the thing it measures.
It also emits silence outside --loopback, stops each unit as soon as the first callback
lands, and holds nothing open after exit.
audiolat --list
audiolat --all [options]
audiolat --device <uid|id|name-substring|default> [options]
audiolat --loopback --out <device> --in <capture-device> [options]
--api auhal|queue|both layer to measure (default both)
--reps N repetitions per device (default 5)
--cold SECONDS idle between reps
--keep-open hold a client with IO running throughout
--fresh run each rep in a new process
--json machine-readable output
loopback only:
--level DBFS burst level (default -12)
--threshold DBFS detection threshold (default: auto, from the noise floor)
--burst MS burst length (default 20)
Devices can be given as a CoreAudio UID, a numeric device ID, a case-insensitive substring
of the name, or default.
Why is my audio delayed on macOS?
Measure before guessing. Run audiolat --all — if totals are in the tens of milliseconds,
the delay you're hearing is not in the CoreAudio path, and --loopback is the next step.
Does keeping the audio device awake reduce latency?
Test it on your hardware with --keep-open. On the machine this was written for it was
worth 9 ms — real, and far below the threshold of perception.
Why is AVAudioPlayer slower than an AudioUnit?
They're different code paths. AVAudioPlayer sits on AudioQueue. Run
--api both to see the gap on your own devices.
Is my USB DAC the problem?
Compare it against your other outputs with --all. Transport is printed for each device,
so USB, DisplayPort, HDMI, Bluetooth and built-in sit side by side.
Time-to-first-audio looks fine but I still hear a late start.
That's the case --loopback was built for — most likely an analog mute in the DAC, below
anything the host can observe.
Does it need microphone permission?
Only --loopback, and only for the capture side. Grant it to the terminal you run it from
in System Settings ▸ Privacy & Security ▸ Microphone.
- Audio-thread callbacks touch only manually managed C-layout memory: no ARC, no allocation, no locks, no logging.
- Onset times are derived from the host time of the specific sample, not from when a callback happened to be scheduled.
- Loopback onsets earlier than the burst's promised presentation time are rejected — without that, room noise trips the detector first and results come out negative.
- Medians and p95, not means: these distributions are multi-modal, clustering around discrete handshake intervals, and a mean lands in a gap where nothing was observed.
Issues and pull requests welcome — particularly measurements from hardware not represented
here (Thunderbolt interfaces, Bluetooth, aggregate devices, pro audio interfaces). Include
audiolat --list and audiolat --all --json output.
macOS 13 Ventura or later. Apple silicon and Intel. No third-party dependencies.
Keywords: macOS audio latency, CoreAudio, Core Audio HAL, AUHAL, AudioUnit, AudioQueue, AVAudioPlayer latency, USB DAC latency, audio output latency measurement, audio benchmark, loopback latency, round-trip latency, Swift CLI, audio delay debugging, coreaudiod.