# How BitChat Implements Push-to-Talk Voice Communication: A Technical Deep Dive

> Discover how BitChat implements push-to-talk for live voice streaming. Learn about AAC audio frame transmission and seamless transition to voice notes.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-07

---

**BitChat implements push-to-talk (PTT) as a live voice streaming system that transmits AAC audio frames in real-time to a single peer while the user holds the record button, then seamlessly transitions the finished burst into a regular voice-note file.**

Push-to-talk voice communication in BitChat combines the immediacy of live audio with the reliability of persistent messaging. According to the **permissionlesstech/bitchat** source code, the implementation spans capture engines, custom packet protocols, and BLE transmission layers. This article examines the complete architecture from microphone input through final message delivery.

## Core Architecture: Three-Stage Pipeline

The PTT system operates through three coordinated components: capture, packetization/transmission, and reception/assembly.

### Stage 1: Audio Capture with Dual Output

The `PTTCaptureEngine` handles microphone input and produces two simultaneous outputs: live AAC frames for streaming and a persistent `.m4a` file for the final voice-note. This dual-output design ensures that even if the live stream fails, the complete recording survives.

In [`PTTCaptureEngine.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTCaptureEngine.swift), the engine runs on a dedicated capture queue and invokes its `onFrames` callback for each batch of encoded audio. The `PTTLiveVoiceSession` (conforming to `VoiceCaptureSession`) manages this flow:

```swift
// Start a live PTT session when the user presses the mic button
let liveSession = PTTLiveVoiceSession(
    sendPacket: { data in meshService.sendVoiceBurst(data) }
)
await liveSession.start()        // begins capture & streaming

```

The `start()` method creates a temporary output URL using `VoiceBurstPacket.makeBurstID()` to generate a unique burst identifier. This ID links the live stream to its eventual voice-note counterpart.

### Stage 2: Packetization and BLE Transmission

Raw AAC frames cannot traverse BLE efficiently. The `VoiceBurstPacketizer` groups frames into `VoiceBurstPacket` structures with a defined wire format specified in [`VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceBurstPacket.swift):

| Packet Type | Flag Value | Payload Contents |
|-------------|-----------|------------------|
| **START** | `0x01` | `[codec]` — advertises `aacLC16kMono` |
| **DATA** | `0x00` | `[len][AAC-frame]...` (up to 8 frames) |
| **END** | `0x02` | `[totalDataPackets][durationMs]` |
| **CANCELED** | `0x04` | empty — signals receivers to discard |

The `PTTLiveVoiceSession` uses `StreamState` to guarantee exactly one START packet, then feeds each frame batch to `VoiceBurstPacketizer.add` and flushes completed packets. The `sendPacket` closure—provided by the chat coordinator—injects packets into `BLEService` for Noise-encrypted transmission.

Packet size respects `TransportConfig.pttMaxBurstContentBytes` to ensure single-BLE-frame delivery after encryption overhead.

### Stage 3: Reception, Assembly, and UI Integration

The `ChatLiveVoiceCoordinator` handles inbound voice bursts. Registered for both private DMs (`NoisePayloadType.voiceFrame`) and public mesh traffic (`MessageType.voiceFrame`), it performs four critical functions:

1. **Decodes packets** via `VoiceBurstPacket.decode`
2. **Buffers out-of-order packets** with a 0.5-second gap timeout (`gapSkipSeconds`)
3. **Progressively writes AAC data** to a temporary `.aac` file
4. **Plays live audio** through `PTTBurstPlayer`

When the END packet arrives, the coordinator creates a `BitchatMessage` representing the live burst. If the sender later delivers the finalized `.m4a` voice-note (`voice_<burstID>.m4a`), the coordinator silently absorbs it—preventing duplicate bubbles in the chat UI.

```swift
// Receiving a voice frame in the coordinator
func handlePublicVoiceFramePayload(
    from peerID: PeerID,
    nickname: String,
    payload: Data,
    timestamp: Date
) {
    // Decode packet, assemble burst, write to temporary file, play live
    chatLiveVoiceCoordinator.handlePublicVoiceFramePayload(
        from: peerID,
        nickname: nickname,
        payload: payload,
        timestamp: timestamp
    )
}

```

