You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(swift): VoiceProcessedAudioIO — capture+playback on one VP engine
Playback renders through the same voice-processed engine as capture, so
the assistant audio is guaranteed to be in the AEC reference path
(review feedback: the split-pair reference is device-level and
route-dependent). Recommended wiring: one instance as both input and
output. Internal lock serializes enqueue/flush/stop against runtime
actor reentrancy.
Refs #559
Co-authored-by: Claude <claude@anthropic.com>
Copy file name to clipboardExpand all lines: docs/src/content/docs/swift/audio/built-in/audio-playback.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,10 @@ description: AVAudioEngine-backed AudioOutput that converts PCM16 frames to floa
5
5
6
6
`AudioPlayback` is part of the `AgentSquadAudio` product. It accepts PCM16 @ 24 kHz frames, converts them to float32, and schedules them on an `AVAudioPlayerNode` for continuous playback. `flush()` provides an instant barge-in cut by discarding all buffered audio.
7
7
8
+
:::tip
9
+
For voice sessions, prefer [`VoiceProcessedAudioIO`](/agent-squad/swift/audio/built-in/voice-processed-audio-io/) — playback renders through the same voice-processed engine as capture, guaranteeing echo cancellation of the assistant's audio.
Copy file name to clipboardExpand all lines: docs/src/content/docs/swift/audio/built-in/mic-capture.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,10 @@ description: AVAudioEngine-backed AudioInput that taps the microphone, converts
7
7
8
8
By default it captures through Apple's **Voice-Processing I/O** unit: echo cancellation that uses the speaker signal as a hardware reference to subtract the assistant's own voice from the mic, plus noise suppression and automatic gain control.
9
9
10
+
:::tip
11
+
For voice sessions, prefer [`VoiceProcessedAudioIO`](/agent-squad/swift/audio/built-in/voice-processed-audio-io/) — capture and playback on **one** engine, which guarantees the assistant's audio is in the AEC reference path. With the split `MicCapture`/`AudioPlayback` pair the reference is device-level and route-dependent.
description: Capture and playback on one voice-processed AVAudioEngine — echo cancellation with the assistant's audio guaranteed in the reference path. The recommended wiring for voice sessions.
4
+
---
5
+
6
+
`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.
7
+
8
+
Conforms to **both**`AudioInput` and `AudioOutput`: pass **one instance** as both `input:` and `output:`.
9
+
10
+
```swift
11
+
importAgentSquadAudio
12
+
13
+
let io =VoiceProcessedAudioIO()
14
+
let runtime =RealtimeRuntime(session: assistant, input: io, output: io)
15
+
tryawait runtime.start()
16
+
```
17
+
18
+
Prefer this over the separate [`MicCapture`](/agent-squad/swift/audio/built-in/mic-capture/) + [`AudioPlayback`](/agent-squad/swift/audio/built-in/audio-playback/) pair for voice sessions — with two engines the echo reference is taken at the device level, which is route-dependent.
|`sampleRate`|`24_000`| Both capture and playback rate. Must match what the realtime session expects (OpenAI Realtime: PCM is always 24 kHz). |
37
+
|`maxBufferedFrames`|`16`| Capacity of the capture `AsyncStream`; oldest frames dropped under back-pressure. |
38
+
|`voiceProcessing`|`.default`| AEC + noise suppression + AGC tuning. **Non-optional** — raw capture defeats this class's purpose; use `MicCapture(voiceProcessing: nil)` for that. |
39
+
|`sessionPolicy`|`.managed`| Who configures the `AVAudioSession` — see [AudioSessionPolicy](/agent-squad/swift/audio/built-in/mic-capture/#audiosessionpolicy-ios-only). |
40
+
|`configureEngine`|`nil`| Escape hatch: runs with the raw `AVAudioEngine` after voice processing is enabled and the player is wired, before the tap is installed. |
`start()` and `stop()` are **idempotent** — `RealtimeRuntime` 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.
56
+
57
+
Failure modes are the shared [`MicCaptureError`](/agent-squad/swift/audio/built-in/mic-capture/#miccaptureerror) cases: `permissionDenied`, `converterUnavailable`, `voiceProcessingUnavailable`.
58
+
59
+
:::caution
60
+
The **simulator performs no echo cancellation** — validate AEC on a real device. Voice-processed output sounds "call-like" and slightly quieter; counter with `duckingLevel: .min`.
61
+
:::
62
+
63
+
---
64
+
65
+
## Related pages
66
+
67
+
-[Audio overview](/agent-squad/swift/audio/overview/) — the `AudioInput`/`AudioOutput` protocols
Copy file name to clipboardExpand all lines: docs/src/content/docs/swift/audio/overview.md
+13-9Lines changed: 13 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,11 +3,11 @@ title: Audio overview
3
3
description: The AgentSquadAudio product and the AudioInput/AudioOutput protocols that connect microphone capture and speaker playback to the voice runtime.
4
4
---
5
5
6
-
`AgentSquadAudio` is a separate Swift package product that ships two AVFoundation-backed implementations — `MicCapture` and `AudioPlayback` — built on top of two protocols declared in the core `AgentSquad` module.
6
+
`AgentSquadAudio` is a separate Swift package product that ships three AVFoundation-backed implementations — `VoiceProcessedAudioIO` (the recommended one), `MicCapture`, and `AudioPlayback` — built on top of two protocols declared in the core `AgentSquad` module.
@@ -48,14 +48,17 @@ public protocol AudioOutput: Sendable {
48
48
49
49
## How they feed the voice runtime
50
50
51
-
[`RealtimeRuntime`](/agent-squad/swift/voice/overview/) accepts an `AudioInput` and an `AudioOutput` at construction time:
51
+
[`RealtimeRuntime`](/agent-squad/swift/voice/overview/) accepts an `AudioInput` and an `AudioOutput` at construction time. The recommended wiring is one `VoiceProcessedAudioIO` instance in both roles — capture and playback share a single voice-processed engine, so the assistant's audio is guaranteed to be in the echo canceller's reference path:
52
52
53
53
```swift
54
-
let runtime =RealtimeRuntime(
55
-
input: MicCapture(),
56
-
output: AudioPlayback(),
57
-
// ... other config
58
-
)
54
+
let io =VoiceProcessedAudioIO()
55
+
let runtime =RealtimeRuntime(session: assistant, input: io, output: io)
56
+
```
57
+
58
+
The split pair works too (the AEC reference is then device-level, which is route-dependent):
59
+
60
+
```swift
61
+
let runtime =RealtimeRuntime(session: assistant, input: MicCapture(), output: AudioPlayback())
59
62
```
60
63
61
64
The runtime drives `start`, `stop`, `enqueue`, and `flush` from its single event pump, so implementations are never called concurrently by the runtime itself.
@@ -70,7 +73,8 @@ The wire format for both protocols is **PCM16 little-endian mono at 24 kHz**. Th
70
73
71
74
| Type | Protocol | Description |
72
75
|---|---|---|
73
-
|[`MicCapture`](/agent-squad/swift/audio/built-in/mic-capture/)|`AudioInput`| AVAudioEngine tap → PCM16 @ 24 kHz, with iOS permission gating |
76
+
|[`VoiceProcessedAudioIO`](/agent-squad/swift/audio/built-in/voice-processed-audio-io/)|`AudioInput` + `AudioOutput`| Capture and playback on **one** voice-processed engine — guaranteed AEC reference path; pass one instance as both input and output |
77
+
|[`MicCapture`](/agent-squad/swift/audio/built-in/mic-capture/)|`AudioInput`| AVAudioEngine tap → PCM16 @ 24 kHz, voice-processed by default, with iOS permission gating |
74
78
|[`AudioPlayback`](/agent-squad/swift/audio/built-in/audio-playback/)|`AudioOutput`| AVAudioEngine + AVAudioPlayerNode, with barge-in flush |
|**`AgentSquadMCP`**|`import AgentSquadMCP`| the official [MCP Swift SDK](https://github.com/modelcontextprotocol/swift-sdk)|`MCPServer` (alias of `MCPToolProvider`) — connect any MCP server with `MCPServer(url:)`, with MCP Apps UI support |
217
-
|**`AgentSquadAudio`**|`import AgentSquadAudio`| AVFoundation (Apple platforms only) |`MicCapture` (echo-cancelled by default) +`AudioPlayback` for the realtime voice runtime — requires `NSMicrophoneUsageDescription` in your Info.plist |
217
+
|**`AgentSquadAudio`**|`import AgentSquadAudio`| AVFoundation (Apple platforms only) |`VoiceProcessedAudioIO` (echo-cancelled capture + playback on one engine — the recommended wiring), `MicCapture`,`AudioPlayback` for the realtime voice runtime — requires `NSMicrophoneUsageDescription` in your Info.plist |
218
218
219
219
So an app that doesn't use MCP never downloads the MCP SDK. Future optional integrations
220
220
(e.g. `AgentSquadLangfuse` for trace export) follow the same pattern — the core never grows a
@@ -229,7 +229,7 @@ dependency. Add a product to your target's `dependencies` to use it:
229
229
230
230
## Voice audio — echo cancellation and full control
231
231
232
-
By default `MicCapture` captures through Apple's **Voice-Processing I/O** unit — the native
232
+
The audio layer captures through Apple's **Voice-Processing I/O** unit by default — the native
233
233
equivalent of what ChatGPT's voice mode gets via WebRTC. The signal sent to the speaker is used
234
234
as a hardware reference to subtract the assistant's own voice from the mic, plus noise
235
235
suppression and automatic gain control. Without it, the assistant hears itself through the
@@ -238,53 +238,58 @@ speaker and interrupts its own answers.
238
238
The audio layer is configurable in four independent levels — each level keeps everything the
239
239
previous levels give you:
240
240
241
-
**Level 0 — defaults.** Echo-cancelled capture, nothing to configure:
241
+
**Level 0 — defaults.**`VoiceProcessedAudioIO` runs capture **and** playback on one
242
+
`AVAudioEngine`, so the assistant's audio renders through the voice-processed output and is by
243
+
construction in the echo canceller's reference path. Pass one instance as both `input:` and
244
+
`output:`:
242
245
243
246
```swift
244
247
importAgentSquadAudio
245
248
246
-
let runtime =RealtimeRuntime(session: assistant, input: MicCapture(), output: AudioPlayback())
249
+
let io =VoiceProcessedAudioIO()
250
+
let runtime =RealtimeRuntime(session: assistant, input: io, output: io)
247
251
tryawait runtime.start()
248
252
```
249
253
254
+
The separate `MicCapture` + `AudioPlayback` pair still works (both echo-cancelled on the capture
255
+
side by default) — but with two engines the echo reference is taken at the device level, which is
256
+
route-dependent. Prefer `VoiceProcessedAudioIO` for voice sessions.
257
+
250
258
**Level 1 — tune voice processing** (or turn it off):
251
259
252
260
```swift
253
261
// Keep AEC but disable gain control and minimize how much the system ducks playback volume:
0 commit comments