Documentation Index
Fetch the complete documentation index at: https://mintlify.com/discord-jda/JDA/llms.txt
Use this file to discover all available pages before exploring further.
This example demonstrates how to connect to voice channels and handle audio streams. The bot will echo back any audio it receives, creating a real-time audio feedback loop.
Overview
The Audio Echo bot will:
- Connect to voice channels via text commands
- Receive audio from users in the voice channel
- Echo the combined audio back to all users
- Handle audio in PCM format at 20ms intervals
Audio features require additional dependencies for the DAVE (Discord Audio & Video End-to-End Encryption) protocol. See the setup section for details.
Required Dependencies
Add DAVE Session Factory
JDA requires a DAVE session factory implementation for voice encryption:<dependency>
<groupId>club.minnced</groupId>
<artifactId>jdave</artifactId>
<version>VERSION</version>
</dependency>
See jdave on GitHub for details. Optional: UDP Queue (Recommended)
To avoid audio stutter caused by JVM garbage collection pauses:<dependency>
<groupId>club.minnced</groupId>
<artifactId>udpqueue</artifactId>
<version>VERSION</version>
</dependency>
See udpqueue.rs on GitHub for details.
Audio Module Configuration
Configure JDA with the required audio modules:
import club.minnced.discord.jdave.interop.JDaveSessionFactory;
import net.dv8tion.jda.api.audio.AudioModuleConfig;
import net.dv8tion.jda.api.audio.dave.DaveSessionFactory;
// [REQUIRED] DAVE protocol implementation
DaveSessionFactory daveSessionFactory = new JDaveSessionFactory();
// [OPTIONAL] Native audio send factory to reduce stutter
// AudioSendFactory audioSendFactory = new NativeAudioSendFactory();
AudioModuleConfig audioModuleConfig = new AudioModuleConfig()
// .withAudioSendFactory(audioSendFactory) // Optional
.withDaveSessionFactory(daveSessionFactory);
Gateway Intents for Audio
Enable the necessary intents:
EnumSet<GatewayIntent> intents = EnumSet.of(
// We need messages in guilds to accept commands from users
GatewayIntent.GUILD_MESSAGES,
// We need voice states to connect to the voice channel
GatewayIntent.GUILD_VOICE_STATES,
// Enable access to message.getContentRaw()
GatewayIntent.MESSAGE_CONTENT
);
Building the JDA Instance
JDABuilder.createDefault(token, intents)
.addEventListeners(new AudioEchoExample())
.setActivity(Activity.listening("to jams"))
.setStatus(OnlineStatus.DO_NOT_DISTURB)
// Enable the VOICE_STATE cache to find a user's connected voice channel
.enableCache(CacheFlag.VOICE_STATE)
// Configure the JDA audio module
.setAudioModuleConfig(audioModuleConfig)
.build();
Handling Voice Commands
Implement message commands to join voice channels:
@Override
public void onMessageReceived(MessageReceivedEvent event) {
Message message = event.getMessage();
User author = message.getAuthor();
String content = message.getContentRaw();
Guild guild = event.getGuild();
// Ignore messages from bots
if (author.isBot()) {
return;
}
// Only handle guild messages
if (!event.isFromGuild()) {
return;
}
if (content.startsWith("!echo ")) {
String arg = content.substring("!echo ".length());
onEchoCommand(event, guild, arg);
} else if (content.equals("!echo")) {
onEchoCommand(event);
}
}
Join User’s Current Channel
private void onEchoCommand(MessageReceivedEvent event) {
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
AudioChannel channel = voiceState.getChannel();
if (channel != null) {
connectTo(channel);
onConnecting(channel, event.getChannel());
} else {
onUnknownChannel(event.getChannel(), "your voice channel");
}
}
Join Specified Channel
private void onEchoCommand(MessageReceivedEvent event, Guild guild, String arg) {
boolean isNumber = arg.matches("\\d+");
VoiceChannel channel = null;
// Try to find by ID
if (isNumber) {
channel = guild.getVoiceChannelById(arg);
}
// Try to find by name
if (channel == null) {
List<VoiceChannel> channels = guild.getVoiceChannelsByName(arg, true);
if (!channels.isEmpty()) {
channel = channels.get(0);
}
}
if (channel == null) {
onUnknownChannel(event.getChannel(), arg);
return;
}
connectTo(channel);
onConnecting(channel, event.getChannel());
}
Connecting to Voice Channels
private void connectTo(AudioChannel channel) {
Guild guild = channel.getGuild();
AudioManager audioManager = guild.getAudioManager();
EchoHandler handler = new EchoHandler();
// Set the send and receive handlers
audioManager.setSendingHandler(handler);
audioManager.setReceivingHandler(handler);
// Connect to the voice channel
audioManager.openAudioConnection(channel);
}
Echo Handler Implementation
The echo handler implements both AudioSendHandler and AudioReceiveHandler:
public static class EchoHandler implements AudioSendHandler, AudioReceiveHandler {
private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
/* Receive Handling */
@Override
public boolean canReceiveCombined() {
// Limit queue to 10 entries to prevent overflow
return queue.size() < 10;
}
@Override
public void handleCombinedAudio(CombinedAudio combinedAudio) {
// Only queue audio when users are actually speaking
if (combinedAudio.getUsers().isEmpty()) {
return;
}
byte[] data = combinedAudio.getAudioData(1.0f); // volume at 100%
queue.add(data);
}
/* Send Handling */
@Override
public boolean canProvide() {
return !queue.isEmpty();
}
@Override
public ByteBuffer provide20MsAudio() {
byte[] data = queue.poll();
return data == null ? null : ByteBuffer.wrap(data);
}
@Override
public boolean isOpus() {
// We're sending PCM audio, not Opus
return false;
}
}
Audio Processing Details
All methods in the echo handler are called by JDA threads when resources are available/ready for processing.
Receiving Audio
- The receiver gets 20ms chunks of PCM stereo audio
- You can receive audio even while deafened
- Combined audio merges all users into a single stream
- Volume can be adjusted (1.0 = 100%, 0.5 = 50%)
Sending Audio
- JDA requests 20ms chunks when ready
- Audio is automatically marked as “speaking” when provided
- PCM format is raw audio data (not compressed)
- Use
isOpus() to return true if sending Opus-encoded audio
Queue Management
private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
@Override
public boolean canReceiveCombined() {
// Prevent buffer overflow by limiting queue size
return queue.size() < 10;
}
Complete Example
import club.minnced.discord.jdave.interop.JDaveSessionFactory;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.audio.AudioModuleConfig;
import net.dv8tion.jda.api.audio.AudioReceiveHandler;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import net.dv8tion.jda.api.audio.CombinedAudio;
import net.dv8tion.jda.api.audio.dave.DaveSessionFactory;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class AudioEchoExample extends ListenerAdapter {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Unable to start without token!");
System.exit(1);
}
String token = args[0];
EnumSet<GatewayIntent> intents = EnumSet.of(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.GUILD_VOICE_STATES,
GatewayIntent.MESSAGE_CONTENT
);
DaveSessionFactory daveSessionFactory = new JDaveSessionFactory();
AudioModuleConfig audioModuleConfig = new AudioModuleConfig()
.withDaveSessionFactory(daveSessionFactory);
JDABuilder.createDefault(token, intents)
.addEventListeners(new AudioEchoExample())
.setActivity(Activity.listening("to jams"))
.setStatus(OnlineStatus.DO_NOT_DISTURB)
.enableCache(CacheFlag.VOICE_STATE)
.setAudioModuleConfig(audioModuleConfig)
.build();
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
Message message = event.getMessage();
User author = message.getAuthor();
String content = message.getContentRaw();
Guild guild = event.getGuild();
if (author.isBot() || !event.isFromGuild()) {
return;
}
if (content.startsWith("!echo ")) {
String arg = content.substring("!echo ".length());
onEchoCommand(event, guild, arg);
} else if (content.equals("!echo")) {
onEchoCommand(event);
}
}
private void onEchoCommand(MessageReceivedEvent event) {
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
AudioChannel channel = voiceState.getChannel();
if (channel != null) {
connectTo(channel);
event.getChannel().sendMessage("Connecting to " + channel.getName()).queue();
} else {
event.getChannel().sendMessage("Unable to connect to your voice channel!").queue();
}
}
private void onEchoCommand(MessageReceivedEvent event, Guild guild, String arg) {
boolean isNumber = arg.matches("\\d+");
VoiceChannel channel = null;
if (isNumber) {
channel = guild.getVoiceChannelById(arg);
}
if (channel == null) {
List<VoiceChannel> channels = guild.getVoiceChannelsByName(arg, true);
if (!channels.isEmpty()) {
channel = channels.get(0);
}
}
if (channel == null) {
event.getChannel().sendMessage("Unable to connect to ``" + arg + "``, no such channel!").queue();
return;
}
connectTo(channel);
event.getChannel().sendMessage("Connecting to " + channel.getName()).queue();
}
private void connectTo(AudioChannel channel) {
Guild guild = channel.getGuild();
AudioManager audioManager = guild.getAudioManager();
EchoHandler handler = new EchoHandler();
audioManager.setSendingHandler(handler);
audioManager.setReceivingHandler(handler);
audioManager.openAudioConnection(channel);
}
public static class EchoHandler implements AudioSendHandler, AudioReceiveHandler {
private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
@Override
public boolean canReceiveCombined() {
return queue.size() < 10;
}
@Override
public void handleCombinedAudio(CombinedAudio combinedAudio) {
if (combinedAudio.getUsers().isEmpty()) {
return;
}
byte[] data = combinedAudio.getAudioData(1.0f);
queue.add(data);
}
@Override
public boolean canProvide() {
return !queue.isEmpty();
}
@Override
public ByteBuffer provide20MsAudio() {
byte[] data = queue.poll();
return data == null ? null : ByteBuffer.wrap(data);
}
@Override
public boolean isOpus() {
return false;
}
}
}
For production bots, consider implementing a disconnect command and automatic timeout to leave channels when inactive.