# How Push-to-Talk Voice Messaging Works in BitChat: A Deep Dive into Live Voice Streaming

> Discover how BitChat's push-to-talk voice messaging streams live audio in real-time, transmitting AAC-encoded frames and converting them into voice notes.

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

---

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

BitChat's push-to-talk functionality is distinct from traditional voice messaging. Instead of recording a complete file before transmission, it streams compressed audio packets to a peer the moment capture begins. This article examines the complete architecture—from microphone capture through packet transmission to live playback—based on the open-source implementation in `permissionlesstech/bitchat`.

## PTT Voice Messaging Architecture Overview

The system operates through three coordinated stages that run in parallel during a live voice session:

| Stage | Component | Core Responsibility |
|-------|-----------|---------------------|
| **Capture** | `PTTCaptureEngine` | Records microphone input, encodes to AAC frames, and triggers downstream processing |
| **Packetization & Transmission** | `VoiceBurstPacketizer` | Groups frames into wire-format packets with START/DATA/END semantics |
| **Reception & Assembly** | `ChatLiveVoiceCoordinator` | Decodes inbound packets, reassembles bursts, and manages the live playback stream |

Each stage is implemented in dedicated Swift files under `bitchat/Features/voice/` and `bitchat/ViewModels/`.

## Live Capture: PTTCaptureEngine and PTTLiveVoiceSession

When a user presses and holds the microphone button, the UI instantiates a `PTTLiveVoiceSession` that conforms to the `VoiceCaptureSession` protocol defined in [`VoiceCaptureSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceCaptureSession.swift).

### Session Initialization

```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 performs three critical operations:

1. Generates a unique **burst ID** via `VoiceBurstPacket.makeBurstID()` that identifies this voice stream
2. Creates a temporary output URL for the eventual `.m4a` voice-note file
3. Initializes `PTTCaptureEngine.start()` to begin microphone capture

### Frame-Level Processing

The engine's audio tap invokes `capture.onFrames` for each batch of AAC frames. The session maintains a `StreamState` machine that guarantees exactly one **START** packet is transmitted, followed by data packets as frames become available:

```swift
// Capture engine delivers AAC frames (called on capture queue)
engine.onFrames = { frames in
    // Each frame is packetised and sent via the closure above
    // (handled automatically inside PTTLiveVoiceSession)
}

```

Simultaneously, the same AAC data is written to disk. This dual-path design ensures that if the live stream fails, the completed voice-note can still be delivered through the standard message pipeline.

## Packet Format: VoiceBurstPacket Structure

The wire protocol is defined in [`VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceBurstPacket.swift). Packets are compact binary structures designed to fit within a single BLE frame after Noise encryption overhead.

### Packet Types and Flags

| Flag | Value | Purpose |
|------|-------|---------|
| **START** | `0x01` | Advertises codec (`aacLC16kMono`); sent once at stream beginning |
| **DATA** | `0x00` | Carries 1-8 AAC frames with length-prefixed payload |
| **END** | `0x02` | Signals completion with metadata: total packets and duration |
| **CANCELED** | `0x04` | Empty payload; instructs receiver to discard the burst |

The **START** packet payload contains only the codec identifier. **DATA** packets use the format `[len][AAC-frame]…` to support variable frame sizes. **END** packets include `[totalDataPackets][durationMs]` to assist the receiver in validating completeness.

### Packetization Limits

The `VoiceBurstPacketizer.add()` method respects `TransportConfig.pttMaxBurstContentBytes`, ensuring that encoded packets never exceed the underlying transport capacity. This is critical for BLE mesh networks where MTU constraints are strict.

## Transmission: BLE/Noise Integration

The `sendPacket` closure provided to `PTTLiveVoiceSession` bridges the voice layer to the transport layer. In practice, this routes through `BLEService` to queue packets for the peer:

```swift
// Packet is handed to this closure inside PTTLiveVoiceSession
sendPacket: { data in meshService.sendVoiceBurst(data) }

```

Packets are encrypted via the Noise protocol (for DMs) or signed for public mesh broadcast. The small packet size enables low-latency streaming—frames arrive at the recipient typically within 50-100ms of capture, depending on network conditions.

## Reception and Assembly: ChatLiveVoiceCoordinator

Inbound voice traffic is handled by [`ChatLiveVoiceCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatLiveVoiceCoordinator.swift), registered for both private direct messages (`NoisePayloadType.voiceFrame`) and public mesh messages (`MessageType.voiceFrame`).

### Frame Handling Pipeline

```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
    )
}

```

The coordinator performs several functions:

- **Packet decoding**: Validates flags and extracts AAC frames via `VoiceBurstPacket.decode`
- **Burst tracking**: Maintains state per burst ID, buffering out-of-order packets with a 0.5-second gap timeout (`gapSkipSeconds`)
- **Progressive file writing**: Appends decoded AAC data to a temporary `.aac` file
- **Live playback**: Streams frames to `PTTBurstPlayer` for real-time audio output

### Deduplication with Finalized Voice Notes

When the **END** packet arrives, the coordinator creates a `BitchatMessage` representing the live burst. However, the sender simultaneously finalizes the `.m4a` file captured during the session. This file—named `voice_<burstID>.m4a`—is sent through the standard voice-note pipeline.

The coordinator detects matching burst IDs and silently absorbs the finalized file, preventing duplicate bubbles in the UI. This merge logic ensures users see a single coherent voice message regardless of how it arrived.

### Playback During Reception

[`PTTBurstPlayer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTBurstPlayer.swift) manages audio output for live streams. Unlike standard voice-note playback, it operates on streaming buffers and handles partial data gracefully, starting playback as soon as sufficient frames are buffered.

## Session Termination and Fallback

When the user releases the record button, the session concludes:

```swift
// Finish the session when the user releases the button
if let noteURL = await liveSession.finish() {
    // `noteURL` is the finalized .m4a file – send it like a regular voice note
    viewModel.sendVoiceNote(fileURL: noteURL)
}

```

The `finish()` method:

1. Signals the capture engine to stop recording
2. Transmits the **END** packet to the peer
3. Returns the URL of the finalized `.m4a` file
4. Allows the caller to send the voice-note through normal channels as reliability fallback

If the session is interrupted—app backgrounding, Bluetooth disconnection, or explicit cancellation—a **CANCELED** packet notifies the receiver to discard the incomplete burst.

## User Experience Integration

The UI layer coordinates with voice state through `setActivePublicVoiceTalker`, which displays the current speaker during live transmission. This indicator activates immediately on **START** packet receipt, before any audio plays, providing sub-second feedback that a peer is speaking.

PTT functionality is gated by user preference in [`PTTSettings.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTSettings.swift), which exposes a toggle to enable or disable live voice streaming. When disabled, the hold-to-record gesture falls back to traditional voice-note capture with deferred transmission.

## Key Source Files

| File | Path | Purpose |
|------|------|---------|
| [`VoiceCaptureSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceCaptureSession.swift) | `bitchat/Features/voice/` | Protocol abstraction and session implementations |
| [`PTTCaptureEngine.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTCaptureEngine.swift) | `bitchat/Features/voice/` | Core audio capture and AAC encoding |
| [`VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/VoiceBurstPacket.swift) | `bitchat/Protocols/` | Wire-format packet definitions and serialization |
| [`ChatLiveVoiceCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatLiveVoiceCoordinator.swift) | `bitchat/ViewModels/` | Inbound packet handling and UI integration |
| [`PTTBurstPlayer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTBurstPlayer.swift) | `bitchat/Features/voice/` | Real-time playback of streaming voice bursts |
| [`PTTSettings.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PTTSettings.swift) | `bitchat/Features/voice/` | User-configurable PTT enable/disable |

## Summary

- **BitChat's push-to-talk voice messaging** streams AAC-encoded audio frames live to peers, eliminating the delay of traditional record-then-send workflows
- `PTTLiveVoiceSession` orchestrates dual-path capture: real-time packet transmission plus local file storage for fallback delivery
- `VoiceBurstPacket` defines a compact binary protocol with explicit START/DATA/END/CANCELED states for reliable stream lifecycle management
- `ChatLiveVoiceCoordinator` handles packet assembly, out-of-order buffering, live playback via `PTTBurstPlayer`, and seamless deduplication when the finalized voice-note arrives
- The architecture gracefully degrades: if live streaming fails, the complete `.m4a` file delivers through standard message channels

## Frequently Asked Questions

### What codec does BitChat use for push-to-talk voice?

BitChat encodes push-to-talk audio using **AAC-LC at 16kHz mono** (`aacLC16kMono`). This codec is advertised in the START packet and provides efficient compression suitable for low-bandwidth BLE mesh networks while maintaining intelligible speech quality.

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

The coordinator uses the **burst ID** to correlate live streams with finalized voice-notes. When `voice_<burstID>.m4a` arrives through the standard pipeline, `ChatLiveVoiceCoordinator` recognizes the matching ID from the prior live session and suppresses the duplicate bubble, preserving only the live message that was already displayed.

### Can push-to-talk work without an internet connection?

Yes. BitChat's PTT operates over **BLE mesh and direct peer-to-peer Noise connections**, requiring no internet infrastructure. Packets route through the local mesh network constrained only by `TransportConfig.pttMaxBurstContentBytes` to ensure they fit within encrypted BLE frames.

### What happens if a push-to-talk session is interrupted?

If capture fails or the user cancels, the session emits a **CANCELED** packet (`flags = 0x04`) with empty payload. Receivers discard the incomplete burst. The sender may alternatively send the partially-captured `.m4a` file through normal voice-note channels if sufficient audio was recorded.