An audio router for macOS with its own virtual device, and a signal path that can be proved bit-exact.
YunAudio routes a microphone, and any application's audio, into a virtual device that other applications open as an ordinary input. It ships that virtual device itself rather than depending on a third-party loopback driver, which is what makes the rest of the design possible.
The original problem was narrow. Discord's WebRTC engine reaches down to the HAL and contends with USB microphones for control of the device, producing a periodic crackle; routing the microphone through a virtual device that Discord opens instead removes the contention. Several macOS capabilities encountered along the way are not exposed by anything else in this category, and the project grew to cover them.
| Platform | macOS 14.2 or later |
| Dependencies | Sparkle 2, embedded for signed application updates |
| Interfaces | Window, menu bar panel, URL scheme, CLI, Unix socket, MCP, MIDI, resident JavaScript |
| Formats | 44.1–96 kHz through the virtual device, to 192 kHz between physical ones; WAV, FLAC and AAC, with per-source stems |
| Licence | Apache 2.0 |
The virtual device derives its sample clock from the microphone being captured,
through GetZeroTimeStamp(). Both devices then advance together, the HAL's drift
correction can be disabled, and no stage of the path resamples. A third-party
loopback device cannot do this, having no knowledge of which input matters.
yunaudio-cli selftest sends a 24-bit pseudorandom sequence through the whole
path, reads it back from the loopback, recovers the delay from the data and
compares every sample:
bit-exact: 261400/261400 samples identical, delay 872 frames
clock lock held at 0.999986 throughout
The same check is available in Preferences → Diagnostics. On a path that is not
clock-locked it reports the actual condition — resampled, with the recovered
delay and the magnitude of the conversion — rather than a pass or a failure. The
0.999986 is the microphone's crystal error: 14 ppm slow, or 50 ms of drift per
hour uncorrected.
The interface states the condition of the path at all times: bit-exact, resampled or processed, with measured round-trip latency and the state of the clock lock. Enabling voice isolation withdraws the bit-exactness claim, because processing the signal contradicts it.
Where the path is not bit-exact it says by how much, in the terms it is heard in: level change, residual, waveform correlation and the octave bands. A path 6 dB quieter and otherwise identical used to read exactly like one carrying a different recording — both were "resampled" with a mean error nobody has a feel for.
yunaudio-cli health answers a different question in three seconds: whether the
system's audio server will still open a device. It builds a private aggregate,
opens it and takes it down again, because that is what a route does — asking
anything simpler gets an answer to a question nobody asked, and three simpler
probes all reported a healthy machine on which nothing worked.
Method, and the limits of what it proves: MEASUREMENT.md.
Loudness is measured to ITU-R BS.1770-4 — K-weighting, 400 ms blocks at 75 % overlap, and the two-pass gate that excludes pauses — and reported as momentary, short-term and integrated values with the distance from a chosen platform target. Discord normalises to approximately −18 LUFS, YouTube to −14, broadcast to −23.
The arithmetic is verified against the standard rather than against itself: a 1 kHz sine reads its own RMS level in LUFS, doubling the amplitude adds 6.02, readings agree at 48 and 96 kHz, and silence between passages does not depress the integrated figure.
The spectrum is twenty-four log-spaced bands with a frequency axis, calibrated so that a tone of known amplitude returns at its own level in decibels. The equaliser's bands sit at the frequencies the analyser draws.
Each captured application receives its own process tap rather than joining a shared mixdown, so gain, role and ducking behaviour are independent per source. Multiple hardware inputs are supported on the same terms, each with its own strip.
Two independent mixes are maintained — the send and the monitor — with a per-source level on each, and per-bus tone control and headphone correction. Additional outputs carry a copy of the send at a level of their own.
Automatic levelling moves the input trim only while Apple's on-device sound classifier reports speech, at 1.5 dB/s within a dead zone and bounded to 15 dB, so pauses and typing do not raise the gain. The same classifier gates ducking, so a cough does not attenuate music.
A chain of gate, high-pass, compressor, limiter, six-band equaliser, pitch,
formant and character stages, alongside Apple's AUSoundIsolation — the model
behind FaceTime's Voice Isolation, measured here at 56 ms of added latency.
Pitch and formants shift independently. The spectral envelope is estimated from the low-quefrency part of the log spectrum, stretched along the frequency axis and divided back out, leaving the harmonics in place. Presets are verified by measurement: a synthetic male voice through the higher-voice preset emerges with its pitch raised by the stated 500 cents and its spectral centroid raised with it.
Third-party Audio Units are inserted after the application's own stages and before the limiter. A unit that fails to load is reported with the step that refused it; one requiring out-of-process hosting is identified before it enters the path, because every render then becomes an XPC round trip.
A stage of its own, alongside the panel in the main window. Both are built from the same controls — the transport, the queue, the words controls, the scoring switch and the key suggestion are one construction each, so a control added to either reaches both. Only the arrangement differs.
Lyrics resolve from Music's own metadata and local .lrc files first, then from
LRCLIB, QQ Music, NetEase Cloud Music and lyrics.ovh, queried concurrently. A
validated timeline wins and cancels the outstanding requests. Traditional and
Simplified metadata, live editions and television-performance labels are matched
without attaching an original recording to an accompaniment. When nothing
matches, the words can be searched for by title, chosen from a file, or driven
by hand against another application's playback.
Words are swept a syllable at a time where the source carries word timing, with pronunciation above the line and conversion between the two Chinese scripts. The offset is remembered per song, for the files that carry no lead-in.
Scoring declares its reference: an exact melody from a matching MIDI file, an audio-derived reference from captured original vocals, or key and timing alone where only an accompaniment is available. The performance is aligned against the reference with banded dynamic time warping before it is measured, so a phrase sung late scores as late rather than as out of tune, and lateness and steadiness are reported separately from pitch. A small on-device model on the Neural Engine picks the voice out when the accompaniment is louder than it — worth 3, 6 and 13 points at one and a half, two and three times the singer's level, and switchable because it cannot help below that. Each microphone keeps its own pitch history and score.
A queue, because a machine nobody has to walk back to between songs is the point: songs go on the end, 插播 goes next, the end stops rather than starting the evening again. The system's own transport — the media keys, Control Centre, a pair of AirPods — drives it.
WAV, FLAC or AAC, with an optional file per source taken before the fader.
Transcription runs on-device through SpeechTranscriber, one instance per source,
so the speaker label follows the routing rather than an inference from the audio.
| Interface | Purpose |
|---|---|
| URL scheme | Shortcuts, Stream Deck, Keyboard Maestro, open |
yunaudio-cli |
The same verbs with a reply, and the verification harness |
Unix socket, chmod 600 |
The transport beneath the CLI and the MCP server |
yunaudio-mcp |
Model Context Protocol server; JSON-RPC 2.0 over stdio |
| Resident JavaScript | Handlers for mute, recording, and device arrival and departure |
| obs-websocket v5 | Mute mirroring, and the sync offset OBS cannot compute itself |
| CoreMIDI | Physical faders, with soft takeover |
One vocabulary underlies all of them, defined once in Sources/YunAudioControl.
Reference: docs/automation.md.
A bare stereo route costs 0.42 % of one core and 7.1 MB resident, measured over
six minutes by yunaudio-cli soak with no interface attached. The application
itself is a different figure and a larger one: with a route running and the
window open it measures around 18 % of one core and 180 MB. That gap is being
worked on, and quoting only the first number would misrepresent it. Zero allocations on the IO thread, asserted in
release builds by a hook in front of every allocation the process makes. The
single exception is stated rather than hidden: Apple's voice isolation model
allocates approximately 0.3 times per cycle internally.
Twenty-one capabilities are documented in full, each with its measurement, in docs/features.md.
- macOS 14.2 or later. The floor is process taps, which is the one thing here with no alternative — everything else that needs a newer system says so rather than being absent. Voice isolation needs macOS 15; live transcription needs macOS 26, as does keeping a capture attached across the application quitting; and the Liquid Glass appearance falls back to the material the system does have. Everything to do with routing, capture, effects and karaoke works throughout.
- An Xcode carrying the macOS 27 SDK to build, because live transcription
uses
AnalyzerInputConverter. The build scripts locate one; a hand-runswift buildneedssource ./App/toolchain.shfirst, otherwise the error iscannot find type 'AnalyzerInputConverter' in scope. - Sparkle 2 is the only third-party dependency. It is embedded in the app and verifies both the update feed and every archive with YunAudio's Ed25519 key.
brew install --cask yuhuanstudio/tap/yunaudioLater versions can be installed with brew upgrade --cask yunaudio. Homebrew
moves the app into Applications; it does not install the optional audio device,
ask for administrator access or restart system audio.
./package.sh produces build/YunAudio-<version>.dmg, containing the
application, the virtual device, and a script each to install and remove it.
The application is ad-hoc signed, so macOS refuses the first launch and reports
that the developer cannot be verified. Privacy & Security in System Settings
offers Open Anyway; once is sufficient. READ ME FIRST.txt in the image
states this, and the steps involved.
Once running from Applications, YunAudio checks a signed update feed through Sparkle. The update itself is verified before extraction. Without a paid Apple Developer identity, macOS may still ask for microphone or Automation access again after the application is replaced.
A locally built binary carries no quarantine attribute, so the first-launch dialog does not apply.
# The virtual device. Installation restarts coreaudiod, briefly stopping all
# system audio, and requires an administrator password.
./Driver/build-driver.sh --install
# The application.
./App/build-app.sh --runRemoval:
sudo rm -rf /Library/Audio/Plug-Ins/HAL/YunAudioDriver.driver
sudo killall coreaudiodApplication capture, the processing chain, recording, transcription, monitoring, OBS integration, MIDI and scripting require nothing to be installed. The device provides the remaining capability: other applications selecting YunAudio as their input, over a bit-exact path.
./App/verify.sh --list # the steps, with their cost
./App/verify.sh # everything not requiring audio hardware
./App/verify.sh --full # and the flow check, which takes the hardware
./App/verify.sh --fresh # and a clean clone built from nothing
./App/verify.sh --only=build,tests # 10 s
./App/verify.sh --flow="more than one input" # one flow-check section, 44 sThe steps are independent by design: the unit suite, a string-table comparison across three languages, an offscreen render of every panel, a photograph of the window as the window server drew it, an assertion that no other instance holds the audio devices, a release-build bit-exactness measurement, and a flow check driving the whole interface against live hardware.
A run that skips a step reports it, and a narrowed run lists everything it did not cover, so a ten-second run cannot be mistaken for a complete one.
Photographs are written to build/screenshots. The gate can establish that a
photograph was taken; it cannot establish that the layout in it is correct.
Each step, and each probe beneath it — the soak test, the echo-cancellation seam, the far-end ring — is documented in docs/verification.md.
Sources/
YunAudioRT/ C shim for os_workgroup and the allocation tripwire, both
unavailable from Swift
YunAudioHAL/ device enumeration, aggregate devices, process taps,
stream formats, clock analysis
YunAudioEngine/ the IOProc, routing matrix, clock anchor publisher,
voice isolation, self test
YunDesign/ the design system, as SwiftUI
YunAudioControl/ the command vocabulary and the control socket, shared by the
application, the command line and the MCP server
YunAudioApp/ the application
yunaudio-cli/ the verification harness, and the command line
yunaudio-mcp/ the MCP server
Driver/ YunAudioDriver.driver, an AudioServerPlugIn
App/ bundle assembly, the icon, and verify.sh
- Issue #9 remains open. A previous session left
coreaudiodand the system Sound menu degraded after YunAudio had exited. Version 0.1.2 removes measured driver, teardown and ownership defects which made that class of failure more likely, but the isolated system-audio recovery validation in issue #9 is still incomplete. Keep the removal command available when using the optional driver. - The app and driver are ad-hoc signed. Distribution without a first-launch dialog, or preserving privacy grants across binary updates, requires a Developer ID identity and notarisation. Sparkle verifies the feed and archive with Ed25519, but an ad-hoc host must disable Library Validation to load the independently signed updater framework.
- Voice isolation causes
AudioUnitRenderto allocate on the IO thread, at approximately 0.3 allocations per cycle, from within Apple's model. The bypass path remains at zero. No dropout has been observed, but the realtime contract is broken while it is enabled. - A driver fault takes
coreaudioddown with all attached system audio. The removal command above is worth keeping to hand. - The virtual device's input level control is implemented but verified only by
inspection; it has not been moved through System Settings on-device, because
installing a driver restarts
coreaudiod. - Echo cancellation costs the clock lock and bit-exactness, and adds a buffer of latency in each direction. This is structural rather than a defect: the canceller must own the microphone and the speaker together, so the microphone leaves the router's aggregate and the clock master becomes the destination. It is disabled by default, and the interface states the cost before it is enabled.
Full list, including approaches that were measured and rejected: docs/limits.md.
An index in all three languages: docs/.
| MEASUREMENT.md | The bit-exactness method, and the limits of what it proves |
| docs/verification.md | The gate, every probe beneath it, and their blind spots |
| docs/features.md | All twenty-one capabilities, with measurements |
| docs/automation.md | CLI, control socket, MCP, scripting, OBS, MIDI |
| docs/hardware.md | Razer HID control, and how the protocol was established |
| docs/limits.md | What does not work, and what was rejected |
| DEVICES.md | Per-device hardware facts, and how each was verified |
| AGENTS.md | The working agreement for changes to this project |
Apache 2.0. See LICENSE and NOTICE.
CONTRIBUTING.md is how to build it, what a change needs before it can be merged, and the two operations that require a human. AGENTS.md is the longer working agreement behind it: the invariants, why the four interface checks are each blind to what the others catch, and the approaches already measured and rejected.
Security reports go through SECURITY.md rather than a public
issue — the virtual device is loaded into coreaudiod and carries every other
application's audio.









