-
Notifications
You must be signed in to change notification settings - Fork 146
Voice
As of version 2.4.0, Discordia supports a subset of Discord's voice features. This page explains how to set up your application to take advantage of these features.
Discord voice uses Opus-encoded, Sodium-encrypted audio data. If you plan to use voice in your Discordia application, you will need to include libopus and libsodium dynamic libraries. Additionally, you will probably want FFmpeg if you plan to stream a variety of audio formats.
- Opus is an open source audio codec by Xiph.Org. It is useful for a wide range of bitrates (6 to 510 kb/s) and sample rates (8 to 48 kHz).
- Sodium is a library used for encryption and other security applications.
- FFmpeg is a software suite that contains a variety of tools for multimedia format conversions and streaming.
Windows users will need .dll
(dynamic-link library) binaries. For convenience, Windows binaries are provided here in the Discordia GitHub repository. Alternatively, you can try to find pre-built binaries for Opus or Sodium, or you can build your own from source.
Linux users will need .so
(shared object) binaries. If you are capable, you can build your own from source, or use a package manager to install the dev versions. For example, Ubuntu has both libopus-dev and libsodium-dev.
Be sure to match your system architecture. If you are not sure of which to use, check jit.arch
in Luvit. This will generally be x86
or x64
.
Discordia targets libopus version 1.2 and sodium version 1.0.
Discordia uses an FFmpeg executable to stream audio files. To obtain FFmpeg, download a static build for your operating system and architecture. The Windows version will be ffmpeg.exe
, while the Linux version will be simply ffmpeg
.
Discordia loads dynamic libraries using LuaJIT's ffi.load
function, documented here. On Windows, you must rename your libopus file to opus.dll
and your libsodium file to sodium.dll
. They must both be placed in a proper directory. Use your current working directory if you are unsure of which to use.
The FFmpeg executable does not need to be "loaded", but it must be in either your current working directory or it must be in a valid PATH directory.
After you run your bot, you can attempt to connect to voice channels. This is done by calling join
on a GuildVoiceChannel:
client:on('ready', function()
local channel = client:getChannel('123456')
local connection = channel:join()
connection:close()
end)
If a voice channel connection is made successfully, a VoiceConnection object is returned. The connection object also exists at GuildVoiceChannel.connection
and Guild.connection
.
If the account does not have permission to join, or if there is a connection issue, the request will timeout and no object will be returned. You should, therefore, check that the connection is not nil
before using it.
Note that a bot can connect to only one voice channel per guild. If a bot is connected to a channel and joins a different channel in the same guild, the bot will move to the other channel and reuse the previous connection in that channel. If the second channel is in a different guild, an independent connection is created.
To disconnect, call the close
method on the connection object.
If it is valid, then you can begin to stream audio over the connection.
If you have FFmpeg installed, you can stream the any audio or video file that FFmpeg supports, where the argument to playFFmpeg
is the Ffmpeg's -i
input argument. For example:
client:on('ready', function()
local channel = client:getChannel('123456')
local connection = channel:join()
connection:playFFmpeg('music.mp3')
end)
File loading behavior is the same as above, where the filename can be used if it is in the current working directory; otherwise, the full path must be used. If you'd only like to play the file for a certain time, you can include a duration in milliseconds:
client:on('ready', function()
local channel = client:getChannel('123456')
local connection = channel:join()
connection:playFFmpeg('music.mp3', 3000)
end)
Note: Audio streaming method are locally blocking, and follow the same behavior that any other locally blocking Discordia function has. If you'd like to do other things while the audio is streaming, you can wrap the method call in a coroutine:
client:on('ready', function()
local channel = client:getChannel('123456')
local connection = channel:join()
coroutine.wrap(function()
connection:playFFmpeg('music.mp3')
print('done streaming!')
end)()
print('starting audio stream')
end)
Using the playPCM
method, you can stream raw PCM data. The first argument must be a Lua string or a Lua function.
If the source is a Lua string, the string is treated as a 1-indexed, little-endian, byte array interpreted as 48 kHz, interleaved 2-channel, 16-bit PCM. Thus, if you have a WAVE that is already in this format, you can do the following:
client:on('ready', function()
local channel = client:getChannel('123456')
local connection = channel:join()
local file = io.open('music.wav', 'rb')
file:seek('set', 44) -- skip the header
connection:playPCM(file:read('*all'))
end)
If the source is a Lua function, it is interpreted as a generator function that, when called, returns a left and right 16-bit PCM sample. If only one value is returned, the sample sample will be streamed on both channels. If no value is returned, a signal of 0 will be streamed. on both channels.
local rate = 48000
local maxint16 = 32767
local function sine(freq, amp)
local h = freq * 2 * math.pi / rate
local a = amp * maxint16
local t = 0
return function()
local s = math.sin(t) * a
t = t + h
return s, s -- left, right
end
end
client:on('ready', function()
local channel = client:getChannel('123456')
local connection = channel:join()
local generator = sine(440, 1) -- freq in Hz, amplitude 0 to 1
connection:playPCM(generator, 3000) -- duration in ms
end)
Streams can be arbitrarily paused, resumed, and stopped.
client:on('messageCreate', function(message)
local content = message.content
local connection = message.guild.connection
if content == '!pause' then
connection:pauseStream()
elseif content == '!resume' then
connection:resumeStream()
elseif content == '!stop' then
connection:stopStream()
end
end)
These methods are idempotent, which means, for example calling pauseStream
on a stream that is already paused will have no effect.