Experimental Discord voice for gaato/discord bots on MoonBit's native backend: voice gateway v8, Opus playback and receive, DAVE E2EE.
moon add gaato/discord_voiceimport {
"gaato/discord",
"gaato/discord_voice" @voice,
}let voice = @voice.VoiceClient()
voice.attach(bot)
// later, with a GatewayCtx:
let connection = voice.join(ctx, guild_id, channel_id)env DISCORD_VOICE_REQUIRE_SHIM=1 MBT_DAVE_REQUIRE_NATIVE=1 moon build --target native --releasepub(open) trait AudioSource {
async fn next_frame(Self) -> Bytes?
}trait DaveBackendpub(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
}pub(open) trait VoiceUdp {
async fn recv(Self, FixedArray[Byte]) -> Int
async fn send(Self, Bytes) -> Unit
async fn close(Self) -> Unit
}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)pub(all) suberror VoiceTransportClosed {
VoiceTransportClosed(code~ : Int?, reason~ : String)
} derive(Debug)pub struct DaveMachine {
// private fields
}fn DaveMachine::encrypt_opus_frame(self : DaveMachine, ssrc~ : UInt, frame : Bytes) -> Bytes raise DaveErrorfn DaveMachine::handle(self : DaveMachine, message : VoiceMessage) -> Array[DaveAction] raise DaveErrorfn DaveMachine::new(protocol_version~ : Int, self_user_id~ : String, channel_id~ : UInt64, roster? : Array[String]) -> DaveMachine raise DaveErrorpub struct OggOpusSource {
// private fields
}impl AudioSource for OggOpusSourceasync 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)
}
}pub struct OggOpusWriter {
// private fields
}fn OggOpusWriter::new(channels? : Int, pre_skip? : Int, serial? : UInt, max_packets_per_page? : Int) -> OggOpusWriterasync 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())
}fn OggOpusWriter::write_frame(self : OggOpusWriter, opus : Bytes, samples? : Int) -> Unit raise OggOpusErrorpub struct OggPacketReader {
// private fields
}pub(all) struct PendingVoiceEvent[T] {
wait : async (Int?) -> T?
cancel : () -> Unit
}pub struct ReorderBuffer[T] {
// private fields
}fn[T] ReorderBuffer::push(self : ReorderBuffer[T], seq~ : Int, arrival_ms~ : Int64, packet : T) -> Array[ReorderOutput[T]]pub struct TransportCipher {
// private fields
}fn TransportCipher::new(mode : EncryptionMode, secret_key : Bytes) -> TransportCipher raise VoiceCryptoErrorfn TransportCipher::open(self : TransportCipher, packet : Bytes, header_len~ : Int) -> Bytes raise VoiceCryptoErrorfn TransportCipher::seal(self : TransportCipher, header~ : Bytes, plaintext : Bytes) -> Bytes raise VoiceCryptoErrorpub struct VoiceClient {
// private fields
}async fn VoiceClient::join(self : VoiceClient, ctx : GatewayCtx, guild_id : Id[GuildMarker], channel_id : Id[ChannelMarker], self_mute? : Bool, self_deaf? : Bool, timeout_ms? : Int, telemetry? : (VoiceTelemetry) -> Unit) -> VoiceConnectionasync 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) -> VoiceConnectionpub struct VoiceConnection {
// private fields
}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 VoiceErrorfn VoiceConnection::subscribe(self : VoiceConnection, user_id~ : String, end_after_silence_ms? : Int?, backlog? : Int) -> VoiceReceiveStream raise VoiceErrorasync 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()
}async fn VoiceConnection::wait_ready(self : VoiceConnection, timeout_ms? : Int) -> Unit raise VoiceErrorpub struct VoiceGateway {
// private fields
}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) -> VoiceGatewaypub(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)pub(all) struct VoiceGatewayPort {
user_id : Id[UserMarker]
intents : Intents
expect_voice_state : ((VoiceState) -> Bool) -> PendingVoiceEvent[VoiceState]
expect_voice_server : ((VoiceServerUpdateEvent) -> Bool) -> PendingVoiceEvent[VoiceServerUpdateEvent]
update_voice_state : async (UpdateVoiceState) -> Unit
}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)pub struct VoiceReceiveStream {
// private fields
}impl AudioSource for VoiceReceiveStreampub(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)let DEFAULT_RECEIVE_BACKLOG : Intlet DEFAULT_VOICE_EVENT_BACKLOG : Intfn build_ip_discovery_request(ssrc : UInt) -> Bytesfn build_rtp_header(sequence~ : UInt16, timestamp~ : UInt, ssrc~ : UInt) -> Bytesasync fn discover_external_address(udp : &VoiceUdp, ssrc : UInt, retries? : Int, timeout_ms? : Int) -> (String, Int)fn encode_binary_client_frame(op : Int, payload : Bytes) -> Bytesfn encode_identify(server_id~ : String, user_id~ : String, session_id~ : String, token~ : String, max_dave_protocol_version? : UInt16) -> Jsonfn encode_resume(server_id~ : String, session_id~ : String, token~ : String, seq_ack~ : Int) -> Jsonfn parse_ip_discovery_response(bytes : Bytes, expected_ssrc~ : UInt) -> (String, Int) raise VoiceUdpErrorfn rtpsize_header_len(bytes : Bytes) -> Intasync 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) -> Unitfn shim_available() -> Boolfn shim_unavailable_reason() -> String?Install
Download zipExperimental Discord voice for gaato/discord bots on MoonBit's native backend: voice gateway v8, Opus playback and receive, DAVE E2EE.