Contents

ryanfrancesconi/spfk-au-host

Audio Unit (v3) hosting, validation, caching, and effects chain management for macOS and iOS.

Features

  • Effects Chain — Actor-based AudioUnitChain for loading, connecting, bypassing, reordering, and removing Audio Units in a serial chain between input and output nodes
  • Component Caching — XML-based cache system (AudioUnitCacheManager) for persisting validated Audio Unit component state across sessions
  • Component Validation — Multi-strategy validation pipeline using AudioComponentValidate, AudioComponentValidateWithResults (macOS 13+/iOS 16+), and external auval/auvaltool fallback (macOS only)
  • Preset Management — Factory preset loading via AudioToolbox APIs and user preset discovery from the ~/Library/Audio/Presets hierarchy (macOS only)
  • Full State Persistence — Plist-based serialization and restoration of Audio Unit full state dictionaries for project save/load
  • Host Musical Context — Tempo, time signature, beat position, and transport state blocks for AUs that need host timing information
  • Manufacturer GroupingAudioUnitManufacturerCollection for organizing available components into manufacturer-grouped hierarchies for menu display
  • Sendable Component WrapperS_AVAudioUnitComponent copies all relevant properties from AVAudioUnitComponent into a Sendable struct for safe cross-isolation use
  • Engine AbstractionAudioEngineConnection protocol decouples chain connection logic from AVAudioEngine, allowing the host to provide its own node attachment strategy
  • Component Observation — Real-time notifications for Audio Unit registration changes and component invalidation (plugin crash detection)

Architecture

SPFKAUHost
  |-- AudioUnitChain                Actor-based effects chain manager
  |   |-- AudioUnitChain+Connect    Connection, bypass, move, remove operations
  |   |-- AudioUnitChain+Insert     Insert, load chain description, AU instantiation
  |   |-- AudioUnitChain+Instantiate  Out-of-process/in-process effect and instrument creation
  |   |-- AudioUnitChainData        Actor holding the effects slot array and state
  |   |-- AudioUnitChainDelegate    Protocol combining AudioEngineConnection + AudioUnitAvailability
  |   |-- AudioUnitChainEvent       Events: insert, remove, bypass, move, connection error
  |   |-- AudioUnitDescription      AVAudioUnit wrapper with independent bypass flag
  |
  |-- AudioUnitCacheManager         Actor managing AU cache lifecycle
  |   |-- +Cache                    XML cache read/write/parse/remove
  |   |-- +Validation               System component discovery and validation pipeline
  |   |-- AudioUnitCacheEvent       Events: caching started, updated, loaded, validating
  |   |-- AudioUnitCacheObservation  NotificationCenter observer for component changes
  |   |-- ComponentCollection       Filtered views: passed, failed, unavailable effects
  |   |-- SystemComponentsResponse  Validation results container
  |   |-- Validation/
  |       |-- AudioUnitValidator    Multi-strategy validator (API, async, external auval)
  |       |-- ComponentValidationResult  Per-component validation state and metadata
  |
  |-- Definitions
  |   |-- AudioUnitManufacturerCollection  Manufacturer-grouped component hierarchy
  |   |-- AudioUnitPresets           Factory and user preset loading and full state I/O
  |   |-- AudioUnitStateNotifier    Parameter change notification via AudioToolbox
  |   |-- HostAUState               Musical context + transport state block provider
  |   |-- HostMusicalContext         Tempo, time signature, beat position
  |   |-- HostTransportState         Transport flags, sample position, cycle boundaries
  |   |-- S_AVAudioUnitComponent     Sendable copy of AVAudioUnitComponent properties
  |
  |-- Protocols
  |   |-- AudioEngineConnection      Node attach/connect abstraction
  |   |-- AudioEngineNode            Input/output node, bypass, detach, format access
  |   |-- AudioUnitAvailability      Available components and manufacturer collections
  |
  |-- Tests
      |-- TestAudioUnitContent       Mock delegate for unit testing without AVAudioEngine

Usage

Effects Chain

import SPFKAUHost

// Create a chain with a delegate that provides engine connection
let chain = AudioUnitChain(delegate: myEngine)
try await chain.updateIO(input: playerNode, output: mixerNode)

// Insert effects by AudioComponentDescription
try await chain.insertAudioUnit(componentDescription: reverbDesc, at: 0)
try await chain.insertAudioUnit(componentDescription: delayDesc, at: 1)
try await chain.connect()

Bypass and Reorder

// Bypass a single effect
try await chain.bypassEffect(at: 0, state: true, reconnect: true)

// Bypass entire chain
try await chain.bypassEffects(state: true)

// Move effect from slot 0 to slot 2
try await chain.moveEffect(from: 0, to: 2)

Component Cache

let cacheManager = AudioUnitCacheManager(cachesDirectory: cachesURL)
cacheManager.update(delegate: self)
cacheManager.update(cacheURL: nil) // uses default

// Load cached components or create fresh cache
try await cacheManager.load()

if cacheManager.validationIsNeeded {
    try await cacheManager.createCache()
}

Host Musical Context

let hostState = HostAUState()
hostState.musicalContext.currentTempo = 120
hostState.musicalContext.timeSignatureNumerator = 4
hostState.musicalContext.timeSignatureDenominator = 4

// Provide to Audio Units via their blocks
await chainData.update(hostAUState: hostState)

// Mutations are reflected live — no need to reassign blocks
hostState.musicalContext.currentTempo = 140

Manufacturer Collection for Menus

let groups = AudioUnitManufacturerCollection.createGroup(
    from: availableComponents
)

for manufacturer in groups {
    print(manufacturer.name)  // "Apple", "FabFilter", etc.
    for component in manufacturer.components {
        print("  \(component.name)")  // "AUDelay", "Pro-Q 3", etc.
    }
}

Engine Connection Protocol

// Implement AudioEngineConnection to provide your own AVAudioEngine
struct MyEngine: AudioEngineConnection {
    let engine: AVAudioEngine

    func connectAndAttach(
        _ node1: AVAudioNode, to node2: AVAudioNode, format: AVAudioFormat?
    ) async throws {
        engine.attach(node1)
        engine.attach(node2)
        engine.connect(node1, to: node2, format: format)
    }
}

Dependencies

| Package | Description | |---|---| | SPFKUtils | Plist utilities, process handling, and audio extensions | | SPFKTesting | Test case base classes (test target only) |

Platform Notes

Most functionality is cross-platform. The following features are macOS-only:

  • External auval validation — Falls back to AudioComponentValidate API results on iOS
  • User preset discoveryAudioUnitPresets.Locations and ~/Library/Audio/Presets browsing are unavailable on iOS; factory preset loading and full state persistence work on both platforms

Requirements

  • Platforms: macOS 13+, iOS 16+
  • Swift: 6.2+

About

Spongefork (SPFK) is the personal software projects of Ryan Francesconi. Dedicated to creative sound manipulation, his first application, Spongefork, was released in 1999 for macOS 8. From 2016 to 2025 he was the lead macOS developer at Audio Design Desk.

Package Metadata

Repository: ryanfrancesconi/spfk-au-host

Default branch: main

README: README.md