Sign in

    discord_voice

    Experimental Discord voice for gaato/discord bots on MoonBit's native backend: voice gateway v8, Opus playback and receive, DAVE E2EE.

    discord
    voice
    dave
    opus
    native
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    7 days ago
    Downloads
    26

    #gaato/discord_voice

    CI Voice shim License

    Experimental Discord voice for bots built with gaato/discord, on MoonBit's native backend: voice gateway v8, Discord's DAVE end-to-end encryption, Opus playback, and per-user Opus receive. The module lives in the discord_voice/ directory of gaato/discord.mbt.

    moon add gaato/discord_voice

    Import it next to gaato/discord, as @voice:

    import {
    "gaato/discord",
    "gaato/discord_voice" @voice,
    }

    Create one VoiceClient per bot, attach it to the builder, and join from a READY handler or a bot service:

    let voice = @voice.VoiceClient()
    voice.attach(bot)
    // later, with a GatewayCtx:
    let connection = voice.join(ctx, guild_id, channel_id)

    The voice guide covers playback, receive, reconnects, and the examples.

    #Native runtime

    DAVE MLS and media encryption come from gaato/dave, the binding to Discord's official libdave (pinned at v1.2.0/cpp). RTP transport AEAD comes from a small Rust library released as voice-shim-v0.1.0; its sources are in shim/. Both are loaded at run time, not linked or installed system-wide.

    A native build bootstraps and verifies both prebuilt runtimes when asked to:

    env DISCORD_VOICE_REQUIRE_SHIM=1 MBT_DAVE_REQUIRE_NATIVE=1 moon build --target native --release

    This module and gaato/dave declare prebuild hooks that Moon runs with node for native builds, so Node.js must be on PATH for every native moon build, moon test, and moon run of a program that depends on this module. Without the opt-in variables the hooks exit without downloading anything. Moon does not run them for moon check.

    #Design

    See DESIGN.md for the accepted design and its milestones.

    #License

    Apache-2.0

    AudioSource

    pub(open) trait AudioSource {
    async fn next_frame(Self) -> Bytes?
    }

    A source of pre-encoded 20 ms Opus frames. None marks end of stream.

    DaveBackend

    trait DaveBackend

    The private seam between gateway orchestration and the official libdave wrapper. Keeping this package-private lets white-box tests model protocol outcomes without making an alternate crypto provider part of the API.

    VoiceTransport

    pub(open) trait VoiceTransport {
    async fn recv(Self) -> VoiceFrame
    async fn send_text(Self, String) -> Unit
    async fn send_binary(Self, Bytes) -> Unit
    async fn close(Self, code~ : Int) -> Unit
    }

    The network seam used by VoiceGateway. Tests can provide an in-memory implementation without opening a WebSocket.

    VoiceUdp

    pub(open) trait VoiceUdp {
    async fn recv(Self, FixedArray[Byte]) -> Int
    async fn send(Self, Bytes) -> Unit
    async fn close(Self) -> Unit
    }

    UDP seam used by voice discovery and media transport.

    DaveError

    pub(all) suberror DaveError {
    DaveInvalid(reason~ : String)
    DaveUnavailable(reason~ : String)
    DaveInternal(reason~ : String)
    } derive(Eq,
    Debug
    )

    Errors reported while coordinating Discord's DAVE protocol.

    OggOpusError

    pub(all) suberror OggOpusError {
    InvalidOggPage(reason~ : String)
    InvalidOpusStream(reason~ : String)
    } derive(Eq,
    Debug
    )

    Errors raised while parsing Ogg pages and Opus stream headers.

    RtpError

    pub(all) suberror RtpError {
    InvalidRtpPacket(reason~ : String)
    } derive(Eq,
    Debug
    )

    Errors raised while validating an RTP packet.

    VoiceCryptoError

    pub(all) suberror VoiceCryptoError {
    NoCompatibleMode(modes~ : Array[String])
    ShimUnavailable(reason~ : String)
    InvalidSecretKey(length~ : Int)
    InvalidEncryptedPacket(reason~ : String)
    AuthenticationFailed(reason~ : String)
    AeadFailed(status~ : Int, reason~ : String)
    NonceExhausted
    } derive(Eq,
    Debug
    )

    Errors raised by transport-mode negotiation and packet encryption.

    VoiceEnvelopeError

    pub(all) suberror VoiceEnvelopeError {
    InvalidTextFrame(reason~ : String)
    InvalidBinaryFrame(reason~ : String)
    } derive(Eq,
    Debug
    )

    A malformed JSON or binary voice gateway envelope.

    VoiceError

    pub(all) suberror VoiceError {
    MissingIntent(intent~ : String)
    JoinTimeout
    ShimUnavailable(reason~ : String)
    NoCompatibleMode(offered~ : Array[String])
    GatewayClosed(code~ : Int?)
    IpDiscoveryFailed(reason~ : String)
    Disconnected
    InvalidBacklog(value~ : Int)
    ClientNotRunning
    } derive(
    Debug
    )

    Errors surfaced while joining or operating a voice connection.

    VoiceTransportClosed

    pub(all) suberror VoiceTransportClosed {
    VoiceTransportClosed(code~ : Int?, reason~ : String)
    } derive(
    Debug
    )

    Raised when a voice transport is closed or cannot continue.

    VoiceUdpError

    pub(all) suberror VoiceUdpError {
    InvalidDiscoveryPacket(reason~ : String)
    IpDiscoveryFailed(attempts~ : Int, reason~ : String)
    } derive(Eq,
    Debug
    )

    Errors raised by voice UDP setup and IP discovery.

    DaveAction

    pub(all) enum DaveAction {
    SendJson(Json)
    SendBinary(op~ : Int, payload~ : Bytes)
    SwitchMediaContext(protocol_version~ : Int)
    } derive(Eq,
    Debug
    )

    Side effects requested by the DAVE gateway/MLS orchestration state machine.

    DaveCommitOutcome

    type DaveCommitOutcome derive(Eq,
    Debug
    )

    DaveMachine

    pub struct DaveMachine {
    // private fields
    }

    DAVE gateway/MLS orchestration. Networking remains outside this type.

    DaveMachine::active_protocol_version

    fn DaveMachine::active_protocol_version(self : DaveMachine) -> Int

    Return the protocol version currently selected for outbound media.

    DaveMachine::encrypt_opus_frame

    fn DaveMachine::encrypt_opus_frame(self : DaveMachine, ssrc~ : UInt, frame : Bytes) -> Bytes raise DaveError

    Encrypt one Opus frame. Passthrough is delegated to libdave as well, so the encryptor remains the sole owner of media-transition state.

    DaveMachine::handle

    fn DaveMachine::handle(self : DaveMachine, message : VoiceMessage) -> Array[DaveAction] raise DaveError

    Handle one decoded voice-gateway message and return transport actions.

    DaveMachine::new

    fn DaveMachine::new(protocol_version~ : Int, self_user_id~ : String, channel_id~ : UInt64, roster? : Array[String]) -> DaveMachine raise DaveError

    Create the production DAVE state machine backed by the official libdave wrapper. protocol_version is the version selected by the voice gateway.

    DaveRosterChange

    type DaveRosterChange derive(Eq,
    Debug
    )

    DaveWelcomeOutcome

    type DaveWelcomeOutcome derive(Eq,
    Debug
    )

    EncryptionMode

    pub(all) enum EncryptionMode {
    AeadAes256GcmRtpsize
    AeadXChaCha20Poly1305Rtpsize
    } derive(Eq,
    Debug
    )

    Discord voice transport encryption modes supported by this package.

    EncryptionMode::negotiate

    fn EncryptionMode::negotiate(modes : Array[String]) -> EncryptionMode raise VoiceCryptoError

    Select the preferred common Discord transport encryption mode.

    OggOpusSource

    pub struct OggOpusSource {
    // private fields
    }

    In-memory Ogg/Opus source. Opus packets must already represent 20 ms audio frames; this demuxer does not decode or resample them.

    OggOpusSource::channels

    fn OggOpusSource::channels(self : OggOpusSource) -> Int

    The channel count declared by the stream's OpusHead header.

    OggOpusSource::from_bytes

    fn OggOpusSource::from_bytes(bytes : Bytes) -> OggOpusSource raise OggOpusError

    Parse a complete in-memory Ogg Opus file into an audio source, validating its OpusHead/OpusTags headers.

    OggOpusSource::from_reader

    Open an Ogg/Opus source backed by an asynchronous reader. The constructor consumes and validates the OpusHead and OpusTags packets; audio packets are read lazily by AudioSource::next_frame.

    Example

    async test {
    let file = @fs.open("voice.ogg")
    defer file.close()
    let source = @voice.OggOpusSource::from_reader(file)
    let audio : &@voice.AudioSource = source
    while audio.next_frame() is Some(frame) {
    play_opus_frame(frame)
    }
    }

    OggOpusSource::next_frame

    async fn OggOpusSource::next_frame(self : OggOpusSource) -> Bytes?

    OggOpusSource::pre_skip

    fn OggOpusSource::pre_skip(self : OggOpusSource) -> Int

    Samples (at 48 kHz) to drop before playback, from the OpusHead header.

    OggOpusWriter

    pub struct OggOpusWriter {
    // private fields
    }

    Pure in-memory Ogg/Opus muxer. Header pages are available immediately from take_output; audio pages become available as packet or lacing limits are reached, and finish emits the final EOS page.

    OggOpusWriter::finish

    fn OggOpusWriter::finish(self : OggOpusWriter) -> Unit

    Flush remaining packets in a final EOS page. Repeated calls are no-ops.

    OggOpusWriter::new

    fn OggOpusWriter::new(channels? : Int, pre_skip? : Int, serial? : UInt, max_packets_per_page? : Int) -> OggOpusWriter

    Create an Ogg/Opus muxer and queue its OpusHead and OpusTags pages.

    Example

    async test {
    let writer = @voice.OggOpusWriter::new(channels=2, pre_skip=312)
    for frame in recorded_opus_frames() {
    writer.write_frame(frame)
    }
    writer.finish()
    let file = @fs.create("recording.ogg")
    defer file.close()
    file.write(writer.take_output())
    }

    Panics

    Panics if channels or pre_skip cannot be represented in an Opus header, or if max_packets_per_page is not positive.

    OggOpusWriter::take_output

    fn OggOpusWriter::take_output(self : OggOpusWriter) -> Bytes

    Drain all complete Ogg pages generated so far.

    OggOpusWriter::write_frame

    fn OggOpusWriter::write_frame(self : OggOpusWriter, opus : Bytes, samples? : Int) -> Unit raise OggOpusError

    Queue one complete Opus packet and advance the 48 kHz granule position.

    OggPacketReader

    pub struct OggPacketReader {
    // private fields
    }

    Streaming Ogg packet reassembler. Feed arbitrary chunks with push, then pull complete packets with next_packet.

    OggPacketReader::new

    Create an empty reader; feed it Ogg data with push and drain packets with next_packet.

    OggPacketReader::next_packet

    fn OggPacketReader::next_packet(self : OggPacketReader) -> Bytes? raise OggOpusError

    Return the next complete Ogg packet, or None when more input is needed.

    OggPacketReader::push

    fn OggPacketReader::push(self : OggPacketReader, bytes : Bytes) -> Unit

    Add bytes to the streaming input without requiring a page boundary.

    PendingVoiceEvent

    pub(all) struct PendingVoiceEvent[T] {
    wait : async (Int?) -> T?
    cancel : () -> Unit
    }

    A main-gateway event the voice join waits for. wait returns it, or None after timeout_ms; cancel abandons it without waiting.

    ReceivedAudio

    pub(all) enum ReceivedAudio {
    Opus(sequence~ : Int, timestamp~ : UInt, opus~ : Bytes)
    Lost(count~ : Int)
    } derive(Eq,
    Debug
    )

    One item of a user's received audio, in RTP sequence order.

    ReorderBuffer

    pub struct ReorderBuffer[T] {
    // private fields
    }

    A small RTP reorder window. The first packet establishes the expected sequence; later gaps are held until hold_ms elapses or eight packets are buffered.

    ReorderBuffer::new

    fn[T] ReorderBuffer::new(hold_ms? : Int) -> ReorderBuffer[T]

    Create an empty window that holds packets behind a sequence gap for up to hold_ms milliseconds.

    ReorderBuffer::push

    fn[T] ReorderBuffer::push(self : ReorderBuffer[T], seq~ : Int, arrival_ms~ : Int64, packet : T) -> Array[ReorderOutput[T]]

    Insert one RTP packet. Arrival time is supplied by the caller so timeout behavior is deterministic in tests.

    ReorderOutput

    pub(all) enum ReorderOutput[T] {
    Deliver(T)
    Lost(count~ : Int)
    } derive(Eq,
    Debug
    )

    Values produced while restoring RTP sequence order.

    RtpPacket

    pub(all) struct RtpPacket {
    sequence : UInt16
    timestamp : UInt
    ssrc : UInt
    header_len : Int
    header : Bytes
    payload : Bytes
    } derive(Eq,
    Debug
    )

    A parsed RTP packet with the rtpsize cleartext header separated.

    TransportCipher

    pub struct TransportCipher {
    // private fields
    }

    Stateful voice transport cipher. The sender counter never wraps: after 2^32 packets seal raises NonceExhausted instead of reusing a nonce. Receivers reconstruct nonces from the packet suffix and need no counter.

    TransportCipher::new

    fn TransportCipher::new(mode : EncryptionMode, secret_key : Bytes) -> TransportCipher raise VoiceCryptoError

    Create a cipher for mode from the 32-byte session secret_key delivered by the voice gateway.

    TransportCipher::open

    fn TransportCipher::open(self : TransportCipher, packet : Bytes, header_len~ : Int) -> Bytes raise VoiceCryptoError

    Authenticate and decrypt a packet, returning header || plaintext.

    TransportCipher::seal

    fn TransportCipher::seal(self : TransportCipher, header~ : Bytes, plaintext : Bytes) -> Bytes raise VoiceCryptoError

    Encrypt an RTP payload and append the four-byte nonce suffix.

    VoiceClient

    pub struct VoiceClient {
    // private fields
    }

    A bot's voice connections: at most one per guild. Create one per bot, attach it to the bot's builder, and join from event or service handlers. The connections run in the client's own task group, which lives as long as run (the service that attach registers).

    VoiceClient::VoiceClient

    fn VoiceClient::VoiceClient() -> VoiceClient

    VoiceClient::attach

    Run the client as the service "voice" of the bot that builder builds.

    VoiceClient::connection

    The connection the client holds for guild_id, if any.

    VoiceClient::join

    Join a guild voice channel through ctx's bot and wait until its voice transport is ready. See join_via for the behavior.

    VoiceClient::join_via

    async fn VoiceClient::join_via(self : VoiceClient, port : VoiceGatewayPort, guild_id :
    Id
    [
    GuildMarker
    ], channel_id :
    Id
    [
    ChannelMarker
    ], self_mute? : Bool, self_deaf? : Bool, timeout_ms? : Int, telemetry? : (VoiceTelemetry) -> Unit) -> VoiceConnection

    Join a guild voice channel and wait until its voice transport is ready.

    A second call for the same guild returns the registered connection; moving an existing connection to another channel is outside this API. Discord may send no response events when the channel is full and the bot lacks MOVE_MEMBERS; that case raises VoiceError::JoinTimeout.

    The bot must connect the main Gateway with the GUILD_VOICE_STATES intent. This method raises VoiceError::MissingIntent before sending Opcode 4 when that intent is absent, and VoiceError::ClientNotRunning when run has not started within timeout_ms.

    Pass telemetry to observe non-fatal voice gateway and media diagnostics.

    VoiceClient::run

    async fn VoiceClient::run(self : VoiceClient) -> Unit

    Run the client's task group until cancelled. Connections and their background tasks run in it, so cancelling run closes every connection. attach runs it as a bot service; call it directly only when the client is not attached to a builder.

    VoiceConnection

    pub struct VoiceConnection {
    // private fields
    }

    High-level owner of voice gateway, UDP media, DAVE, sender, and receiver tasks. All background work is attached to the task group passed to start.

    VoiceConnection::disconnect

    async fn VoiceConnection::disconnect(self : VoiceConnection) -> Unit

    Leave the Discord voice state and tear down media transports. Repeated calls are harmless.

    VoiceConnection::latency_ms

    fn VoiceConnection::latency_ms(self : VoiceConnection) -> Int64?

    Latest voice gateway heartbeat round-trip in milliseconds, if measured.

    VoiceConnection::next_event

    async fn VoiceConnection::next_event(self : VoiceConnection) -> VoiceEvent

    Pull the next membership or lifecycle event. At most event_backlog unread events are kept; older ones are discarded first. Received audio is read through subscribe.

    VoiceConnection::play

    async fn VoiceConnection::play(self : VoiceConnection, source : &AudioSource) -> Unit

    Replace the current source. The old sender is asked to stop and flush its five silence frames before the latest source begins.

    VoiceConnection::set_speaking

    async fn VoiceConnection::set_speaking(self : VoiceConnection, flags : Int) -> Unit

    Send a Speaking (opcode 5) frame with the given flag bits. The playback loop manages this automatically; call it directly only for custom speaking indicators.

    VoiceConnection::start

    fn[X] VoiceConnection::start(group :
    TaskGroup
    [X], credentials : VoiceCredentials, user_id~ : String, channel_id~ : UInt64, leave~ : async () -> Unit, rejoin~ : async () -> VoiceCredentials, connector? : async (String) -> &VoiceTransport, udp_opener? : async (String, Int) -> &VoiceUdp, receive? : Bool, event_backlog? : Int, telemetry? : (VoiceTelemetry) -> Unit, sleeper? : async (Int) -> Unit, rand? :
    Rand
    ) -> VoiceConnection raise VoiceError

    Start a voice connection and attach all of its tasks to group.

    VoiceConnection::state

    The current lifecycle state of this voice connection.

    VoiceConnection::stop

    async fn VoiceConnection::stop(self : VoiceConnection) -> Unit

    Stop the active source; the send loop emits five silence frames and clears the speaking flag.

    VoiceConnection::subscribe

    fn VoiceConnection::subscribe(self : VoiceConnection, user_id~ : String, end_after_silence_ms? : Int?, backlog? : Int) -> VoiceReceiveStream raise VoiceError

    Subscribe to received audio from one Discord user. Only subscribed users' packets are decrypted; everything else is discarded on arrival.

    With end_after_silence_ms, the first read waits indefinitely and later reads end the stream after the requested silence interval. At most backlog items wait unread; newer frames are dropped past that and reported as ReceivedAudio::Lost (and VoiceTelemetry::ReceiveDropped). Raises InvalidBacklog when backlog is below 2: after a drop, the next delivery needs one slot for the Lost marker and one for its frame, so a single slot would never let the stream recover.

    Example

    async test {
    // Echo one speaker until they are silent for one second.
    connection.play(
    connection.subscribe(user_id="1234", end_after_silence_ms=Some(1000)),
    )

    // Or record the same stream into an Ogg/Opus file.
    let stream = connection.subscribe(user_id="1234")
    let audio : &@voice.AudioSource = stream
    let writer = @voice.OggOpusWriter::new(channels=2)
    while audio.next_frame() is Some(frame) {
    writer.write_frame(frame)
    }
    writer.finish()
    }

    VoiceConnection::wait_ready

    async fn VoiceConnection::wait_ready(self : VoiceConnection, timeout_ms? : Int) -> Unit raise VoiceError

    Wait until the current connection handshake reaches Ready.

    VoiceConnectionState

    pub(all) enum VoiceConnectionState {
    Connecting
    Ready
    Reconnecting
    Rejoining
    Closed(code~ : Int?)
    } derive(Eq,
    Debug
    )

    Public voice connection lifecycle.

    VoiceCredentials

    pub(all) struct VoiceCredentials {
    server_id : String
    session_id : String
    token : String
    endpoint : String
    } derive(Eq,
    Debug
    )

    Credentials supplied by the main Discord gateway voice-state handshake.

    VoiceEvent

    pub(all) enum VoiceEvent {
    SpeakingChanged(user_id~ : String, ssrc~ : UInt, flags~ : Int)
    UserConnected(user_id~ : String)
    UserDisconnected(user_id~ : String)
    ConnectionReady
    ConnectionResumed
    } derive(Eq,
    Debug
    )

    Membership and lifecycle events of a live voice connection. Received audio is not an event: it is delivered per user through VoiceConnection::subscribe.

    VoiceFrame

    pub(all) enum VoiceFrame {
    Text(String)
    Binary(Bytes)
    } derive(Eq,
    Debug
    )

    A complete WebSocket message received from the voice gateway.

    VoiceGateway

    pub struct VoiceGateway {
    // private fields
    }

    Voice gateway v8 driver. The caller-owned task group controls its lifetime; cancelling the group tears down connection and heartbeat tasks.

    VoiceGateway::close

    async fn VoiceGateway::close(self : VoiceGateway) -> Unit noraise

    Request graceful shutdown of the voice WebSocket.

    VoiceGateway::latency_ms

    fn VoiceGateway::latency_ms(self : VoiceGateway) -> Int64?

    Latest matched heartbeat round-trip time in milliseconds.

    VoiceGateway::next

    async fn VoiceGateway::next(self : VoiceGateway) -> VoiceGatewayEvent

    Pull the next voice gateway event. Blocks until one is available.

    VoiceGateway::send_binary

    async fn VoiceGateway::send_binary(self : VoiceGateway, op~ : Int, payload : Bytes) -> Unit

    Send a client-to-server DAVE binary envelope.

    VoiceGateway::send_json

    async fn VoiceGateway::send_json(self : VoiceGateway, payload : Json) -> Unit

    Send an arbitrary JSON voice gateway envelope.

    VoiceGateway::start

    fn[X] VoiceGateway::start(group :
    TaskGroup
    [X], server_id~ : String, user_id~ : String, session_id~ : String, token~ : String, endpoint~ : String, max_dave_protocol_version? : UInt16, select_protocol~ : async (UInt, String, Int, Array[String]) -> (String, Int, String), connector? : async (String) -> &VoiceTransport, queue_capacity? : Int, telemetry? : (VoiceGatewayEvent) -> Unit, sleeper? : async (Int) -> Unit, rand? :
    Rand
    ) -> VoiceGateway

    Spawn a voice gateway driver into group and return its handle. max_dave_protocol_version is advertised in Identify. It defaults to zero so low-level callers do not negotiate DAVE without an active backend; pass a supported maximum explicitly to enable it.

    VoiceGateway::state

    Current voice gateway lifecycle state.

    VoiceGatewayEvent

    pub(all) enum VoiceGatewayEvent {
    ReadyReceived
    SessionEstablished(mode~ : String, secret_key~ : Bytes, dave_protocol_version~ : Int)
    Message(VoiceMessage)
    Connected(resumed~ : Bool)
    Disconnected(code~ : Int?, resuming~ : Bool)
    ConnectFailed(reason~ : String)
    NeedsRejoin(code~ : Int?)
    FatallyClosed(code~ : Int)
    } derive(Eq,
    Debug
    )

    Events surfaced by VoiceGateway::next.

    VoiceGatewayPort

    What joining voice needs from the main gateway, as values and functions. from_gateway builds one from a bot's GatewayCtx; a test can build its own to drive VoiceClient::join_via without a gateway connection.

    VoiceGatewayPort::from_gateway

    The gateway capabilities of ctx's bot. Opcode 4 goes to the shard that owns each guild, whichever shard ctx belongs to.

    VoiceGatewayState

    pub(all) enum VoiceGatewayState {
    Disconnected(reconnect_attempts~ : Int)
    Connecting
    Identifying
    SelectingProtocol
    Active
    Resuming
    FatallyClosed(code~ : Int)
    } derive(Eq,
    Debug
    )

    Voice gateway lifecycle state.

    VoiceMessage

    pub(all) enum VoiceMessage {
    Ready(ssrc~ : UInt, ip~ : String, port~ : Int, modes~ : Array[String])
    Hello(heartbeat_interval~ : Int)
    SessionDescription(mode~ : String, secret_key~ : Bytes, dave_protocol_version~ : Int)
    HeartbeatAck(nonce~ : Int64)
    Speaking(ssrc~ : UInt, user_id~ : String, flags~ : Int)
    ClientsConnect(user_ids~ : Array[String])
    ClientDisconnect(user_id~ : String)
    Resumed
    DavePrepareTransition(transition_id~ : Int, protocol_version~ : Int)
    DaveExecuteTransition(transition_id~ : Int)
    DavePrepareEpoch(epoch~ : Int, protocol_version~ : Int)
    DaveMlsExternalSender(payload~ : Bytes)
    DaveMlsProposals(payload~ : Bytes)
    DaveMlsAnnounceCommitTransition(transition_id~ : Int, commit~ : Bytes)
    DaveMlsWelcome(transition_id~ : Int, welcome~ : Bytes)
    Unknown(op~ : Int)
    } derive(Eq,
    Debug
    )

    A decoded voice gateway v8 message.

    VoiceReceiveStream

    pub struct VoiceReceiveStream {
    // private fields
    }

    A per-user stream of received audio. Streams are created with VoiceConnection::subscribe; read them with next_packet, or as an AudioSource (for play or recording) when only the Opus frames matter.

    VoiceReceiveStream::close

    fn VoiceReceiveStream::close(self : VoiceReceiveStream) -> Unit

    End this stream and unregister it from its connection. Repeated calls are harmless, and a blocked read wakes with None.

    VoiceReceiveStream::next_frame

    async fn VoiceReceiveStream::next_frame(self : VoiceReceiveStream) -> Bytes?

    VoiceReceiveStream::next_packet

    async fn VoiceReceiveStream::next_packet(self : VoiceReceiveStream) -> ReceivedAudio?

    Yield the subscribed user's next frame or loss marker; None after close, once the connection ends, or once the configured trailing silence elapses.

    VoiceReconnectPolicy

    pub(all) enum VoiceReconnectPolicy {
    Resume
    Rejoin
    Fatal
    } derive(Eq,
    Debug
    )

    Action after a voice WebSocket close: resume the session, rejoin from scratch, or give up because reconnecting cannot succeed.

    VoiceTelemetry

    pub(all) enum VoiceTelemetry {
    GatewayEvent(VoiceGatewayEvent)
    DaveMediaContextActivated(protocol_version~ : Int)
    DaveBinaryControlSent(opcode~ : Int, payload_bytes~ : Int)
    PacketDropped(reason~ : String)
    DaveEncryptDropped(reason~ : String)
    DaveDecryptDropped(user_id~ : String?, ssrc~ : UInt, count~ : Int, reason~ : String)
    EventDropped(backlog~ : Int)
    ReceiveDropped(user_id~ : String, backlog~ : Int)
    } derive(Eq,
    Debug
    )

    Non-fatal diagnostics from the connection's gateway and media paths.

    DEFAULT_RECEIVE_BACKLOG

    let DEFAULT_RECEIVE_BACKLOG : Int

    Default number of unread items a receive stream holds: five seconds of 20 ms Opus frames.

    DEFAULT_VOICE_EVENT_BACKLOG

    let DEFAULT_VOICE_EVENT_BACKLOG : Int

    Default number of unread VoiceEvents a connection keeps for next_event.

    SILENCE_FRAME

    let SILENCE_FRAME : Bytes

    Discord's canonical Opus silence frame.

    build_ip_discovery_request

    fn build_ip_discovery_request(ssrc : UInt) -> Bytes

    Build Discord's 74-byte voice IP discovery request.

    build_rtp_header

    fn build_rtp_header(sequence~ : UInt16, timestamp~ : UInt, ssrc~ : UInt) -> Bytes

    Build Discord's RTP header for a 20 ms Opus frame.

    connect_voice_websocket

    async fn connect_voice_websocket(url : String) -> &VoiceTransport

    Connect to a voice-gateway WebSocket. The caller supplies the complete v8 URL, normally wss://<endpoint>?v=8.

    discover_external_address

    async fn discover_external_address(udp : &VoiceUdp, ssrc : UInt, retries? : Int, timeout_ms? : Int) -> (String, Int)

    Run voice IP discovery, retrying only receive timeouts.

    encode_binary_client_frame

    fn encode_binary_client_frame(op : Int, payload : Bytes) -> Bytes

    Prefix a client-to-server binary DAVE payload with its one-byte opcode.

    encode_heartbeat

    fn encode_heartbeat(t~ : Int64, seq_ack~ : Int) -> Json

    Voice gateway Heartbeat (opcode 3) payload carrying the nonce and last acknowledged server sequence.

    encode_identify

    fn encode_identify(server_id~ : String, user_id~ : String, session_id~ : String, token~ : String, max_dave_protocol_version? : UInt16) -> Json

    Voice gateway Identify (opcode 0) payload, advertising the selected maximum DAVE protocol version. It defaults to zero so low-level callers do not advertise DAVE without an active backend; pass a supported maximum explicitly to enable negotiation.

    encode_invalid_commit_welcome

    fn encode_invalid_commit_welcome(transition_id~ : Int) -> Json

    DAVE Invalid Commit/Welcome (opcode 31) payload asking the server to reset the group after a rejected MLS message.

    encode_resume

    fn encode_resume(server_id~ : String, session_id~ : String, token~ : String, seq_ack~ : Int) -> Json

    Voice gateway Resume (opcode 7) payload for reattaching to an existing session.

    encode_select_protocol

    fn encode_select_protocol(address~ : String, port~ : Int, mode~ : String) -> Json

    Voice gateway Select Protocol (opcode 1) payload announcing the discovered UDP address and chosen encryption mode.

    encode_speaking

    fn encode_speaking(ssrc~ : UInt, flags~ : Int, delay? : Int) -> Json

    Voice gateway Speaking (opcode 5) payload for the sender's SSRC.

    encode_transition_ready

    fn encode_transition_ready(transition_id~ : Int) -> Json

    DAVE Transition Ready (opcode 23) payload acknowledging an announced protocol transition.

    open_voice_udp

    async fn open_voice_udp(ip : String, port : Int) -> &VoiceUdp

    Open a connected UDP socket for a Discord voice server.

    parse_binary_frame

    fn parse_binary_frame(bytes : Bytes) -> (VoiceMessage, Int?) raise VoiceEnvelopeError

    Decode a server-to-client binary DAVE frame. Its first two bytes are the big-endian v8 sequence number and the third byte is the opcode.

    parse_ip_discovery_response

    fn parse_ip_discovery_response(bytes : Bytes, expected_ssrc~ : UInt) -> (String, Int) raise VoiceUdpError

    Parse Discord's 74-byte voice IP discovery response.

    parse_rtp_packet

    fn parse_rtp_packet(bytes : Bytes) -> RtpPacket raise RtpError

    Parse an RTP v2 packet and retain extension data in payload.

    parse_text_frame

    fn parse_text_frame(text : String) -> (VoiceMessage, Int?) raise VoiceEnvelopeError

    Decode a JSON voice gateway message and return its optional top-level buffered-resume sequence number.

    rtpsize_header_len

    fn rtpsize_header_len(bytes : Bytes) -> Int

    Return the cleartext RTP header length used as AEAD associated data.

    run_send_loop

    async fn run_send_loop(source : &AudioSource, cipher : TransportCipher, udp : &VoiceUdp, gateway : VoiceGateway, ssrc~ : UInt, initial_sequence? : UInt16, initial_timestamp? : UInt, stop? :
    Ref
    [Bool], frame_duration_ms? : Int, dave? : DaveMachine, on_dave_error? : (String) -> Unit) -> Unit

    Internal paced sender used by the M4 connection driver.

    shim_available

    fn shim_available() -> Bool

    Whether a compatible transport-only ABI v3 voice shim is available.

    shim_unavailable_reason

    fn shim_unavailable_reason() -> String?

    Explain why the native voice shim could not be loaded.

    voice_on_close

    fn voice_on_close(code : Int?, has_session : Bool) -> VoiceReconnectPolicy

    Classify a voice WebSocket close code. A resumable transport failure still requires a fresh join when no established voice session exists.