Skip to content
Tanoshii edited this page Jul 8, 2024 · 10 revisions

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.

Acquiring Audio Binaries

libopus and libsodium

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.

FFmpeg

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.

Loading Audio Binaries

libopus and libsodium

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.

FFmpeg

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.

Connecting to Voice Channels

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.

Streaming Audio

If it is valid, then you can begin to stream audio over the connection.

Audio Files

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)

Raw PCM

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)

Managing Streams

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.

Clone this wiki locally