Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AudioDelay

A tiny macOS menu bar app that delays all system audio by an adjustable 0–500 ms, so sound stays in sync when video goes through a high-latency path (wireless HDMI → projector) while audio plays through a low-latency one (USB interface → speakers).

Everything the Mac plays — Plex, browsers, anything — is delayed. You tune the amount by ear with a slider until lips match voices.

How it works

macOS system output is pointed at BlackHole, a free virtual audio device. AudioDelay reads the audio back out of BlackHole, holds it in a ring buffer for the delay you set, and plays it out your USB interface. Both devices are wrapped in one Aggregate Device so a single real-time IO proc handles the whole path.

System audio → BlackHole (virtual) → AudioDelay ring buffer → USB interface → speakers

One-time setup

1. Install BlackHole 2ch

brew install blackhole-2ch

(or use the installer from existential.audio — either way macOS will ask for your password because it installs an audio driver).

2. Create the Aggregate Device

  1. Open Audio MIDI Setup (in /Applications/Utilities).
  2. Click + in the bottom-left → Create Aggregate Device.
  3. In the device list on the right, check BlackHole 2ch and your USB interface.
  4. Set the Clock Source to your USB interface.
  5. Enable Drift Correction on BlackHole 2ch. Don't skip this — the two devices have independent clocks and will drift audibly apart over a two-hour movie without it.
  6. Rename the aggregate to something identifiable, e.g. Delay Bridge (double-click the name in the sidebar).

3. Point system audio at BlackHole

System Settings → Sound → Output → BlackHole 2ch.

(Or hold ⌥ and click the volume icon in the menu bar.)

4. Run AudioDelay

./build.sh
open build/AudioDelay.app

Click the in the menu bar:

  • Pick your aggregate device under Aggregate Device (it auto-picks an aggregate containing BlackHole if you don't).
  • Click Enable Delay. macOS will ask for microphone access — this is BlackHole presenting as an input device; the app reads system audio from it and records nothing. If you deny it, the app gets silence; there's a menu item to reopen the privacy setting.
  • Drag the delay slider (or type a value) while a video plays until lips sync. Steps of 1 ms, changes are live and click-free.
  • The Volume slider below it controls loudness in the delay path (squared taper, smoothed so it never clicks) — handy since the keyboard volume keys don't work with BlackHole.

The delay value, chosen device, and enabled state persist across relaunches.

Things to know

  • Your keyboard volume keys will stop working while output is set to BlackHole — it has no hardware volume, and macOS routes those keys to the output device. Use the Volume slider in the app's menu instead (applied in the delay path, click-free, persisted) or the physical knob on your interface.
  • To get normal audio back (e.g. laptop speakers), just switch Sound Output back to your usual device. No need to quit the app, though disabling it releases the audio hardware entirely.
  • Sleep/wake is handled — the app re-establishes the audio path after the Mac wakes.
  • If the USB interface is unplugged, the app stops cleanly and shows an error in the menu; re-enable after replugging.
  • Sample rate changes are handled by restarting the path automatically.

Debugging

Print every audio device, its streams, and how the app would map an aggregate's channels:

.build/release/AudioDelay --list-devices

Runtime events (device loss, sample-rate changes, processor overloads) go to the unified log:

log stream --predicate 'process == "AudioDelay"'

Building from source

Requires macOS 13+ and the Xcode Command Line Tools (xcode-select --install). No full Xcode, no dependencies.

./build.sh          # → build/AudioDelay.app

To keep it around, copy build/AudioDelay.app to /Applications and add it to Login Items if you want it at startup.

Design notes

  • Pure CoreAudio (AudioDeviceCreateIOProcID) — no AVAudioEngine, so the latency path is explicit.
  • The IO proc is real-time-safe: preallocated ring buffer, no allocation/locks/ObjC in the callback; the delay value crosses threads through a C11 atomic.
  • Delay changes crossfade over ~15 ms between the old and new read positions, so adjusting during playback never clicks.
  • Channel mapping is discovered from the aggregate's sub-device list at start, never hardcoded, and logged so it can be verified.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages