# How Push-to-Talk Voice Communication Operates Over BitChat's Mesh Network

> Discover how BitChat's mesh network enables secure, low-latency push-to-talk voice communication using encrypted VoiceBurstPackets over BLE mesh.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-07-27

---

**BitChat implements a "smart" push-to-talk system that converts tap-and-hold mic gestures into live AAC-LC audio streams, transmitting encrypted VoiceBurstPackets over BLE mesh with approximately 470ms end-to-end latency while maintaining compatibility with traditional offline voice notes.**

BitChat's open-source mesh messaging platform transforms standard voice note capture into walkie-talkie-style live communication by streaming compressed audio frames through its existing decentralized network. This architecture enables real-time voice transmission between peers without centralized servers, leveraging the same cryptographic sessions and relay infrastructure used for text messages. Understanding how push-to-talk voice communication operates over BitChat's mesh requires examining the three-stage pipeline: capture and encoding on the sender, encrypted transport across the mesh, and frame assembly with jitter-buffered playback on the receiver.

## Audio Capture and Frame Encoding

The sender-side pipeline begins when a user holds the mic button in `ContentComposerView.micButtonView`, triggering a live capture path that runs parallel to the standard voice note recorder. An `AVAudioEngine` input tap feeds 16kHz mono PCM audio into `PTTFrameEncoder`, an `AVAudioConverter` instance that produces AAC-LC frames approximately 130 bytes each, representing 64ms of audio per frame.

The `VoiceBurstPacketizer` class defined in [`bitchat/Protocols/VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/VoiceBurstPacket.swift) (lines 69-82) batches one or two frames into a `VoiceBurstPacket` while enforcing the constraint that total payload size remains under `TransportConfig.pttMaxBurstContentBytes` (approximately 210 bytes). This size limitation ensures that after Noise encryption and padding, each packet fits within a single BLE frame. Each burst receives an 8-byte random `burstID` generated via `VoiceBurstPacket.makeBurstID()` (lines 48-56), uniquely identifying the stream for the sender-peer-scope triple.

When the user releases the button, the system emits an `END` packet, writes the accumulated PCM to an `.m4a` file for the traditional file-transfer pipeline, and deletes the temporary buffer if the gesture was cancelled instead.

```swift
// Live capture encoder workflow
let frameData: Data = // AAC-LC frame from AVAudioConverter
var packetizer = VoiceBurstPacketizer(burstID: myBurstID)
let packets = packetizer.add(frameData) // Returns completed packets
for pkt in packets {
    // BLEService.sendVoiceFrame wraps the packet in a Noise payload
    await BLEService.shared.sendVoiceFrame(pkt, to: peerID)
}

```

## Wire Protocol and Transport Layer

BitChat differentiates between direct messages and public mesh broadcasts through distinct wire formats while maintaining consistent payload structures. For direct messages, the system uses `NoisePayloadType.voiceFrame` (hex `0x08`), embedding the `VoiceBurstPacket` inside an existing Noise session with the same confidentiality and integrity guarantees as text messages. The `RelayController` forwards these encrypted frames exactly like other DM traffic.

Public mesh transmissions utilize `MessageType.voiceFrame` (hex `0x29`), sending signed broadcast packets with no additional padding to stay under the BLE MTU limit. These packets relay using the same fragment-style jitter and clamp mechanisms as other mesh traffic. According to [`bitchat/Protocols/VoiceBurstPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/VoiceBurstPacket.swift) (lines 25-34), the binary layout follows `[burstID:8][seq:2][flags:1][payload…]`, where flags indicate `START`, `END`, `CANCELED`, or data frames.

Security varies by transport: DM packets inherit Noise protocol encryption automatically, while public packets require signature verification against the sender's announce before decoding. Unknown or malformed packets undergo immediate dropping at the transport layer.

## Assembly and Playback on the Receiver

The `ChatLiveVoiceCoordinator` class in [`bitchat/ViewModels/ChatLiveVoiceCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatLiveVoiceCoordinator.swift) serves as the core runtime component managing inbound voice streams. It exposes `handleVoiceFramePayload` for DMs and `handlePublicVoiceFramePayload` for public mesh, both delegating to `handle(_:from:scope:nickname:timestamp:)` (lines 75-87) for unified processing.

Upon receiving the first packet, the coordinator creates a new `Assembly` instance that opens a temporary `.aac` file prefixed with `voice_live_`. If the user currently views the conversation with live voice enabled, the assembly registers a `PTTBurstPlayer` instance. The coordinator implements a jitter buffer delaying playback start until approximately 350ms of audio accumulates, with gap handling that skips silent periods exceeding 0.5 seconds (`gapSkipSeconds`).

The `PTTBurstPlayer` receives decoded AAC frames and enqueues them for `AVAudioPlayerNode`, continuing playback until the buffer drains after finalization. When an `END` packet arrives—or an idle timeout fires—the temporary file closes and renames from `voice_live_` to `voice_`, preventing quota-sweep deletion. The finalized `.m4a` voice note sent via the normal file-transfer path later triggers `absorbFinalizedVoiceNote()` (lines 56-84), which replaces the live bubble without creating duplicate chat rows.

```swift
func handleVoiceFramePayload(from peerID: PeerID, payload: Data, timestamp: Date) {
    // Forward to live-voice coordinator for assembly
    chatLiveVoiceCoordinator.handleVoiceFramePayload(
        from: peerID,
        payload: payload,
        timestamp: timestamp
    )
}

// Absorption of finalized voice note
if chatLiveVoiceCoordinator.absorbFinalizedVoiceNote(message) {
    // Live bubble replaced; no duplicate row created
}

```

Resource protection mechanisms include maximum concurrent assembly limits, byte caps, and burst-age limits to prevent mesh abuse, maintaining the network's approximately 15KB/s per-link bandwidth budget. Codec validation restricts incoming frames to AAC-LC 16kHz only, with size caps and rate-limiting guarding against malicious payloads.

## Latency Budget and Bandwidth Constraints

The mouth-to-ear latency for single-hop direct messages totals approximately 470ms, breaking down as follows:

- **Frame accumulation**: 64ms (single frame duration)
- **Encoding and packetization**: approximately 5ms
- **BLE write and delivery**: 30-60ms per hop
- **Jitter buffer**: 350ms initial buffering

Additional relay hops add 10-40ms each, keeping two-to-three-hop paths comfortably under 800ms total latency. This performance profile suits the "walkie-talkie" use case while respecting the mesh network's bandwidth limitations and BLE frame constraints.

## Summary

- **BitChat's push-to-talk system** streams live AAC-LC audio at 16kHz mono, packetizing frames into VoiceBurstPackets under 210 bytes to fit single BLE frames after encryption.
- **Transport security** leverages existing Noise sessions for DMs (payload type `0x08`) and signed broadcasts for public mesh (message type `0x29`), with strict validation rejecting malformed packets.
- **The ChatLiveVoiceCoordinator** manages assembly, jitter buffering (~350ms), and playback via PTTBurstPlayer, while absorbing finalized voice notes to prevent chat duplication.
- **End-to-end latency** averages 470ms for direct connections, scaling to under 800ms for multi-hop mesh relays.
- **Resource limits** including concurrent burst caps and 15KB/s per-link budgets protect the mesh infrastructure from audio stream abuse.

## Frequently Asked Questions

### What audio codec and sample rate does BitChat use for push-to-talk?

BitChat uses **AAC-LC** (Advanced Audio Coding Low Complexity) at **16kHz mono** for all push-to-talk transmissions. The `PTTFrameEncoder` converts raw PCM from `AVAudioEngine` into AAC-LC frames approximately 130 bytes each, representing 64ms of compressed audio. This codec choice balances compression efficiency with computational overhead suitable for BLE transport constraints.

### How does BitChat handle packet loss during live voice transmission?

The system employs a **jitter buffer** in `ChatLiveVoiceCoordinator` that delays playback start until approximately 350ms of audio accumulates, accommodating out-of-order packet arrival. For gaps exceeding 0.5 seconds (`gapSkipSeconds`), the coordinator skips ahead rather than accumulating silence. Missing packets within a burst do not halt playback; the decoder continues with subsequent frames, relying on AAC's error concealment capabilities.

### What is the difference between DM and public mesh voice transmission?

Direct messages use `NoisePayloadType.voiceFrame` (hex `0x08`) transported inside encrypted Noise sessions, providing confidentiality and integrity. Public mesh broadcasts use `MessageType.voiceFrame` (hex `0x29`) as signed packets without padding, verified against the sender's announce. DMs route through existing peer sessions, while public mesh packets flood through the network using standard relay mechanisms with fragment-style jitter control.

### How does the system prevent abuse of live voice resources?

Multiple safeguards protect the mesh: **rate limiting** and **byte caps** restrict per-peer bandwidth usage, **maximum concurrent assembly** limits prevent memory exhaustion, and **burst-age timeouts** discard stale partial transmissions. The 15KB/s per-link budget reserved for mesh traffic ensures voice streams cannot starve text messages or control traffic. Additionally, codec validation rejects any non-AAC-LC payloads, and size checks enforce the ~210 byte maximum per-packet limit.