## Session Lifecycle: From Press to Release

The complete user interaction follows a strict state machine:

**Press to start:**

```swift
let liveSession = PTTLiveVoiceSession(
    sendPacket: { data in meshService.sendVoiceBurst(data) }
)
await liveSession.start()
// START packet emitted, UI shows setActivePublicVoiceTalker indicator

```

**Hold to stream:**

- `PTTCaptureEngine` delivers AAC frames via `onFrames`
- Each batch packetized and transmitted
- `PTTBurstPlayer` plays inbound streams from peer

**Release to finish:**

```swift
if let noteURL = await liveSession.finish() {
    // noteURL is the finalized .m4a file
    viewModel.sendVoiceNote(fileURL: noteURL)
}
// END packet emitted, voice-note enters normal message pipeline

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`VoiceCaptureSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceCaptureSession.swift) | Protocol definition and concrete session implementations including `PTTLiveVoiceSession` |
| [`PTTCaptureEngine.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTCaptureEngine.swift) | Low-level audio engine for recording, AAC encoding, and frame emission |
| [`VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceBurstPacket.swift) | Wire-format packet structures and `VoiceBurstPacketizer` logic |
| [`ChatLiveVoiceCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatLiveVoiceCoordinator.swift) | Inbound packet handling, burst assembly, and UI integration |
| [`PTTBurstPlayer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTBurstPlayer.swift) | Real-time playback of incoming live voice frames |
| [`PTTSettings.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTSettings.swift) | User preference toggle for enabling/disabling live PTT |

## Performance and Reliability Considerations

The BitChat PTT system prioritizes **low latency over guaranteed delivery**. The 0.5-second gap tolerance in `ChatLiveVoiceCoordinator` prevents stalls from minor packet loss, while the dual-output capture ensures message persistence regardless of streaming success.

Codec selection (`aacLC16kMono` at 16 kHz) balances quality against BLE bandwidth constraints. The 8-frame maximum per DATA packet and `pttMaxBurstContentBytes` limit keep transmissions within single BLE frames, avoiding fragmentation latency.

## Summary

- **Push-to-talk in BitChat** streams AAC audio live while recording, then promotes the recording to a standard voice-note
- **`PTTLiveVoiceSession`** orchestrates capture through `PTTCaptureEngine` and packetization via `VoiceBurstPacketizer`
- **`VoiceBurstPacket`** defines START/DATA/END/CANCELED states with compact binary encoding for BLE efficiency
- **`ChatLiveVoiceCoordinator`** handles reception, assembly, live playback, and deduplication with final voice-notes
- The **burst ID** ties live streams to their persistent counterparts, ensuring seamless UX across network conditions

## Frequently Asked Questions

### What happens if the BLE connection drops during a push-to-talk transmission?

The live stream terminates without an END packet, and receivers discard the partial burst after the 0.5-second gap timeout. However, the sender's local `.m4a` recording completes normally and can be sent as a regular voice-note once connectivity returns.

### How does BitChat prevent duplicate voice messages in the chat UI?

The `ChatLiveVoiceCoordinator` tracks burst IDs. When it receives both a completed live burst (via PTT packets) and its matching voice-note file (via the standard message pipeline), it absorbs the file silently and presents only the unified message bubble.

### Can users disable push-to-talk while keeping standard voice notes?

Yes. [`PTTSettings.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTSettings.swift) provides a user-facing toggle that disables the live streaming pathway without affecting `VoiceNoteCaptureSession` or the standard voice-note recording flow.

### Why does BitChat use AAC instead of Opus for voice compression?

The `aacLC16kMono` codec selection in [`VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceBurstPacket.swift) reflects hardware encoder availability on iOS devices and predictable frame sizes for BLE packetization. The 16 kHz mono configuration provides sufficient voice quality within the byte budget of `pttMaxBurstContentBytes`.