Sample CodeiOS, iPadOS, Mac Catalyst, macOSReviewed 2026-07-21View on Apple Developer

Creating a custom speech synthesizer

At a glance

Item Summary
Purpose Publishes custom speech voices from an extension and renders requested speech from bundled audio.
App architecture A SwiftUI host edits shared voice configuration; an Audio Unit extension exposes and renders those voices for the system.
Main patterns Host/extension split, factory, app-group handoff, framework subclass hooks, real-time render callback.
Project style Two targets with separate host and extension folders; the app-group suite is their only data bridge.

Project structure

CustomSpeechSynthesizerExample/
├── CustomSpeechSynthesizerExample/
│   ├── CustomSpeechSynthesizerExampleApp.swift
│   └── ContentView.swift
└── CustomSpeechSynthesizerExampleExtension/
    ├── AudioUnitFactory.swift
    ├── CustomSpeechSynthesizerExampleAudioUnit.swift
    └── Info.plist

Structure observations

  • Target folders make the process boundary explicit: the host never owns the provider audio unit.
  • Both targets independently open the same app-group UserDefaults suite rather than sharing in-memory objects.

Overall architecture

Reference code

CustomSpeechSynthesizerExample/ContentView.swift:55 — the host persists configuration and explicitly notifies the speech system.

private func saveVoicesToGroupDefaults() {
    groupDefaults?.set(voices, forKey: "voices")
    AVSpeechSynthesisProviderVoice.updateSpeechVoices()
}

Interpretation

This is a multi-target architecture. The host is a configuration UI; the extension is the runtime provider. Durable app-group storage is the cross-process contract, and system callbacks drive extension creation and rendering.

Ownership and state

Ownership evidence

CustomSpeechSynthesizerExampleExtension/AudioUnitFactory.swift:10 — the extension factory constructs and retains the provider instance.

public class AudioUnitFactory: NSObject, AUAudioUnitFactory {
    var audioUnit: AUAudioUnit?

    public func createAudioUnit(with componentDescription: AudioComponentDescription) throws -> AUAudioUnit {
        audioUnit = try CustomSpeechSynthesizerExampleAudioUnit(
            componentDescription: componentDescription, options: [])
        return audioUnit!
    }
}
Owner Object or state Relationship Mutation authority
ContentView Voice names and input text Owns SwiftUI state Host view
App-group defaults Serialized voice list Shared external store Host writes; extension reads
AudioUnitFactory Current provider audio unit Creates and retains System factory callback
Provider audio unit Request, bus, buffer, format, frame position Creates/retains render state Provider request/render methods

Class and protocol design

Type Responsibility Depends on
ContentView Edit and persist available voice names SwiftUI, app-group defaults
AudioUnitFactory Fulfill the system’s AUAudioUnitFactory creation contract CoreAudioKit
CustomSpeechSynthesizerExampleAudioUnit Advertise voices and render requested speech AVSpeechSynthesisProviderAudioUnit
AVSpeechSynthesisProviderVoice System-facing voice description Names from shared defaults

Access control

Symbol Access Verified effect Likely rationale
Factory class and factory methods public Visible to extension infrastructure outside the Swift module. Required system entry boundary.
Provider class and overridden hooks public System can query voices/busses and invoke request/render hooks. Framework subclass contract.
Request, bus, buffer, frame position, format private No other extension type can corrupt render state. Preserve request/render invariants.
saveVoicesToGroupDefaults private Only the host view’s add/delete actions persist and notify. Localize side effects.
Host view state implicit internal Accessible in the host target only. No public app API.

Reference code

CustomSpeechSynthesizerExampleExtension/CustomSpeechSynthesizerExampleAudioUnit.swift:10 — public system hooks wrap private mutable render state.

public class CustomSpeechSynthesizerExampleAudioUnit: AVSpeechSynthesisProviderAudioUnit {
    private var request: AVSpeechSynthesisProviderRequest?
    private var currentBuffer: AVAudioPCMBuffer?
    private var framePosition: AVAudioFramePosition = 0
    private var format: AVAudioFormat
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Voice editing and persistence Host ContentView Configuration UI concern.
Provider construction AudioUnitFactory Extension entry contract.
Voice advertisement Provider speechVoices override System queries the audio unit.
SSML-to-sample selection getAudioBufferForSSML Sample synthesis policy belongs to provider.
Frame copying/completion internalRenderBlock Audio-unit render boundary.

Design patterns

Pattern Source evidence Purpose or tradeoff
Host app + extension CustomSpeechSynthesizerExample/ContentView.swift:15 and CustomSpeechSynthesizerExampleExtension/CustomSpeechSynthesizerExampleAudioUnit.swift:14 Separates configuration UI from system-hosted rendering.
Factory CustomSpeechSynthesizerExampleExtension/AudioUnitFactory.swift:19 Lets Core Audio instantiate the correct provider type.
Shared-store handoff CustomSpeechSynthesizerExample/ContentView.swift:58 Communicates across targets without direct object ownership.
Template method/subclass hooks CustomSpeechSynthesizerExampleExtension/CustomSpeechSynthesizerExampleAudioUnit.swift:53 Implements framework-defined customization points.
Render callback CustomSpeechSynthesizerExampleExtension/CustomSpeechSynthesizerExampleAudioUnit.swift:74 Fills system-supplied buffers on demand.

Naming conventions

  • Target-qualified type names (CustomSpeechSynthesizerExampleAudioUnit) make the provider role explicit.
  • Factory and request methods mirror framework terminology: createAudioUnit, synthesizeSpeechRequest, cancelSpeechRequest.
  • Private storage uses implementation-oriented names such as _outputBusses and currentBuffer.
  • Host and extension directories match Xcode target boundaries.

Architecture takeaways

  • Treat the app group as a data contract, not shared ownership between processes.
  • Keep system-facing extension entry points public and render state private.
  • Use a factory as the only construction route for framework-hosted components.
  • Put host configuration and provider execution in separate targets and document both halves.

Source map

Source file Relevant symbols
CustomSpeechSynthesizerExample/ContentView.swift Voice state, shared-default persistence, system notification
CustomSpeechSynthesizerExample/CustomSpeechSynthesizerExampleApp.swift Host app entry
CustomSpeechSynthesizerExampleExtension/AudioUnitFactory.swift Extension factory
CustomSpeechSynthesizerExampleExtension/CustomSpeechSynthesizerExampleAudioUnit.swift Voice provider and render block