Prerequisites
Audio functionality requires additional dependencies beyond JDA core:
- DAVE Protocol Implementation (Required) - For Discord’s end-to-end encryption
- Audio Send Factory (Optional) - For better performance and reduced stuttering
Required Dependencies
Add the DAVE protocol implementation:dependencies {
implementation("net.dv8tion:JDA:5.2.1")
implementation("com.github.MinnDevelopment:jdave:1.0.0") // DAVE protocol
}
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
Optional: Better Performance
For production bots, add udpqueue to avoid audio stuttering from JVM GC pauses:implementation("club.minnced:udpqueue:1.0.0")
Required Gateway Intents
Voice connections require specific intents:import net.dv8tion.jda.api.requests.GatewayIntent;
import java.util.EnumSet;
EnumSet<GatewayIntent> intents = EnumSet.of(
GatewayIntent.GUILD_MESSAGES, // For accepting commands
GatewayIntent.GUILD_VOICE_STATES, // For voice state updates
GatewayIntent.MESSAGE_CONTENT // For message content access
);
VOICE_STATE cache flag:
import net.dv8tion.jda.api.utils.cache.CacheFlag;
JDABuilder.createDefault(token, intents)
.enableCache(CacheFlag.VOICE_STATE)
.build();
Configuring Audio Module
Before building JDA, configure the audio module:import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.audio.AudioModuleConfig;
import net.dv8tion.jda.api.audio.dave.DaveSessionFactory;
import club.minnced.discord.jdave.interop.JDaveSessionFactory;
// Required: DAVE protocol implementation
DaveSessionFactory daveSessionFactory = new JDaveSessionFactory();
// Optional: Better audio send performance
// AudioSendFactory audioSendFactory = new NativeAudioSendFactory();
AudioModuleConfig audioModuleConfig = new AudioModuleConfig()
.withDaveSessionFactory(daveSessionFactory);
// .withAudioSendFactory(audioSendFactory); // If using udpqueue
JDA jda = JDABuilder.createDefault(token, intents)
.setAudioModuleConfig(audioModuleConfig)
.enableCache(CacheFlag.VOICE_STATE)
.addEventListeners(new VoiceBot())
.build();
Connecting to Voice Channels
Get the AudioManager
Each guild has its own AudioManager:
Guild guild = event.getGuild();
AudioManager audioManager = guild.getAudioManager();
Set Audio Handlers
Configure sending and/or receiving handlers:
AudioSendHandler sendHandler = new MyAudioSendHandler();
AudioReceiveHandler receiveHandler = new MyAudioReceiveHandler();
audioManager.setSendingHandler(sendHandler);
audioManager.setReceivingHandler(receiveHandler);
Open the Connection
Connect to an audio channel:
AudioChannel channel = voiceChannel; // or StageChannel
audioManager.openAudioConnection(channel);
Complete Echo Bot Example
Based on AudioEchoExample.java, here’s a bot that echoes voice channel audio:import club.minnced.discord.jdave.interop.JDaveSessionFactory;
import net.dv8tion.jda.api.JDABuilder;
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.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
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.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class AudioEchoBot extends ListenerAdapter {
public static void main(String[] args) {
String token = System.getenv("BOT_TOKEN");
EnumSet<GatewayIntent> intents = EnumSet.of(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.GUILD_VOICE_STATES,
GatewayIntent.MESSAGE_CONTENT
);
AudioModuleConfig audioConfig = new AudioModuleConfig()
.withDaveSessionFactory(new JDaveSessionFactory());
JDABuilder.createDefault(token, intents)
.addEventListeners(new AudioEchoBot())
.enableCache(CacheFlag.VOICE_STATE)
.setAudioModuleConfig(audioConfig)
.build();
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (event.getAuthor().isBot() || !event.isFromGuild()) {
return;
}
String content = event.getMessage().getContentRaw();
if (content.equals("!echo")) {
Member member = event.getMember();
AudioChannel channel = member.getVoiceState().getChannel();
if (channel != null) {
connectTo(channel);
event.getChannel()
.sendMessage("Connecting to " + channel.getName())
.queue();
} else {
event.getChannel()
.sendMessage("You need to be in a voice channel!")
.queue();
}
}
}
private void connectTo(AudioChannel channel) {
AudioManager audioManager = channel.getGuild().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<>();
// Receiving audio
@Override
public boolean canReceiveCombined() {
return queue.size() < 10; // Limit queue size
}
@Override
public void handleCombinedAudio(CombinedAudio combinedAudio) {
// Only queue audio when users are speaking
if (combinedAudio.getUsers().isEmpty()) {
return;
}
byte[] data = combinedAudio.getAudioData(1.0f); // Volume 100%
queue.add(data);
}
// Sending audio
@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; // We're sending PCM audio
}
}
}
Audio Sending
ImplementAudioSendHandler to send audio to Discord:
import net.dv8tion.jda.api.audio.AudioSendHandler;
import java.nio.ByteBuffer;
public class MyAudioSendHandler implements AudioSendHandler {
private ByteBuffer buffer;
@Override
public boolean canProvide() {
// Return true when you have audio to send
return buffer != null && buffer.hasRemaining();
}
@Override
public ByteBuffer provide20MsAudio() {
// Return 20ms of PCM audio (stereo, 48kHz, 16-bit)
// That's 3840 bytes: 48000 Hz * 2 channels * 2 bytes * 0.02 seconds
ByteBuffer provided = buffer;
buffer = null;
return provided;
}
@Override
public boolean isOpus() {
// Return true if you're providing Opus-encoded audio
// Return false if you're providing PCM audio (most common)
return false;
}
// Your method to add audio data
public void provideAudio(ByteBuffer audio) {
this.buffer = audio;
}
}
PCM audio format: 48kHz sample rate, stereo (2 channels), 16-bit samples. Each 20ms frame is 3840 bytes.
Audio Receiving
ImplementAudioReceiveHandler to receive audio from Discord:
Receiving Combined Audio
Get all users mixed together:import net.dv8tion.jda.api.audio.AudioReceiveHandler;
import net.dv8tion.jda.api.audio.CombinedAudio;
public class MyAudioReceiveHandler implements AudioReceiveHandler {
@Override
public boolean canReceiveCombined() {
return true;
}
@Override
public void handleCombinedAudio(CombinedAudio combinedAudio) {
// Get the users currently speaking
List<User> users = combinedAudio.getUsers();
// Get the audio data at specified volume
byte[] audio = combinedAudio.getAudioData(1.0f); // 100% volume
// Process the audio...
}
}
Receiving Per-User Audio
Get audio separately for each speaking user:import net.dv8tion.jda.api.audio.UserAudio;
public class MyAudioReceiveHandler implements AudioReceiveHandler {
@Override
public boolean canReceiveUser() {
return true;
}
@Override
public void handleUserAudio(UserAudio userAudio) {
User user = userAudio.getUser();
byte[] audio = userAudio.getAudioData(1.0f);
System.out.println("Received audio from: " + user.getName());
// Process this user's audio...
}
}
Finding Voice Channels
Get User’s Current Channel
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
AudioChannel channel = voiceState.getChannel();
if (channel != null) {
System.out.println("User is in: " + channel.getName());
} else {
System.out.println("User is not in a voice channel");
}
Find Channel by Name or ID
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import java.util.List;
// By ID
VoiceChannel channel = guild.getVoiceChannelById("123456789");
// By name (exact match, case-insensitive)
List<VoiceChannel> channels = guild.getVoiceChannelsByName("General", true);
if (!channels.isEmpty()) {
VoiceChannel channel = channels.get(0);
}
AudioManager Configuration
AudioManager audioManager = guild.getAudioManager();
// Set to self-deafened
audioManager.setSelfDeafened(true);
// Set to self-muted
audioManager.setSelfMuted(true);
// Get connection status
boolean isConnected = audioManager.isConnected();
boolean isAttempting = audioManager.isAttemptingToConnect();
// Get the connected channel
AudioChannel connectedChannel = audioManager.getConnectedChannel();
// Close the connection
audioManager.closeAudioConnection();
Audio Threading Model
Important: All audio handler methods are called by JDA’s audio threads. Do not block these threads with long operations.
public class MyAudioSendHandler implements AudioSendHandler {
private final Queue<ByteBuffer> audioQueue = new ConcurrentLinkedQueue<>();
@Override
public boolean canProvide() {
// This is called frequently - keep it fast!
return !audioQueue.isEmpty();
}
@Override
public ByteBuffer provide20MsAudio() {
// This must return quickly - use pre-prepared buffers
return audioQueue.poll();
}
// Add audio from another thread
public void queueAudio(ByteBuffer buffer) {
audioQueue.offer(buffer);
}
}
Integration with Music Libraries
For playing music, use a library like Lavaplayer:import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
public class LavaplayerAudioSendHandler implements AudioSendHandler {
private final AudioPlayer audioPlayer;
private AudioFrame lastFrame;
public LavaplayerAudioSendHandler(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
}
@Override
public boolean canProvide() {
lastFrame = audioPlayer.provide();
return lastFrame != null;
}
@Override
public ByteBuffer provide20MsAudio() {
return ByteBuffer.wrap(lastFrame.getData());
}
@Override
public boolean isOpus() {
return true; // Lavaplayer provides Opus
}
}
Best Practices
Performance Tips:
- Use udpqueue for production bots to avoid GC-related audio stuttering
- Keep audio handler methods fast - use queues for buffering
- Limit receive queue sizes to prevent memory issues
- Use Opus encoding when possible to reduce bandwidth
Common Pitfalls:
- Don’t forget to enable
CacheFlag.VOICE_STATE - Don’t block audio threads with I/O or long computations
- Remember to configure the DAVE session factory
- Always check if a user is in a voice channel before connecting
Next Steps
- Scale your bot with sharding for large deployments
- Implement error handling for robust audio features
- Check out Lavaplayer documentation for music bots