Experimental Discord voice for gaato/discord bots on MoonBit's native backend: voice gateway v8, Opus playback and receive, DAVE E2EE.
Dependencies
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 VoiceTransportClosed {
VoiceTransportClosed(code~ : Int?, reason~ : String)
} derive(Debug)pub 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(all) struct PendingVoiceEvent[T] {
wait : async (Int?) -> T?
cancel : () -> Unit
}pub 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, next_server? : async () -> VoiceServer?, next_channel? : async () -> UInt64?, server_timeout_ms? : Int, connector? : async (String) -> &VoiceTransport, udp_opener? : async (String, Int) -> &VoiceUdp, receive? : Bool, event_backlog? : Int, telemetry? : (VoiceTelemetry) -> Unit, clock? : &Clock, 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(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 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.
Dependencies