Play audio in a voice channel with @discordjs/voice

Join a voice channel and play a local audio file straight from your bot — no Lavalink — using @discordjs/voice. Everything it needs (the opus, encryption, and ffmpeg pieces) is already on Falix's Node image.

Sometimes you don't need a whole Lavalink stack — you just want your bot to join a voice channel and play a sound file it already has. That's what @discordjs/voice does: your bot streams the audio itself. It's perfect for a soundboard, a join chime, a text-to-speech clip, or a single local track. This guide gets one playing.

At a glance
You need a working discord.js bot on a Falix Node.js server, and an audio file to play
Plan free or premium
Time about twenty minutes

🎯 Good to know: @discordjs/voice streams audio from your bot's process. That's great for short clips and a personal music bot, but it doesn't scale like Lavalink — for a public music bot serving many servers, use the Lavalink approach instead. This is the lightweight path.

What playing audio actually needs

Discord voice has three moving parts under @discordjs/voice: an opus encoder (Discord's audio codec), an encryption library (voice packets are encrypted), and ffmpeg (to turn an mp3/other file into a stream Discord accepts). The good news for Falix: ffmpeg is already installed on the Node.js image, and the opus and encryption packages install as prebuilt binaries — there's nothing to compile.

1. Install the packages

Open the Packages page in your server menu and install these three:

  • @discordjs/voice — the voice library itself.
  • @discordjs/opus — the opus encoder.
  • libsodium-wrappers — the encryption library.

Then restart so your bot loads them. (You don't need to install ffmpeg — it's part of the image.)

💡 Tip: If you'd rather not use a native package, opusscript (pure-JavaScript opus) works in place of @discordjs/opus. @discordjs/opus is faster and installs cleanly here, so it's the default recommendation.

2. Add the voice intent

Joining voice needs one extra intent beyond a normal slash-command bot — GuildVoiceStates, which is not privileged, so there's nothing to toggle in the Developer Portal:

const { Client, GatewayIntentBits } = require('discord.js');

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildVoiceStates, // needed to see who's in voice
  ],
});

3. Put an audio file on the server

Upload your file — say sound.mp3 — to /home/container with the File Manager (drag and drop). Your code will reference it by name.

⚠️ Heads up: Files you upload live on the server itself, so a reinstall wipes them — the same rule as any local file. Keep the original somewhere safe (or in your Git repo) so you can re-upload.

4. The code

A /play command that joins the caller's voice channel, plays the file, and leaves when it's done:

const {
  joinVoiceChannel,
  createAudioPlayer,
  createAudioResource,
  AudioPlayerStatus,
  VoiceConnectionStatus,
} = require('@discordjs/voice');

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== 'play') return;

  // The person must be in a voice channel.
  const channel = interaction.member.voice.channel;
  if (!channel) {
    return interaction.reply({ content: 'Join a voice channel first!', ephemeral: true });
  }

  const connection = joinVoiceChannel({
    channelId: channel.id,
    guildId: channel.guild.id,
    adapterCreator: channel.guild.voiceAdapterCreator,
  });

  const player = createAudioPlayer();
  const resource = createAudioResource('sound.mp3');
  player.play(resource);
  connection.subscribe(player);

  await interaction.reply('▶️ Playing!');

  // Leave the channel when the track finishes.
  player.on(AudioPlayerStatus.Idle, () => connection.destroy());
});

Four steps in order: join the channel (the adapterCreator is how discord.js hands the voice connection to @discordjs/voice), build a player, wrap your file in an audio resource, and subscribe the connection to the player so what the player plays goes out to the channel. The Idle handler makes the bot leave cleanly instead of lurking after the sound ends.

Register the /play command the same way as every other command — see the discord.js bot guide.

Verify it works

Start the bot, join a voice channel yourself, and run /play. The bot joins, you hear the file, and it leaves when the clip ends. The console stays quiet on success — the audio and the bot's presence in the channel are your signal.

💡 Tip: If anything's off, log @discordjs/voice's built-in report: const { generateDependencyReport } = require('@discordjs/voice'); console.log(generateDependencyReport());. It prints exactly which opus, encryption, and ffmpeg pieces it found — the fastest way to spot a missing package.

Troubleshooting

  • Cannot find module '@discordjs/voice' (or opus/sodium) — the package isn't installed. Add all three from the Packages page and restart. See Bot appears offline for reading install errors.
  • Bot joins but there's no sound — usually a missing opus or encryption library. Run the dependency report above; you want an opus library and an encryption library both listed as found.
  • Bot never joins — either the caller isn't in a voice channel (the code checks for that) or the GuildVoiceStates intent is missing from the Client.
  • "Cannot connect" / no permission — the bot needs Connect and Speak on the channel. Give it a role that has them.
  • Killed under load / audio stutters — streaming audio uses memory and CPU; a starved free server can get killed. For a busy music bot, move to Lavalink.

Next steps

Was this guide helpful?