ryanfrancesconi/spfk-au-host
Audio Unit (v3) hosting, validation, caching, and effects chain management for macOS and iOS.
Features
- Effects Chain — Actor-based
AudioUnitChainfor 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 externalauval/auvaltoolfallback (macOS only) - Preset Management — Factory preset loading via AudioToolbox APIs and user preset discovery from the
~/Library/Audio/Presetshierarchy (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 Grouping —
AudioUnitManufacturerCollectionfor organizing available components into manufacturer-grouped hierarchies for menu display - Sendable Component Wrapper —
S_AVAudioUnitComponentcopies all relevant properties fromAVAudioUnitComponentinto aSendablestruct for safe cross-isolation use - Engine Abstraction —
AudioEngineConnectionprotocol decouples chain connection logic fromAVAudioEngine, 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 AVAudioEngineUsage
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 = 140Manufacturer 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
auvalvalidation — Falls back toAudioComponentValidateAPI results on iOS - User preset discovery —
AudioUnitPresets.Locationsand~/Library/Audio/Presetsbrowsing 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