How the Push-to-Talk (PTT) Voice Burst Protocol Works in BitChat

BitChat’s push-to-talk (PTT) voice burst protocol streams live AAC-LC audio frames over BLE mesh or Noise-encrypted direct messages while simultaneously recording a fallback voice-note file, ensuring real-time walkie-talkie functionality with guaranteed delivery.

The voice burst protocol is the core mechanism that enables BitChat’s smart Push-to-Talk (PTT) feature. Unlike traditional voice notes that require full recording before transmission, this protocol—implemented in the permissionlesstech/bitchat repository—transmits encoded audio packets the moment the user presses the microphone button. The system gracefully degrades to a standard voice-note file if live streaming fails, providing both low-latency interaction and reliability.

Capturing and Encoding Voice Bursts

Audio capture begins the instant the user taps the microphone button, creating a real-time pipeline from hardware to network.

Audio Capture Pipeline

The PTTCaptureEngine class (bitchat/Features/voice/PTTCaptureEngine.swift) instantiates a fresh AVAudioEngine for each PTT session. It installs an input tap on the audio node, then pipes raw PCM buffers through PTTInputResampler to convert the stream to 16 kHz mono format. This standardized format ensures consistent encoding regardless of device hardware capabilities.

AAC-LC Frame Encoding

Resampled audio flows into PTTFrameEncoder (bitchat/Features/voice/PTTAudioCodec.swift), which compresses the stream using AAC-LC at approximately 16 kbps. Each encoded frame produces a payload of roughly 130 bytes, extracted from AVAudioCompressedBuffer objects. These compact frames minimize wireless transmission latency while maintaining acceptable voice quality for walkie-talkie-style communication.

The Voice Burst Wire Protocol

A voice burst consists of a sequence of packets sharing an 8-byte random burstID. The wire format uses a compact binary header:


[burstID:8][seq:UInt16 BE][flags:1][payload…]

  • burstID – A random 8-byte identifier linking all frames in a single utterance.
  • seq – Big-endian UInt16 sequence number for ordering and loss detection.
  • flags – Control bits indicating frame type:
    • START (0x01) – Sent at the beginning and retransmitted every ~2 seconds for mid-burst synchronization, carrying codec description metadata.
    • DATA (0x00) – Standard audio payload containing length-prefixed AAC frames.
    • END (0x02) – Signals burst completion and carries total packet count and duration.
    • CANCELED (0x04) – Immediately aborts playback on receivers.

Transmission occurs via dedicated packet types: 0x29 for public BLE mesh broadcasts and 0x08 for Noise-encrypted direct messages (bitchat/Services/BLE/BLEService.swift and bitchat/Nostr/BitchatProtocol.swift).

Session Lifecycle and Transmission

The PTTLiveVoiceSession class (bitchat/Features/voice/VoiceCaptureSession.swift) orchestrates the end-to-end flow, subclassing VoiceCaptureSession to manage the dual-path streaming and recording logic.

Session State Management

When initiated, the session generates a unique burstID and enters a streaming state. The onFrames closure in PTTCaptureEngine delivers encoded AAC data to VoiceBurstPacketizer, which wraps payloads in the wire format described above. The session simultaneously writes the same PCM data to an .m4a file on disk, creating the fallback voice note.

BLE Mesh Transmission

Each packetized frame routes through BLEService.sendVoiceFrame for direct messages, or as a signed MessageType.voiceFrame for public mesh broadcasts. The protocol transmits frames without fragmentation, relying on the underlying BLE link layer for packet integrity.

Receiving and Playing Voice Bursts

Inbound voice bursts reassemble on the receiver side through a coordinated assembly and playback pipeline.

Frame Assembly and Buffering

ChatLiveVoiceCoordinator (bitchat/ViewModels/ChatLiveVoiceCoordinator.swift) monitors incoming packets and instantiates a VoiceBurstAssembler for each unique (sender, burstID) pair. The assembler maintains a jitter buffer, reordering frames by sequence number and detecting gaps. Missing frames are substituted with silence to prevent playback stutter.

Playback and Fallback Handling

Once approximately 350 ms of audio buffers, the PTTBurstPlayer begins decoding via PTTFrameDecoder and pipes PCM data to an AVAudioPlayerNode. When the burst terminates—signaled by the END flag or a timeout—the player writes the assembled AAC frames to a temporary file, later promoted to voice_<burstID>.m4a. If the receiver already rendered the live burst, this final file replaces the provisional bubble, ensuring no data is lost if packets dropped during transmission.

Policy Controls and UX Behavior

BitChat applies strict policy gates to prevent accidental live audio transmission. The PTT option appears only when noiseService.hasEstablishedSession returns true or when the peer is reachable on the mesh; otherwise, the UI defaults to classic voice-note recording (docs/PUSH-TO-TALK-DESIGN.md).

Users control live voice functionality globally via PTTSettings.liveVoiceEnabled (bitchat/Features/voice/PTTSettings.swift). When disabled, incoming bursts store silently without auto-playback, displaying a "live bubble" requiring manual tap to hear. Auto-playback activates only when the app is foregrounded, the conversation is active, and the toggle is enabled.

Summary

  • Real-time streaming – The voice burst protocol transmits AAC-LC frames immediately upon capture using an 8-byte burstID and 16-bit sequence numbers for ordering.
  • Dual-path reliability – While streaming live, the system records an .m4a fallback file named with the burstID, ensuring delivery even if the live stream fails.
  • Wire efficiency – Compact 130-byte frames at ~16 kbps travel over BLE packet types 0x29 (public) and 0x08 (DM) without fragmentation.
  • Smart assembly – Receivers buffer ~350 ms before playback, filling missing frames with silence and finalizing bursts into standard voice-note files.
  • User control – Global settings and session prerequisites prevent unintended transmission, requiring explicit Noise sessions or mesh visibility.

Frequently Asked Questions

What audio codec does BitChat use for PTT voice bursts?

BitChat uses AAC-LC (Advanced Audio Coding Low Complexity) running at 16 kbps. The PTTFrameEncoder class converts 16 kHz mono PCM buffers into ~130-byte compressed frames, balancing bandwidth efficiency with intelligible voice quality suitable for push-to-talk communication.

How does BitChat handle packet loss during a live voice burst?

The protocol employs a jitter buffer in VoiceBurstAssembler that reorders frames by sequence number. Missing sequence numbers are detected after a timeout, and the PTTBurstPlayer substitutes silence for lost frames rather than pausing playback. Additionally, the fallback .m4a file sent after the burst completes allows receivers to replace any incomplete live playback with the full recording.

What is the latency budget for live PTT playback?

The receiver intentionally delays playback for approximately 350 milliseconds to accumulate a buffer of incoming frames. This jitter buffer absorbs network variance and prevents underruns, resulting in a total end-to-end latency typically under one second—sufficient for natural walkie-talkie conversation without noticeable echo or collision.

How does the system distinguish between live PTT and regular voice notes?

The UI and backend check for established Noise sessions or mesh reachability before offering the live PTT mode. When active, PTTLiveVoiceSession manages the dual stream-and-record pipeline, whereas standard voice notes use the VoiceCaptureSession base class without the packetizer or VoiceBurstPacketizer components. Receivers identify live bursts by the MessageType.voiceFrame packet type and the presence of burst headers, distinct from fileTransfer voice-note deliveries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →