unbot/lib/discordcr/examples/voice_send.cr
2026-04-19 17:27:33 +02:00

160 lines
5.7 KiB
Crystal
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# This is a simple music bot that can connect to a voice channel and play back
# some music in DCA format. It demonstrates how to use VoiceClient and
# DCAParser.
#
# For more information on the DCA file format, see
# https://github.com/bwmarrin/dca.
require "../src/discordcr"
# Make sure to replace this fake data with actual data when running.
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)
cache = Discord::Cache.new(client)
client.cache = cache
# ID of the current user, required to create a voice client
current_user_id = nil
# The ID of the (text) channel in which the connect command was run, so the
# "Voice connected." message is sent to the correct channel
connect_channel_id = nil
# Where the created voice client will eventually be stored
voice_client = nil
client.on_ready do |payload|
current_user_id = payload.user.id
end
client.on_message_create do |payload|
if payload.content.starts_with? "!connect "
# Used as:
# !connect <guild ID> <channel ID>
# Parse the command arguments
ids = payload.content[9..-1].split(' ').map(&.to_u64)
client.create_message(payload.channel_id, "Connecting...")
connect_channel_id = payload.channel_id
client.voice_state_update(ids[0].to_u64, ids[1].to_u64, false, false)
elsif payload.content.starts_with? "!vs"
# Used as:
# !vs
reply = begin
vs = cache.resolve_voice_state(payload.guild_id.not_nil!, payload.author.id)
vc = cache.resolve_channel(vs.channel_id.not_nil!)
# The voice region will be nil if the channel is set to automatically determine it.
rtc_region = vc.rtc_region || "Automatic"
"You are connected to channel #{vs.channel_id} (region: #{rtc_region}) at guild #{vs.guild_id}"
rescue
"No voice state"
end
client.create_message(payload.channel_id, reply)
elsif payload.content.starts_with? "!vr"
# Used as:
# !vr [guild ID]
regions : Array(Discord::VoiceRegion) = if payload.content.size > 4
id = payload.content[4..-1]
client.get_guild_voice_regions(id.to_u64)
else
client.list_voice_regions
end
client.create_message(payload.channel_id, "Voice Regions: #{regions.map(&.name).join(", ")}")
elsif payload.content.starts_with? "!play_dca "
# Used as:
# !play_dca <filename>
#
# Make sure the DCA file you play back is valid according to the spec
# (including metadata), otherwise playback will fail.
unless voice_client
client.create_message(payload.channel_id, "Voice client is nil!")
next
end
filename = payload.content[10..-1]
file = File.open(filename)
# The DCAParser class handles parsing of the DCA file. It doesn't do any
# sending of audio data to Discord itself that has to be done by
# VoiceClient.
parser = Discord::DCAParser.new(file)
# A proper DCA(1) file contains metadata, which is exposed by DCAParser.
# This metadata may be of interest, so here is some example code that uses
# it.
if metadata = parser.metadata
tool = metadata.dca.tool
client.create_message(payload.channel_id, "DCA file was created by #{tool.name}, version #{tool.version}.")
if info = metadata.info
client.create_message(payload.channel_id, "Song info: #{info.title} by #{info.artist}.") if info.title && info.artist
end
else
client.create_message(payload.channel_id, "DCA file metadata is invalid!")
end
# Set the bot as speaking (green circle). This is important and has to be
# done at least once in every voice connection, otherwise the Discord client
# will not know who the packets we're sending belongs to.
voice_client.not_nil!.send_speaking(true)
client.create_message(payload.channel_id, "Playing DCA file `#{filename}`.")
# For smooth audio streams Discord requires one packet every
# 20 milliseconds. The `every` method measures the time it takes to run the
# block and then sleeps 20 milliseconds minus that time before moving on to
# the next iteration, ensuring accurate timing.
#
# When simply reading from DCA, the time it takes to read, process and
# send the frame is small enough that `every` doesn't make much of a
# difference (in fact, some users report that it actually makes things
# worse). If the processing time is not negligibly slow because you're
# doing something else than DCA parsing, or because you're reading from a
# slow source, or for any other reason, then it is recommended to use
# `every`. Otherwise, simply using a loop and `sleep`ing `20.milliseconds`
# each time may suffice.
Discord.every(20.milliseconds) do
frame = parser.next_frame(reuse_buffer: true)
break unless frame
# Perform the actual sending of the frame to Discord.
voice_client.not_nil!.play_opus(frame)
end
# Alternatively, the above code can be realised as the following:
#
# parser.parse do |frame|
# Discord.timed_run(20.milliseconds) do
# voice_client.not_nil!.play_opus(frame)
# end
# end
#
# (The `parse` method reads the frames consecutively and passes them to the
# block.)
file.close
end
end
# The VOICE_SERVER_UPDATE dispatch is sent by Discord once the op4 packet sent
# by voice_state_update has been processed. It tells the client the endpoint
# to connect to.
client.on_voice_server_update do |payload|
begin
vc = voice_client = Discord::VoiceClient.new(payload, client.session.not_nil!, current_user_id.not_nil!)
vc.on_ready do
client.create_message(connect_channel_id.not_nil!, "Voice connected.")
end
vc.run
rescue e
e.inspect_with_backtrace(STDOUT)
end
end
client.run