Can BitChat Transmit Voice Bursts Over BLE Mesh? Technical Implementation Guide

Yes, BitChat can transmit voice bursts over BLE mesh by encapsulating AAC-LC audio frames into VoiceBurstPacket structures that respect BLE frame budgets, then routing them through the mesh stack as opaque encrypted binary payloads.

The permissionlesstech/bitchat repository implements an offline-first messaging protocol that supports push-to-talk (PTT) voice communication. According to the source code in bitchat/Protocols/VoiceBurstPacket.swift and the architecture documentation in docs/BLE-ARCHITECTURE-V3.md, BitChat transmits voice bursts over BLE mesh without requiring internet connectivity, using packetization strategies that ensure real-time audio fits within the constraints of BLE link layers.

How Voice Bursts Travel Over BLE Mesh

The VoiceBurstPacket Wire Format

BitChat defines a concrete packet structure in bitchat/Protocols/VoiceBurstPacket.swift that represents a single transmissible unit of voice data. Each VoiceBurstPacket contains a unique burst identifier, a sequence number for ordering, flags indicating the packet type (start, data, end, or cancel), and the actual audio payload. The structure is designed to remain small enough that after Noise encryption and protocol padding, the final binary representation fits comfortably within a single BLE LE frame.

The packet supports multiple codec signatures, with .aacLC16kMono being the standard for PTT sessions. This allows the receiver to initialize the correct decoder upon receiving the START marker without external negotiation.

Packetization and BLE Frame Budget

Before transmission, raw microphone captures flow through VoiceRecorder.swift and VoiceCaptureSession.swift, which encode audio into AAC-LC frames. The VoiceBurstPacketizer class (defined alongside VoiceBurstPacket) aggregates these frames into chunks that respect TransportConfig.pttMaxBurstContentBytes. This configuration constant represents the maximum usable bytes per BLE mesh payload after accounting for encryption overhead and protocol headers.

The packetizer maintains internal state to track sequence numbers and burst boundaries. When the user initiates a push-to-talk session, the system generates a unique burst ID via VoiceBurstPacket.makeBurstID(), ensuring that packets belonging to different voice sessions can be demultiplexed even when interleaved with other mesh traffic.

Code Implementation: Sending and Receiving Voice Bursts

Transmitting Voice Over the Mesh

The following Swift implementation demonstrates how BitChat constructs and transmits voice bursts using the BLE transport layer:

import bitchat.Features.voice

// Generate a unique burst ID for this voice session
let burstID = VoiceBurstPacket.makeBurstID()

// Initialize the packetizer with the BLE mesh byte budget
var packetizer = VoiceBurstPacketizer(burstID: burstID)

// Send the START packet to advertise codec information
let startPacket = VoiceBurstPacket(
    burstID: burstID,
    seq: 0,
    kind: .start(codec: .aacLC16kMono)
)!.encode()
BLEService.shared.send(data: startPacket)

// Process encoded AAC frames and transmit full packets
for aacFrame in encodedFrames {
    let fullPackets = packetizer.add(aacFrame)
    for packet in fullPackets {
        BLEService.shared.send(data: packet)
    }
}

// Transmit the END marker with metadata
let endPacket = VoiceBurstPacket(
    burstID: burstID,
    seq: packetizer.nextSeq,
    kind: .end(totalDataPackets: packetizer.dataPacketCount,
               durationMs: UInt32(totalDurationMs))
)!.encode()
BLEService.shared.send(data: endPacket)

Receiving and Decoding Voice Bursts

On the receiving side, incoming BLE data passes through the same BLEService into the voice subsystem:

func handleIncomingBLEData(_ data: Data) {
    guard let burst = VoiceBurstPacket.decode(data) else { return }
    
    switch burst.kind {
    case .start(let codec):
        // Initialize decoder based on advertised codec
        voiceDecoder = VoiceDecoder(codec: codec)
    case .frames(let frames):
        // Accumulate frames for playback
        frames.forEach { voiceDecoder?.append(frame: $0) }
    case .end(let totalPackets, let durationMs):
        // Finalize audio playback session
        voiceDecoder?.finalize(totalPackets: totalPackets, 
                               durationMs: durationMs)
    case .canceled:
        // Abort current playback and reset state
        voiceDecoder?.reset()
    }
}

These implementations show how VoiceBurstPacket acts as the bridge between audio codecs and the BLE transport, enabling true mesh-based voice communication.

Key Components in the Source Code

The voice-over-mesh capability spans several specific files within the repository:

Summary

  • BitChat transmits voice bursts over BLE mesh using specialized VoiceBurstPacket structures that include burst IDs, sequence numbers, and codec metadata.
  • The VoiceBurstPacketizer batches AAC-LC frames to fit within TransportConfig.pttMaxBurstContentBytes, ensuring compatibility with BLE frame constraints after Noise encryption.
  • BLEService.swift routes voice packets through the mesh network identically to text messages, enabling offline push-to-talk functionality.
  • The implementation supports robust session management via START, END, and CANCEL packet types defined in the protocol layer.

Frequently Asked Questions

Does BitChat require internet connectivity to send voice bursts?

No. According to the BLE-ARCHITECTURE-V3.md design documents and the BLEService.swift implementation, voice packets transmit as encrypted binary payloads directly over the BLE mesh network. The system operates entirely offline, routing voice bursts through peer-to-peer mesh hops without cellular or Wi-Fi connectivity.

What audio codec does BitChat use for BLE mesh voice transmission?

BitChat uses AAC-LC (Advanced Audio Coding Low Complexity) at 16 kHz mono for voice bursts. This codec provides efficient compression suitable for low-bandwidth BLE links while maintaining intelligibility. The specific codec signature is embedded in the START packet's .aacLC16kMono variant, allowing receivers to initialize compatible decoders dynamically.

How does BitChat handle packet loss during voice transmission?

The VoiceBurstPacket structure includes monotonically increasing sequence numbers that allow the receiver to detect missing packets. While the current implementation in VoiceCaptureSession.swift focuses on real-time streaming without retransmission for latency-sensitive PTT, the packet format supports NACK (negative acknowledgment) flags that future iterations could use to request specific missing frames from mesh neighbors.

Is voice data encrypted when transmitted over the BLE mesh?

Yes. As documented in the BLE architecture, all payloads—including voice bursts—undergo Noise protocol encryption before transmission. The VoiceBurstPacket data is treated as an opaque binary blob by BLEService.shared.send, meaning the voice content receives the same cryptographic protections as text messages within the BitChat mesh network.

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 →