7.6 KiB
Live voice v1 wire protocol
This document defines the interoperable live push-to-talk protocol used by Android, Wear OS, and iOS clients. Live packets are ephemeral and best-effort; the ordinary voice note sent when the user releases the microphone remains the reliable, persistent fallback.
All multi-byte integers in this document use network byte order (big-endian).
Type assignments
| Layer | Name | Value | Purpose |
|---|---|---|---|
| Outer mesh packet | MessageType.VOICE_FRAME |
0x29 |
Signed public-mesh burst packet |
| Noise inner payload | NoisePayloadType.VOICE_FRAME |
0x08 |
Encrypted direct-message burst packet |
| Outer private packet | MessageType.NOISE_ENCRYPTED |
0x11 |
Existing recipient-directed Noise envelope |
These values are canonical. Implementations must not emit live voice under a different outer or inner type.
Transport envelopes
Public mesh
The sender emits a normal mesh packet with:
- version
1; - type
VOICE_FRAME (0x29); - a broadcast recipient (either no recipient field or the eight-byte all-
FFbroadcast value; receivers accept both encodings); - the encoded
VoiceBurstPacketas its payload; - origin TTL
7; and - an Ed25519 signature over the canonical outer packet signing bytes.
Receivers accept public live voice only from a known peer whose announcement has established a verified nickname and signing key. They must reject an unsigned packet, an invalid signature, a non-broadcast recipient, or a packet whose timestamp is more than 30 seconds from the receiver's current time. A rejected public voice packet must not be relayed.
Public live voice is not added to gossip sync. Older clients ignore the unknown
0x29 type and therefore remain compatible.
Direct message
The sender requires an established Noise session. It encodes:
[NoisePayloadType.VOICE_FRAME: 0x08][VoiceBurstPacket]
and encrypts that value in a recipient-directed NOISE_ENCRYPTED (0x11) mesh
packet. The existing Noise session provides confidentiality and authentication.
Intermediate relays see only the ordinary Noise envelope and route it according
to the existing directed-packet rules.
There are no delivery acknowledgements or retransmissions for live packets. If no established session exists when recording begins, the client records and sends only the ordinary voice note.
VoiceBurstPacket
Every packet in one press-and-hold gesture shares an opaque, randomly generated eight-byte burst ID.
+------------+-------------+-------------+------------------+
| burstID | sequence | flags | payload |
| 8 bytes | UInt16 BE | UInt8 | variant-specific |
+------------+-------------+-------------+------------------+
The fixed header is 11 bytes. flags is a complete discriminator, not a
bitset that may combine values. Unknown or combined flag values are invalid.
START (flags = 0x01)
[codec: UInt8]
Current senders use sequence 0. The only v1 codec is:
| Codec | Value | Encoded frames |
|---|---|---|
| AAC-LC, 16 kHz, mono | 0x01 |
Raw AAC access units without ADTS headers |
Receivers must reject unsupported codec values. Because START can be lost on a best-effort mesh, a receiver may establish a v1 assembly from a valid data packet and use the sole v1 codec.
Data (flags = 0x00)
The payload contains one to eight length-prefixed AAC access units:
[length: UInt16 BE][AAC access unit] ...
Each length must be non-zero and must not extend past the packet. A sender uses
sequence 1 for the first data packet and increments the value for each later
data packet. The sequence counts data packets, not individual AAC frames.
The v1 sender budget is 210 bytes for the entire VoiceBurstPacket. This keeps
one live packet below the transport's fragmentation threshold after the Noise
type byte, authentication tag, and BLE padding are applied. An encoded frame
that cannot fit this budget is dropped rather than fragmented.
END (flags = 0x02)
[totalDataPackets: UInt16 BE][durationMs: UInt32 BE]
totalDataPackets is the number of data packets emitted for the burst; it does
not include START or END. durationMs describes the encoded audio duration.
The END packet uses the next sequence value after the final data packet.
Receivers use the total to account for tail loss before finalizing playback.
CANCELED (flags = 0x04)
CANCELED has no payload and uses the next sequence value. A receiver stops live playback, discards buffered audio, removes the transient message, and does not wait for a finalized voice note.
Sender lifecycle
A successful live gesture follows this order:
- Generate one eight-byte burst ID.
- Emit START at sequence
0after the first encoded AAC access unit becomes available. - Emit data packets starting at sequence
1. - On release, flush pending data and emit END.
- Send the finalized M4A through the existing voice-note transfer path.
The finalized file name is voice_<burst-id-hex>.m4a, where the hexadecimal
component is the 16 lowercase characters representing the same eight-byte
burst ID. This lets the receiver replace the transient live capture with the
reliable final voice note without adding another wire field.
If capture is canceled or does not produce a valid recording, the sender emits CANCELED and does not send a final note.
AAC-LC at 16 kHz uses 1,024 samples per access unit, so each encoded frame represents 64 ms of audio. Live access units are ADTS-less; receivers may add an ADTS header locally for streaming playback or temporary-file assembly.
Receiver safety and ordering
Receivers must fail closed on malformed framing and must bound concurrent assemblies, buffered out-of-order packets, bytes per burst, and inbound byte rate. The Android/Wear OS v1 bounds are:
- 8 concurrent assemblies;
- 384 KiB per burst;
- 6,000 inbound bytes per second per assembly, with a two-second initial allowance; and
- 128 buffered out-of-order packets.
Duplicate and already-delivered sequence values are ignored. Android/Wear OS wait 550 ms for a sequence gap before skipping it and finalize an idle partial burst after 3 seconds. These timers are receiver policy rather than additional wire fields.
Relay behavior
Public VOICE_FRAME packets use the normal mesh relay path only after signature,
sender, timestamp, recipient, and burst validation succeeds. Relays add a small
8–25 ms jitter. In a mesh larger than six peers, the relayed TTL is capped at
five after the normal per-hop decrement. Live packets remain excluded from
gossip sync and file-transfer retransmission.
Private frames retain the existing NOISE_ENCRYPTED relay behavior because an
intermediate node cannot inspect the 0x08 inner type.
Golden vectors
For burst ID 01 02 03 04 05 06 07 08, the canonical encodings are:
# START, sequence 0, AAC-LC/16 kHz/mono
01 02 03 04 05 06 07 08 00 00 01 01
# One data packet, sequence 1, one three-byte frame DE AD BE
01 02 03 04 05 06 07 08 00 01 00 00 03 DE AD BE
# END, sequence 42, 41 data packets, duration 2688 ms
01 02 03 04 05 06 07 08 00 2A 02 00 29 00 00 0A 80
# CANCELED, sequence 3
01 02 03 04 05 06 07 08 00 03 04
The corresponding executable Android vectors live in
VoiceBurstPacketTest.encodedGoldenVectorsMatchIos.
Implementation references
app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.ktapp/src/main/java/com/bitchat/android/model/NoiseEncrypted.ktapp/src/main/java/com/bitchat/android/features/voice/VoiceBurstPacket.ktapp/src/main/java/com/bitchat/android/features/voice/LiveVoiceManager.ktapp/src/test/java/com/bitchat/android/features/voice/VoiceBurstPacketTest.kt