Skip to main content

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

1

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.
2

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.