How vorssaint-utils Detects and Reconciles Frozen Audio Engines After Sleep

vorssaint-utils detects frozen audio engines by listening for macOS wake notifications, clearing stale render state, and running a reconciliation pass that tears down wedged engines and rebuilds them fresh.

When a Mac wakes from sleep, the underlying CoreAudio HAL can leave per-application audio engines in a "wedged" state—where the tap's aggregate device remains active but the render callback stops firing, causing applications to remain silent. The vorssaint/vorssaint-utils repository solves this through a systematic detection and recovery pipeline implemented in the audio mixing layer. This article breaks down the exact mechanisms used to identify stalled engines and restore audio functionality automatically.

The Problem: Wedged Audio Engines on macOS Wake

The audio mixer in vorssaint-utils runs a per-app "engine" that taps each application's audio stream, applies volume and routing changes, and writes the result back to the system output. During normal operation, these engines maintain render callbacks that process audio buffers in real-time. However, when macOS wakes from sleep, the CoreAudio HAL may leave an engine's aggregate device active while silently stopping its render callbacks. This creates a wedged engine—the system believes audio is flowing, but no buffers are being processed, resulting in silent applications.

Wake Detection and Immediate Response

The recovery process begins the moment the system signals a wake event. The mixer registers for system notifications during initialization and responds by purging stale state and forcing a full audit of all active engines.

Listening for NSWorkspace.didWakeNotification

In AppVolumeMixer.swift, the start() method registers an observer for NSWorkspace.didWakeNotification during mixer initialization. This observer triggers a dedicated wake handler that performs four critical operations in sequence:

  1. Clears cached render observations from engineRenderProgress
  2. Clears any pending recovery state from engineRecovery
  3. Forces a full refresh of the application list via refreshApps()
  4. Immediately executes reconcileEngines(with: self.apps) to audit all engines
// AppVolumeMixer.start() – registers the wake observer at lines 188-199
// Source: Sources/Vorssaint/Services/Audio/AppVolumeMixer.swift

This sequence ensures that no stale data from the pre-sleep state interferes with the post-wake reconciliation.

The Reconciliation Pipeline

The reconcileEngines(with:) method serves as the core detection logic, examining every live engine to determine if it has stalled and requires teardown.

Detecting Stalled Render Cycles

At lines 1159-1205 of AppVolumeMixer.swift, the reconciliation logic compares each engine's render count against its previous state. If the render count has stalled while the parent application is still reporting playback activity, the engine is classified as wedged. The mixer immediately tears down wedged engines, allowing the system to create fresh taps with functional render callbacks.

The detection heuristic relies on two conditions:

  • Render stall: The engine's renderCycles counter has not incremented since the last check
  • Active playback: The application reports ongoing audio session activity

When both conditions are met, the engine is marked for destruction rather than reuse.

Handling Temporary Object Loss

If an application temporarily loses its audio objects during the wake transition, the mixer applies a short teardown delay to avoid premature engine destruction. If the objects do not return within the grace period, the engine is discarded. After processing all existing engines, the mixer calls applyRouting(for:) to instantiate new engines for any applications that now require audio taps.

Scheduling Delayed Rebuilds

Some engines require additional time before they can be safely rebuilt—typically when waiting for hardware devices to become available. The scheduleEngineReconcile(after:) method (lines 66-73) posts a one-off dispatch after the specified delay, keeping the system event-driven rather than blocking the main thread with busy-waiting.

// Scheduling the next reconcile pass
// Source: scheduleEngineReconcile() – lines 66-73

This mechanism ensures that engines dependent on specific hardware readiness states do not enter retry loops that consume CPU resources.

Practical Code Examples

You can manually trigger the wake reconciliation sequence for testing or custom recovery scenarios:

// Example: Manually trigger a wake-reconciliation (used in tests)
AppVolumeMixer.shared.wakeObserver?.let {
    // Simulate the wake callback
    AppVolumeMixer.shared.engineRenderProgress.removeAll()
    AppVolumeMixer.shared.engineRecovery.clearAll()
    AppVolumeMixer.shared.refreshApps()
    AppVolumeMixer.shared.reconcileEngines(with: AppVolumeMixer.shared.apps)
}

To force a reconciliation after changing output devices or other asynchronous events:

// Example: Force a reconciliation after a custom delay
AppVolumeMixer.shared.scheduleEngineReconcile(after: 0.5)   // half-second later

For debugging specific engines, inspect the render progress dictionary:

// Example: Inspect an engine's render status
if let progress = AppVolumeMixer.shared.engineRenderProgress[app.id] {
    print("Engine \(app.id) rendered \(progress.renderCycles) cycles")
}

Key Source Files

The reconciliation system spans multiple files within the vorssaint-utils codebase:

Summary

  • Wake detection relies on NSWorkspace.didWakeNotification observers registered in AppVolumeMixer.start().
  • State invalidation clears engineRenderProgress and engineRecovery to prevent stale data from corrupting the recovery process.
  • Stall detection identifies wedged engines by monitoring render cycle counts while applications report active playback.
  • Automatic teardown destroys wedged engines immediately while scheduling delayed rebuilds for engines waiting on hardware availability.
  • Event-driven architecture uses scheduleEngineReconcile(after:) to avoid polling loops and maintain efficient resource usage.

Frequently Asked Questions

How does vorssaint-utils know when to check for frozen engines?

The mixer registers for NSWorkspace.didWakeNotification in the start() method of AppVolumeMixer.swift. When macOS emits this notification after waking from sleep, the mixer's observer callback immediately triggers the reconciliation pipeline without requiring periodic polling.

What specific condition identifies a "wedged" audio engine?

An engine is considered wedged when its render cycle count has stopped incrementing while the associated application still reports active audio playback. This condition is evaluated in reconcileEngines(with:) at lines 1159-1205 of AppVolumeMixer.swift, where the mixer compares current render progress against previous observations.

Can I manually trigger the reconciliation process without putting the Mac to sleep?

Yes. You can manually invoke the same sequence used during wake recovery by clearing the progress caches, refreshing the app list, and calling reconcileEngines(with:) directly on the shared mixer instance. This approach is commonly used in unit tests to verify recovery logic without system sleep cycles.

Why does the mixer wait before rebuilding some engines?

Certain engines depend on specific hardware devices that may not be immediately available after waking from sleep. The scheduleEngineReconcile(after:) method queues a delayed rebuild rather than attempting immediate reconstruction, preventing resource contention and ensuring the hardware layer is ready before creating new audio taps.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →