Skip to content

VoiceProcessedAudioIO

VoiceProcessedAudioIO is part of the AgentSquadAudio product. It runs microphone capture and assistant playback on a single AVAudioEngine with Apple’s Voice-Processing I/O unit enabled. Because the assistant’s audio renders through the same engine’s voice-processed output, it is by construction in the echo canceller’s reference path — the configuration the VP unit is designed around.

Conforms to both AudioInput and AudioOutput: pass one instance as both input: and output:.

import AgentSquadAudio
let io = VoiceProcessedAudioIO()
let runtime = RealtimeRuntime(session: assistant, input: io, output: io)
try await runtime.start()

Prefer this over the separate MicCapture + AudioPlayback pair for voice sessions — with two engines the echo reference is taken at the device level, which is route-dependent.


public init(
sampleRate: Double = 24_000,
maxBufferedFrames: Int = 16,
voiceProcessing: VoiceProcessing = .default,
sessionPolicy: AudioSessionPolicy = .managed,
configureEngine: (@Sendable (AVAudioEngine) throws -> Void)? = nil
)
ParameterDefaultNotes
sampleRate24_000Both capture and playback rate. Must match what the realtime session expects (OpenAI Realtime: PCM is always 24 kHz).
maxBufferedFrames16Capacity of the capture AsyncStream; oldest frames dropped under back-pressure.
voiceProcessing.defaultAEC + noise suppression + AGC tuning. Non-optional — raw capture defeats this class’s purpose; use MicCapture(voiceProcessing: nil) for that.
sessionPolicy.managedWho configures the AVAudioSession — see AudioSessionPolicy.
configureEnginenilEscape hatch: runs with the raw AVAudioEngine after voice processing is enabled and the player is wired, before the tap is installed.

public let frames: AsyncStream<Data> // AudioInput — captured PCM16 LE mono frames
public func start() async throws // both roles; idempotent
public func enqueue(_ pcm16: Data) async // AudioOutput — schedule one frame
public func flush() async // AudioOutput — instant barge-in cut
public func stop() async // both roles; idempotent
public func playedMilliseconds() async -> Double? // ms actually played of the current burst (survives flush)

playedMilliseconds() feeds the session’s conversation.item.truncate on barge-in, keeping the server’s context aligned with what the user actually heard.

start() and stop() are idempotentRealtimeRuntime calls each twice on the same instance (once through the AudioOutput role, once through AudioInput), and the second call is a no-op. enqueue/flush before start() or after stop() are safe no-ops. One instance serves one session: stop() finishes the frames stream for good — create a new instance to start again.

Failure modes are the shared MicCaptureError cases: permissionDenied, converterUnavailable, voiceProcessingUnavailable.


  • Audio overview — the AudioInput/AudioOutput protocols
  • MicCapture — capture-only built-in (split-pair wiring, VoiceProcessing, AudioSessionPolicy docs)
  • AudioPlayback — playback-only built-in
  • Voice overview — the RealtimeRuntime that consumes this class