Imported from cmicali/vibe (
Vibe/Audio/Mac/Devices/AGENTS.md). Install upstream withnpx skills add cmicali/vibe --skill Devices. Copyright stays with the author.
Output devices (macOS only)
The CoreAudio HAL output-device layer: AudioDevice, AudioDeviceManager, AudioDeviceEventLog, CoreAudioUtil, OutputFormatRules.h, AudioPlayer+Devices, and AudioPlayer+BitPerfect. The iOS counterpart is Audio/iOS/, which is an AVAudioSession and nothing like this.
CoreAudioUtil is three files. CoreAudioUtil.m keeps the bounded read and the device identity reads. Its +BitPerfect category holds transport, rate, physical format, volume and hog. Its +Diagnostics category holds Save Debug Info's read and the HAL event log's formatter. All three are declared in CoreAudioUtil.h, and they share their HAL helpers through CoreAudioUtilInternal.h.
CoreAudioUtil.diagnosticDescriptionOfDeviceID: is the one read-everything call (formats, declared latency, clock, exclusive owner). It exists for Save Debug Info (Mac/App/AGENTS.md), and nothing that changes a device reads it. The player's side of that report is bitPerfectReportDictionary and outputDeviceDiagnosticSnapshot, which dump_state reads too. The snapshot reads on the player queue, so shipping callers invoke it off main.
AudioDeviceManager
A singleton that owns device-change listening. At init it registers CoreAudio listeners for the default output device and the device list, kept for the process's life, then sweeps. outputDevices serves the latest immutable snapshot and waits at most 250 ms for the first one, so an unavailable coreaudiod cannot strand a caller's serial queue. cachedOutputDevices, knowsOutputDeviceIsAbsent: and outputDeviceForId: never wait, because the player queue calls them.
With VIBE_VERBOSE_LOGGING=1, the HAL: event log (AudioDeviceEventLog) follows published devices, watched device and stream property changes, arrivals and removals, default and alert output changes, and coreaudiod restarts. TRAP: HAL delivers running/overload notifications synchronously on its IO thread, and a property read there can deadlock its stop. A delivery queue does not make them asynchronous: HAL waits for that queue. So the event-log listeners register with a NULL queue, copy the addresses, and enqueue their reads on _eventLogQueue (AudioDeviceEventLog.listenerForObject:name:queue:). Playback never waits on that queue, and its work changes no playback state.
A snapshot is normally published only after every read agrees: the device list, the system default, each device's output-channel, name and UID properties, and the default appearing in the list. A partial result keeps the prior snapshot and takes the two-second retry, so a transient HAL failure cannot look like removal and persist a false System Output fallback.
But strictness is bounded, because a persistent failure is not a transient one. A driver that always fails a property read, a device with an empty name, or a default with no output channels would otherwise keep every device unpublished for the process's life. After kMaxIncompleteSweeps (3) refusals in a row, one sweep publishes what it could read, omitting the devices that failed, and logs that it did. The three inputs with no partial form (the list, its size and the system default) still return nil however many sweeps failed. AudioDevice.transportType is an optional refinement that never discards a device.
TRAP: absence in outputDevices is not removal. It answers @[] both for "no output devices exist" and for "no sweep published yet". Every decision that treats absence as removal, and so falls back to System Output and persists it, asks knowsOutputDeviceIsAbsent: instead. That answers NO until there is a real snapshot, without waiting for one. Its callers are the player's device-list observer, setOutputUnitDevice:, and the isAbsent: blocks that retire the hog, listener and format-restore obligations. The rate edge cannot wait for a snapshot, since a listener fires before the list catches up. It asks the device itself (deviceIsConfirmedDead:), off the player queue.
TRAP: never call outputDevices from _refreshQueue. Setup, and the resolution completions its first sweep delivers, run there. A call from inside them waits out the full 250 ms on a setup that cannot finish until it returns.
refreshOutputDevicesCache runs only on that serial refresh queue, so an older sweep can never overwrite a newer one. initWithEnumerator:retryScheduler: lets the host-less suite control the HAL and timer boundaries. refreshOutputDevicesWithCompletion: is the shared refresh entry for notifications and tests.
Observers are fanned out on main in the common run-loop modes, not with a plain dispatch_async. GCD main-queue blocks do not run during menu tracking, and this way the devices menu refreshes while open. Observers (AudioDeviceManagerObserver) are held weakly: AudioPlayer, OutputDevicesMenuController and SettingsAudioOutputViewController.
AudioPlayer+Devices
All device management: resolution, switching, the rate the pipeline runs at, and parking or falling back when a device vanishes. Its (PlatformOutput) implementation owns the output unit's device half: binding it, starting it under exclusive access, feeding it the device's rate. AudioPlayer+Pipeline owns the platform-blind half. The unit itself, Audio/AudioOutputUnit, is shared with iOS, and everything device-shaped in it (the bind, the HAL latency reads) is TARGET_OS_OSX. (DevicesInternal) owns the device policy. It is declared in AudioPlayer+DevicesInternal.h, with the device ivars and bit-perfect's (BitPerfectInternal). Only AudioPlayerInternal.h imports that header, under TARGET_OS_OSX. So it is on the player's private surface. Its public half, (Devices), has its own header, which the macOS shell imports beside AudioPlayer.h.
The unit waits on the HAL so the player queue never does
Binding, configuring, starting and stopping each wait on a device's IO thread: hundreds of milliseconds on some interfaces, seconds for a waking DAC. The unit's methods are called on the player queue and record their effect at once. deviceID, format and running answer the state the unit is headed for. The HAL work runs on the unit's own serial queue in call order, so device changes and transport stay ordered and the player's rebind still commits synchronously.
- A stop closes the gate before it returns, so the proc is never called on a unit still at its previous device or format.
- A start superseded by a later start or stop never runs (
runGeneration). The start opens the gate, then re-checks, so a racing stop always wins. - A refusal arrives later. A bind is refused at once only for a device the snapshot knows is gone (
setOutputUnitDevice:, no HAL read). Any other refusal of a bind, configure or start reachesfailureHandlerandoutputUnitFailedOnQueue:. Unless a later start or stop owns the unit, that stops the output, parks the current voice Paused and reportsEngineStartFailedto the owning play. A refused bind reportsDeviceUnavailable, and the unit forgets that device. Run generations come from one counter every unit shares, so another unit's never match. A missing unit still refuses synchronously. runningis true from the request, so a start in flight counts as rendering.
TRAP: the unit's own reads of its device are on its queue too. Its latencies (after each bind, configure and start) and its channel map (after each landed bind and configure) are read there and answered from fields (channelMap, presentationLatency, bufferLatency; AudioOutputUnit.h). Read live, a hung device held the player queue 30 s per HAL call, and main with it through dump_state.
What the player queue still reads of a device is bounded. The bound device's nominal rate after each bind (followOutputDeviceRateOnQueue), the audio path's device stage and the report's device reads each go through CoreAudioUtil.performBoundedRead:. That is one serial reads queue the caller waits on for at most 0.5 s. A late rate is followed when it comes, and while one read is overdue nothing waits. The bind and System Output's default ask the manager's snapshot, never the HAL. Still read unbounded on the player queue: the system default once, when the unit is created, and bit-perfect's own reads and writes, which the mode blocks at by design (below).
TRAP: a device write on the player queue must waitUntilIdle first. The stop it follows is only queued, and a hogged device's format written under a running output strands its next start with error 35. The physical-format write and confirmation (prepare and restore, one funnel) and a change of hog ownership each wait. So bit-perfect and exclusive output block at those edges and nowhere else. Teardown stops the unit and waits too, so no queued start can pull a freed master bus.
TRAP: AUHAL can reinitialize before its device-format notification and keep the old converter. The device's confirmed rate can precede the unit's notification, with its converter still at 48 → 44.1 kHz. So AudioOutputUnit.halConfigureFormat: creates a fresh HALOutput component, binds the current device, then sets the client format and initializes. It keeps the wrapper, queue, render gate and run generations, so stop and start ordering is unchanged. RemoteIO is not recreated for a format change.
Resolving the saved device
The player's async init asks the manager to resolve the saved device without blocking the player queue. resolveOutputDeviceForUID:modelUID:name:completion: waits for the first successful snapshot and matches by device UID, then model UID, then name. The player starts on System Output, bound to the system default at its rate. While that device lives, the unit moves only when a rebind moves it. Nothing follows the system default on its own. An unmatched saved preference stays pending during ordinary playback. With bit-perfect armed, a published absence disables the mode and commits System Output. A match is committed when the synchronous rebuild accepts the selection. The HAL bind runs later on the output unit's queue and may still fail. If playback wins the race, the intent stays pending for the next eligible edge.
TRAP: a class-compliant USB interface's device UID is its USB location, not the unit. AppleUSBAudioEngine builds it from manufacturer, product and locationID, so the same interface has another UID in the next port. Such a device exposes no serial number. kAudioDevicePropertyModelUID is the documented, port-independent identifier for the model, and is what finds the device on its new port. It is compared as an opaque token and never parsed. Vendor drivers often key the device UID on a serial, and Bluetooth devices on the MAC address, so for them the UID is already per unit. Until the rebuild accepts a selection, currentlyRequestedAudioDeviceId stays -1 and the saved preference stays pending.
Where that deferred bind may land is VibeCanBindSavedOutputDevice: whenever nothing is audible. That is Stopped, Loading while the output is not running, or a Paused track. Loading is included because a launch by double-clicking a file starts an open within milliseconds of the async init. Without it the first track would play through the system default. TRAP: an ordinary mid-session track change is also Loading, with the outgoing voice still fading on a running output. Rebinding there would stop the output under that fade and click, which is why the rule checks the output. Playing is excluded, since a failed bind there tears down live playback. A pause is not excluded. A vanished device's playback continues on System Output with its identity pending, and a pause is the first silent moment that can move it back. The rebuild restores a paused track as Paused, and the pause verb calls the resolver.
TRAP: returning devices resolve synchronously against cachedOutputDevices. An asynchronous answer can lose to a local open that moves Loading to Playing and makes the bind ineligible. Only initial discovery waits, and its callback re-reads the current preference and snapshot so it cannot revive an older choice. TRAP: hold _pendingSavedDeviceLookupInFlight across every bind, cached resolutions included. A refused bind publishes Stopped, and its reset must not queue another attempt. Other resets defer resolution to the next queue turn, after the enclosing rollback. An absent result with bit-perfect off does nothing, keeping the intent without new lookups.
didChangeOutputDevice: is sent on every settled mutation, not only when the id moved. A refused selection re-announces the retained choice, which repairs a UID cleared by a Settings reset. A still-pending saved preference is kept, not overwritten with the temporary binding. The delegate persists the choice and drives the menu checkmark, and is idempotent. Suppressing an unchanged value would leave a path that made Settings and the binding disagree with no call to resynchronize them.
A vanished device and an absent launch preference share the pending identity slot. Falling back disables unavailable modes and follows System Output without forgetting that identity, so a later notification or silent transport edge can re-adopt it. Every -1 announcement derives its involuntary reason from the pending slot and passes it to the delegate by value, captured before the hop to main. Only an explicit System Output selection or a successful concrete selection clears pending intent.
System Output's default is the snapshot's isSystemDefault device, never a HAL read on the player queue. The manager refreshes before either observer fires. A default read there waits behind a busy coreaudiod and holds every transport command. No snapshot yet, or devices with none the default, is unknown, not absence: macOS names no default for a moment while it moves it. Playback is kept, one coalesced delayed bind is tried, then it waits for a notification. Only a published snapshot with no output device parks playback. The menu checkmark follows currentlyRequestedAudioDeviceId (Mac/Menu/).
Device edges
The edges are the manager's two observers, the bound device's listener in every mode (its nominal rate and its hog owner), and, while a bit-perfect device is prepared, that device's one listener.
- The system default moving reaches
setOutputDeviceOnQueue:-1when System Output is the policy, and its rebind moves the unit. - The device list changing asks
knowsOutputDeviceIsAbsent:and falls back. - A rate other than the pipeline's on the bound device, not confirmed dead, is another process moving it (Audio MIDI Setup, a DAW, the loopback verifier). Both are read on
performBoundedRead:'s queue.configureOutputDeviceOnQueue:rebinds in place, following the rate and re-voicing. TRAP: a hosted unit left at the old rate renders nothing. - The prepared device's listener sees the same rate event, and there the prepare sets the mode's format back instead.
Vibe's own format writes arrive with the pipeline already at the rate, a no-op. The hosted unit never stops on its own. When its device vanishes (unplug, a coreaudiod restart, which renumbers every device), AUHAL moves it to the system default and keeps pulling, and the device-list observer's rebind makes that explicit. So macOS has no configuration-change recovery.
Another app taking the bound device parks playback and says so. TRAP: a foreign hog halts the unit's IO without stopping it (no IsRunning change), so the transport would read Playing over a frozen position. The listener reads the hog owner in the same bounded read as the rate, and so does the read after every bind. _boundDeviceHeldElsewhere records it. Taken under Playing, followBoundDevice:toRate:heldElsewhere: parks the voice as a lost device is parked and reports VibeAudioErrorDeviceInUse, shown on the parked track's info line (Mac/MainWindow/AGENTS.md). Under any other state it stops the output, so a Loading settlement cannot start into the frozen device. While the flag stands, startOutputUnitOnQueueWithError: refuses every start with the same error. A resume stays Paused, and a new play lands Stopped. The refusal is policy, not the HAL's: a unit started under a foreign hog plays, and would mix into the output another app asked to have alone (CoreAudioUtil.h). The release clears the flag and nothing else. Playback never restarts on its own.
A rebind stops the output and keeps the current voice. The stop kills every fading voice. The current voice keeps its ring and gain, and a restart continues it in place. Only when the source segment must be rebuilt (the mode or the output's format changed) does the voice die with it, and the rebind starts a new one at the retained intent. Either way the successor is re-queued after (Audio/AGENTS.md). Selecting the active device still rebuilds when bit-perfect is wanted but the device is not prepared, so a newly eligible selection prepares the current track.
Losing an output parks the current voice where it is. parkPlaybackForMissingOutputDeviceOnQueue stops the output, killing every fading voice, and pauses the current voice with a cut that lands at the first render after any restart. No schedule survives to fire a false track end. A failed re-adoption at natural end cannot clear the already-captured finish settlement. A track end captures its track and submission before optional device recovery, since a failed bind resets the active identifier.
Bit-perfect output
AudioPlayer.setBitPerfectOutput:exclusiveOutput:enableFX:allowAnyDevice: (Settings > Audio) requests an output path that preserves decoded samples. The report identifies any format, channel or volume mismatch. The mode is AudioPlayer+BitPerfect. Its public half, (BitPerfect), is the setter and the report, in its own header, which the macOS shell imports beside AudioPlayer.h. The queue-side mechanism is (BitPerfectInternal), declared in AudioPlayer+DevicesInternal.h.
Declick (AppSettings.declick, default on, Audio/AGENTS.md) decides what the mode does at a transport edge. On, each play, seek, pause, resume, stop or track change gets the ≤10 ms declick ramp, and every other frame, a gapless boundary included, is unchanged. Off, the edge is a cut that leaves every sample untouched. It reaches the player as AudioPlayer.declick through VibeSettingsLiveEffectDeclick, with no rebuild. effectiveCrossfadeMilliseconds clamps the crossfade to the declick minimum. Declick off removes transport gain ramps. Player volume still applies and reports VolumeScaled below full. The loopback verifier turns Declick off around its captures, which compare from the first frame.
Both modes belong to the destination UID. AppSettings remembers only enabled modes, keeping entries while devices are unplugged. The shell supplies them through audioPlayer:outputModesForDeviceUID:bitPerfectOutput:exclusiveOutput:, a read-only query safe from main and the player queue. Explicit selection and saved-device resolution share selectOutputDeviceOnQueue:. It reads the destination's modes before the one rebuild, and restores the previous flags if the switch commits nothing.
- TRAP: a failed rebuild may already have rewired or prepared the destination. After restoring the flags, a Stopped player stops the output and takes
leaveOutputDeviceOnQueue, retiring the destination's format, listeners and hog obligations. A player still holding its track keeps it. - TRAP: a name fallback can resolve different hardware. Read the destination's own modes before preparing or hogging it, never the missing device's. Pushing modes after a bind would first rewrite the format of, and possibly hog, a device whose mode is off.
- The one exception is a model-UID match under a new device UID: the same model on another USB port. Only when the destination has no saved modes does
selectSavedOutputDeviceOnQueue:read the old UID's (_modesUIDForNextSelection).carriedModesFromUID:carries that decision by value into the delegate callback. The shell persists only that carry, on main, throughAppSettings.carryOutputModesFromDeviceUID:toDeviceUID:. A manual selection never infers a carry. The carry copies, so the old port keeps its entry, and never overwrites the destination's own modes. Two identical units of one model share modes, because the OS cannot tell them apart. - Until the main-thread callback has saved a carry, it stays readable through
_unpersistedOutputModeSources, under_stateLock. Queued FX edits resolve it transitively, consecutive port changes included. The callback clears only its own entry. Never call main synchronously from the player queue to persist a carry. - Queued mode and FX edits re-read the queue's current UID (or the pending saved UID), so a snapshot submitted behind a switch cannot overwrite the destination's preferences. System Output has no UID and reads off. If its default read fails, its committed mode change still rebuilds the current binding: an unknown read keeps playback, while confirmed absence parks it.
- The public selection completion runs on main after the device callback, failed switches included, so the shell can release its mode-edit guard.
setOutputDeviceOnQueue:also rebuilds when only the standing graph disagrees with the flags. A switch from a bit-perfect device to System Output may stay on the same hardware but must restore its format and FX route.
The decisions are OutputFormatRules.h. Raw HAL reads and writes are CoreAudioUtil's. The loopback oracle (scripts/test/verify-bit-perfect.swift) measures 44.1, 48, 88.2 and 96 kHz WAVs and a 96/24 FLAC sample-exact.
Exclusive output is a separate opt-in, off by default. Bit-perfect, exclusive output and the saved FX preference land on the player queue in one setter. They use the same-device rebuild, which keeps the track, position and pause state, reading getPlaybackIntent:forTrack: before stopping the output. Changed settings stop the output before changing ownership. Identical settings return without rebuilding. An exclusive preference changed while bit-perfect is off is saved without touching the graph. Disabling exclusivity releases the device without restoring its format. Disabling bit-perfect restores the format and releases it through leaveOutputDeviceOnQueue, inside the rebuild. While saved-device discovery is pending, turning bit-perfect off still works on the current binding. An in-flight open also rebinds the warm output without consuming the request, so its settlement starts with the latest preference. The saved exclusive preference survives a bit-perfect toggle, but acts only while bit-perfect is on. With an eligible device prepared, it is hogWanted.
Off is dormant. Format preparation and reporting gate on _bitPerfectWanted. Disabling publishes an Off report and removes the prepared-device listener after restoring the device. Ordinary playback still reads the bound device's rate and hog owner. A failed restore, listener removal or release is kept and retried at the next cleanup edge, never by polling. Listener removal retries once and keeps its handle on failure, refusing a second device until it succeeds.
The format rides the play settlement; the hog rides the running output. prepareOutputOnQueueForFile: runs in finishPlayOnQueueWithFile:… after the request is consumed and before the source segment is reconciled. It also runs in rebindOutputOnQueueToDevice:, which configureOutputDeviceOnQueue: calls. It reads the bound device's first output stream once and applies two rules:
- The rate rule: exact, else the smallest integer multiple, else untouched. It scans PCM offers carrying every source channel and continuous ranges, including 3x, 6x and ratios above 16x. Encoded-only rates cannot hide a usable PCM multiple.
- The depth rule: the widest integer depth the device offers, never one below the source's, else the widest float format. Float and lossy sources take the float format first, since what reaches the device is a float32 decode. The bits added to an integer source are zero. A sample the player changes (its volume, a declick) is then rounded at the widest depth, never at 16 bits without dither. One depth for every integer source also lets a 16-bit and a 24-bit track at one rate share a format. Moving between them needs no switch, and a splice between them can arm. The chooser uses the device's advertised formats.
It rejects physical formats with fewer channels than the source. When the device's format or the bus's rate differs, it stops the output itself (both callers guarantee nothing is audible). It writes one kAudioStreamPropertyPhysicalFormat and polls for confirmation with a 1.5 s deadline. Individual HAL calls can block longer. It then brings the pipeline to the device's rate through applyOutputRateOnQueue:. TRAP: an accepted format write precedes the nominal rate. setOutputFormatOnQueue:stream:device: confirms every physical-format field but the reserved word, and the nominal rate, before the pipeline follows. Matching rate, depth and channels alone cannot confirm a restore with different packing or byte order. Preparation and restoration share it.
acquireExclusiveOutputOnQueue runs in startOutputUnitOnQueueWithError:, before the unit starts. releaseExclusiveOutputOnQueue runs after the idle stop's output stop, before a rebind, on mode off and at quit. CoreAudioUtil.supportsHogModeForDeviceID: checks that the hog property exists and is writable, for physical and virtual devices alike. That is a capability check, and the owner read-back decides success. Loopback recording requires Exclusive output off.
TRAP: setting kAudioDevicePropertyHogMode ignores the value and toggles ownership. So setHogOwnedByThisProcess: reads first and writes only when the owner has to change. "Owned by this process" is the whole state, and a release while another process holds it is already true. The sandboxed build needs no entitlement for it. _hoggedDeviceID records the obligation before the take, since a successful write followed by a failed read-back may own the device. Release retries once, then keeps that slot until a release succeeds or the manager confirms the device gone. A second device cannot overwrite it. The report reads the prepared device's actual owner.
Taking the system default output device moves the default; the hosted unit stays. coreaudiod answers the hog by moving the default to another device. A HALOutput unit bound to the device stays bound through the take. So acquireExclusiveOutputOnQueue returns once the hog is confirmed. configureOutputDeviceOnQueue: keeps the hog, the format slot and the prepared device on a rebuild of the device already held.
The pipeline runs at the bound device's rate, in every mode. The bus, the varispeed, the FX chain and the unit all run at the rate applyOutputRateOnQueue: last applied. followOutputDeviceRateOnQueue reads the nominal rate after every bind, bounded. The prepare applies the rate its format write settled on. Any file-rate conversion stays in the voice decoder. The master bus and output unit share one rate. applyOutputRateOnQueue: stops the output, reconfigures the unit and sets the master bus's format, which re-hosts a connected FX chain and replaces the meter. TRAP: the source segment stays at the old rate for the caller to reconcile, which re-voices when it rebuilds. Rebuilt inside applyOutputRateOnQueue:, the caller's reconcile would find the bus already at the rate and never start the replacement voice: silence while Playing. A format the HAL refuses fails the next start, which parks the voice through the refusal path above. outputNeedsSwitchOnQueueForFile: still counts a bus rate that differs from the chosen one as a switch, for a rate moved by another process before its listener landed.
A switching settlement stops the output before changing the device format. An unreadable format is unknown compatibility and refuses a gapless successor. The settlement rejects an obsolete open identifier before any device query. A format switch kills every fading voice. Bit-perfect fades nothing but its declicks, so a cut there costs only a declick.
The chain is pruned by the switch, not the device. With the mode on, ensureSourceSegmentOnQueueRebuilt: (AudioPlayer+Pipeline) builds the bus at the output's format, which is the file's rate once the prepare moved the device there. It has no varispeed stage. The ordinary varispeed also bypasses its kernel at zero pitch. The FX segment is disconnected before preparation, and leaving bit-perfect reconnects it when FX is enabled. Every settlement reconciles the segment right before the voice starts, so a toggle during an open lands on the chain the mode wants. The device rebind reconciles it on the current track, which is how both mode toggles rebuild the current track in place.
Restore is one slot. The first format change to a device in a run remembers the device, its stream and its format. Mode off, a switch to another device and prepareForTermination retry the write once. They clear the slot only after success or a confirmed removal. leaveOutputDeviceOnQueue restores, forgets the prepared device and releases the hog, in that order. A failed restore stays in the slot for the next leave, prepare or quit. Another device may play at its existing format but cannot change format until then. CoreAudioUtil.releaseDeviceObligation:attempt:isAbsent: is the shared retry loop for restore and release. A vanished device takes the off path: abandonBitPerfectForVanishedDeviceOnQueue drops the mode, restores and releases before the fallback binds System Output. The device's stored modes survive.
Eligibility is an allowlist of transports (VibeBitPerfectDeviceEligible): built-in, PCI, USB, FireWire, Thunderbolt, HDMI, DisplayPort, AVB and virtual. Bluetooth, AirPlay, Continuity, the Remote types, aggregates and Unknown are out, and so is anything Apple adds until argued in. The System Output policy is the absence of a chosen device, and the current default device is judged like any other. Advanced's allowBitPerfectOnAnyDevice bypasses the allowlist for testing. It changes eligibility, not the device's capabilities, and changing it rebuilds an enabled mode. The switch, the Output menu and the report all read the one rule. The device the mechanism drives is the requested id, or the destination while a switch is in flight (_rebindDeviceID, set during configureOutputDeviceOnQueue:). The switch prepares and hogs its destination before setOutputDeviceOnQueue: commits the id, since a failed switch must not commit. It is never the unit's own device, which names the one being left.
The report (VibeBitPerfectReport, bitPerfectReport) carries the status and every fold input. It also carries the file's depth (sourceBits), which the Active caption shows. The device runs at its widest integer depth, so its own depth says nothing about the file. Each publication computes it from its owners: the mode, the pipeline, the chosen device, the hog, the current file's source format (so a gapless promote reports the promoted file), the prepared physical format, the HAL's volume, balance and mute, and AudioPlayer.volume. A player volume below full reports VolumeScaled. Its setter refolds the report with no device read (refoldBitPerfectReportForPlayerVolumeOnQueue:), since a slider drag lands there every tick.
formatConfirmedcompares the live format against what the prepare asked (_prepared*). It also requires no varispeed, the FX disconnected, the unit bound to that device, the bus and unit at the physical rate, successful format and control reads, and the device listener installed. A device at the right rate cannot confirm a pipeline that still resamples.- An unconfirmed output reports SwitchFailed before a rate mismatch can report RateUnsupported. A prepared fallback rate still reports RateUnsupported.
channelsMatchrequires the source, the decoder, the bus and the unit's input to carry the source's channel count. The bus is stereo, so a wider file reports ChannelConversion. The unit'sChannelMapmust route those channels in order into the prepared stream, every other hardware output silent. So wider interfaces qualify without a downmix, swapped pair, duplicate output or unknown map.- Mute has its own status even at full volume. Off-center balance or driver pan is VolumeScaled even at 100%. The reader checks the driver's main volume and mute and every source channel in the prepared stream. Missing optional controls mean unity, center and unmuted. Unreadable controls cannot confirm the output.
depthOKrejects non-PCM formats and asks whether the file's processing format carries the source as well as the device's format does. The handle decodes to float32, whose significand is 24 bits, so a 32-bit integer source is never Active.
The publication rides existing edges: refreshOutputAudioActiveOnQueue (every state publication and fade completion), the end of a device switch, the two hog edges and the mode toggle. It also rides one HAL listener on the prepared device: its output properties, every element, and its nominal rate. A changed report is announced through audioPlayerDidChangeBitPerfectReport: on main, the one edge the header's lock and the Settings caption redraw from. A publication's device reads are one bounded read, and the output controls are cached. The physical format, the hog owner, the starting channel and any uncached controls are read together through performBoundedRead:. A device that does not answer in 0.5 s reads as unconfirmed. The volume, balance and mute reads (ten-odd, about 20 ms per track change on one USB interface) are kept per prepared device and stream while _outputLevelListener is registered. That listener clears the cache before it republishes, a fresh registration clears it too, and a failed read is never kept. A Loading publication asks for no channels and reuses the last track's reading. Beta builds log each fresh read and any publication whose device reads took 2 ms or more.
The gapless splice is gated on the mode too. A queued successor keeps the device's format, so maybeArmSuccessorOnQueue asks outputNeedsSwitchOnQueueForFile: (and VibePCMFormatsMatch on the two processing formats). A next file that wants another format, its channel order included, takes the settlement, which switches the device or rebuilds the bus. The idle stop waits 6 s in both modes, and longer while an FX tail rings (Audio/AGENTS.md).
Termination. AppDelegate detaches the delegate on main. prepareForTermination (from applicationShouldTerminate:) closes admission and uses stopOnQueue to retire the voice and cancel the open, the park and the successor. It then stops the output and restores and releases device obligations. Discovery, notifications, playback and prefetch cannot restart it during NSTerminateLater.
