Voice Guide
Overview
discord.go provides full voice support including WebSocket connection management, UDP transport, RTP packet handling, AES-256-GCM transport encryption, Opus frame sending, and DAVE/MLS end-to-end encryption. This guide covers the voice connection lifecycle, encryption, and audio handling.
Voice Connection Lifecycle
Voice connections follow a two-step process:
- Send a Voice State Update via the main Gateway to join a channel.
- Receive
VOICE_SERVER_UPDATEand create avoice.Clientto connect to the voice WebSocket.
// Step 1: Join a voice channel via the main gateway
err := botClient.JoinVoiceChannel(guildID, channelID, false, false)
if err != nil {
log.Fatal(err)
}
// Step 2: Handle VOICE_SERVER_UPDATE to create the voice client
botClient.OnRawEvent(func(ctx context.Context, event string, data json.RawMessage) {
if event != "VOICE_SERVER_UPDATE" {
return
}
var vsu struct {
Token string `json:"token"`
GuildID string `json:"guild_id"`
Endpoint string `json:"endpoint"`
}
json.Unmarshal(data, &vsu)
voiceClient := voice.NewClient(conn, guildID, channelID, userID)
voiceClient.Token = vsu.Token
voiceClient.Endpoint = vsu.Endpoint
voiceClient.SessionID = sessionID
// Connect and start sending audio
})Opus Encoding and Decoding
The library handles transport encryption (AES-256-GCM) automatically. Applications provide Opus-encoded audio frames via SendOpus:
// Send a 20ms Opus frame
err := voiceClient.SendOpus(opusFrame)
if err != nil {
log.Printf("send opus: %v", err)
}SendOpus handles:
- RTP header construction (sequence, timestamp, SSRC)
- DAVE MLS encryption (if a DAVE session is active)
- AES-256-GCM transport encryption with the cached cipher
- Nonce counter management
- Packet assembly (header + ciphertext + nonce suffix)
For receiving audio, set the OnAudioPacket callback:
voiceClient.OnAudioPacket = func(userID string, sequence uint16, timestamp uint32, audio []byte) {
// audio is a decrypted Opus frame
// Decode with your Opus decoder (e.g., hraban/opus)
}DAVE Protocol
DAVE (Discord Audio Visual End-to-end) provides MLS-based end-to-end encryption for voice. The library supports DAVE protocol version 1.
When a DAVE session is active, SendOpus encrypts the Opus frame with MLS before transport encryption. The DaveSession field on voice.Client manages the MLS group state.
DAVE decryption happens automatically in the audio read loop before delivering frames to OnAudioPacket.
Encryption Modes
The library supports aead_aes256_gcm_rtpsize mode:
- AES-256-GCM authenticated encryption.
- The RTP header (12 bytes) is used as additional authenticated data (AAD).
- A 4-byte nonce counter is zero-padded to 12 bytes for GCM.
- The nonce is appended as a 4-byte suffix to each packet.
The cipher.AEAD is cached on the voice.Client when the secret key is received in SessionDescription, avoiding per-packet cipher allocation.
The nonce counter is a uint32 that increments on each SendOpus call. If the counter reaches its maximum value (2^32 - 1), SendOpus returns an error instead of wrapping to zero, which would reuse a nonce and break GCM. The caller must re-establish the voice session to get a new key. At 50 packets per second (20 ms Opus frames), this limit is reached after approximately 2.7 years of continuous audio.
UDP Packet Structure
Voice packets follow this layout:
[RTP Header (12 bytes)] [Encrypted Payload] [Nonce Suffix (4 bytes)]The RTP header contains:
- Version (2 bits, always 2)
- Padding (1 bit)
- Extension (1 bit)
- CSRC count (4 bits)
- Marker (1 bit)
- Payload type (7 bits, 0x78)
- Sequence number (16 bits)
- Timestamp (32 bits)
- SSRC (32 bits)
Speaking Flags
Use speaking flags to indicate the bot is transmitting audio:
// The voice client sets speaking state automatically when sending audio
// Speaking flags: SpeakingFlagMicrophone, SpeakingFlagSoundshareVoice Receiving
The audio read loop runs in a goroutine after the voice connection is established. It:
- Reads UDP packets from the voice server.
- Extracts the RTP header and nonce suffix.
- Decrypts the transport encryption using the cached AEAD.
- If DAVE is active, decrypts the end-to-end encryption.
- Strips RTP extension data.
- Delivers the decrypted Opus frame to
OnAudioPacket.
Common Patterns
- Always join via the main gateway first, then create the voice client.
- Use
context.Contextwith deadlines for voice operations. - Send Opus frames at 20ms intervals (50 packets/second).
- Handle
VOICE_SERVER_UPDATEandVOICE_STATE_UPDATEevents.
Best Practices
- Keep the voice WebSocket and UDP connection alive with heartbeats.
- Monitor
unackedHeartbeatsfor connection health. - Use DAVE end-to-end encryption when available.
- Close the voice client when leaving the channel.
Common Mistakes
- Not waiting for
VOICE_SERVER_UPDATEbefore creating the voice client. - Sending non-Opus data to
SendOpus. - Not setting
OnAudioPacketbefore connecting if receiving is needed. - Forgetting to close the voice client on shutdown.