Audio Playback⚓︎
Reminder
Remember to install discord.py[voice]
rather than just discord.py
in order to obtain all necessary libraries for voice functionality.
Installing ffmpeg⚓︎
It's an executable needed to work with audio in discord.py. You can also use avconv
instead
Adding ffmpeg to PATH (for Windows)⚓︎
The steps may vary slightly depending on your Windows version, but they are generally the same. If you're having trouble, you can find guides for your particular version on YouTube.
Press Win
and search for "Edit environment variables"
Click Path
, then click Edit
Click New
Enter your path to the bin
directory of ffmpeg
Voice basics⚓︎
Connecting to the voice channel⚓︎
ctx.author.voice
is None
if user is not in the voice channel
Disconnecting from the voice channel⚓︎
ctx.voice_client
is None
if bot is not in the voice channel
Moving to another voice channel⚓︎
@bot.command()
async def disconnect(ctx: commands.Context, channel: discord.VoiceChannel):
await ctx.voice_client.move_to(channel)
Playing an audio⚓︎
@bot.command()
async def play(ctx: commands.Context):
ctx.voice_client.play(discord.FFmpegPCMAudio("my-cool-music.mp3"))
If you need to set path to ffmpeg
executable directly, use
You can see other FFmpegPCMAudio
possible arguments in the docs. Instead of FFmpegPCMAudio
also FFmpegOpusAudio
can be used.
What is PCM and Opus⚓︎
PCM - Think of this as raw audio; it is not encoded in anything special and is used by your computer at a lower level.
Opus - This is a lossy audio format; it's an encoding applied to PCM that makes music playable over Discord. An Opus encoder generates Opus packets, which can play over Discord.
FFmpegPCMAudio
produces PCM packets that need to be encoded to Opus.
FFmpegOpusAudio
produces Opus packets, skipping the encoding step done by the library.
Pause playing⚓︎
Resume playing⚓︎
Changing volume⚓︎
If you want to have volume controls, use PCMVolumeTransformer
when initializing the audio. Without that you won't be able to change the audio volume
@bot.command()
async def volume(ctx: commands.Context, volume: int):
ctx.voice_client.source.volume = volume / 100
Examples⚓︎
Putting all together⚓︎
import discord
from discord.ext import commands
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix=".", intents=intents)
@bot.command()
async def connect(ctx: commands.Context):
if ctx.author.voice is None:
return await ctx.send("You are not connected to the voice channel!")
await ctx.author.voice.channel.connect()
await ctx.send("Connected!")
@bot.command()
async def disconnect(ctx: commands.Context):
if ctx.voice_client is None:
return await ctx.send("I am not connected to the voice channel!")
await ctx.voice_client.disconnect()
await ctx.send("Disconnected!")
@bot.command()
async def pause(ctx: commands.Context):
if ctx.voice_client is None:
return await ctx.send("I am not connected to the voice channel!")
if ctx.voice_client.is_playing():
ctx.voice_client.pause()
await ctx.send("Paused!")
@bot.command()
async def resume(ctx: commands.Context):
if ctx.voice_client is None:
return await ctx.send("I am not connected to the voice channel!")
if ctx.voice_client.is_paused():
ctx.voice_client.resume()
await ctx.send("Resumed playing!")
@bot.command()
async def play(ctx: commands.Context, file: str):
if ctx.voice_client is None:
return await ctx.send("I am not connected to the voice channel!")
ctx.voice_client.play(discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(f"music/{file}.mp3")))
await ctx.send(f"Playing `{file}` :musical_note:")
@bot.command()
async def volume(ctx: commands.Context, volume: int):
ctx.voice_client.source.volume = volume / 100
await ctx.send(f"Changed volume to {volume}%")
bot.run("token")
Tip
You may improve the code better by using custom checks rather than things likeif ctx.author.voice is None: ...
Creating custom voice client⚓︎
We can only play one song at a time right now. Let's extend VoiceCLient
class to include a music queue
class CustomVoiceClient(discord.VoiceClient):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queue = []
def track_finished(self, error):
self.queue.pop(0)
if self.queue:
self.play(self.queue[0], after=self.track_finished)
def add_track(self, track: discord.AudioSource):
self.queue.append(track)
if len(self.queue) == 1:
self.play(track, after=self.track_finished)
def skip_track(self):
if self.is_playing():
self.stop()
elif self.queue:
self.queue.pop(0)
To use it instead of default one we need to pass the class in the cls
argument when connecting to the channel
Adding buttons⚓︎
import discord
from discord.ext import commands
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix=".", intents=intents)
class CustomVoiceClient(discord.VoiceClient):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queue = []
def track_finished(self, error):
self.queue.pop(0)
if self.queue:
self.play(self.queue[0], after=self.track_finished)
def add_track(self, track: discord.AudioSource):
self.queue.append(track)
if len(self.queue) == 1:
self.play(track, after=self.track_finished)
def skip_track(self):
if self.is_playing():
self.stop()
elif self.queue:
self.queue.pop(0)
class PlayerView(discord.ui.View):
def __init__(self, voice_client: CustomVoiceClient):
super().__init__(timeout=None)
self.voice_client = voice_client
@discord.ui.button(emoji="🔉")
async def reduce_volume(self, interaction: discord.Interaction, button: discord.Button):
self.voice_client.source.volume -= 0.1
await interaction.response.send_message(f"Volume is now {int(self.voice_client.source.volume * 100)}%", ephemeral=True)
@discord.ui.button(emoji="⏯️")
async def pause_or_resume(self, interaction: discord.Interaction, button: discord.Button):
if self.voice_client.is_paused():
self.voice_client.resume()
await interaction.response.send_message("Resumed!", ephemeral=True)
else:
self.voice_client.pause()
await interaction.response.send_message("Paused!", ephemeral=True)
@discord.ui.button(emoji="🔊")
async def increase_volume(self, interaction: discord.Interaction, button: discord.Button):
self.voice_client.source.volume += 0.1
await interaction.response.send_message(f"Volume is now {int(self.voice_client.source.volume * 100)}%", ephemeral=True)
@discord.ui.button(emoji="⏭️")
async def next_track(self, interaction: discord.Interaction, button: discord.Button):
self.voice_client.skip_track()
await interaction.response.send_message(f"Track skipped!", ephemeral=True)
@bot.command()
async def play(ctx: commands.Context, file: str):
if ctx.author.voice is None:
return await ctx.send("You are not connected to the voice channel!")
if ctx.voice_client is None:
voice_client = await ctx.author.voice.channel.connect(cls=CustomVoiceClient)
voice_client.add_track(discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(f"music/{file}.mp3")))
await ctx.send(embed=discord.Embed(title="🎶 🎵 🎶 Playing music 🎶 🎵 🎶"), view=PlayerView(voice_client))
else:
ctx.voice_client.add_track(discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(f"music/{file}.mp3")))
bot.run("token")
Tip
You can use your custom emojis to make the buttons look very cool
Getting music from SoundCloud⚓︎
For that we will use a very nice wrapper of Lavalink for discord.py named wavelink. In addition, it can work with other platforms besides SoundCloud.
This is just a tiny sample of what you can do. Check out wavelink docs for more
Run this in a terminal to install it:
A Lavalink node will then be required. You can set it up by yourself, or you can find some in the internet. Actually, there are plenty of free ones.
import discord
from discord.ext import commands
import wavelink
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix=".", intents=intents)
def format_duration(mseconds):
minutes, seconds = divmod(mseconds // 1000, 60)
if minutes >= 60:
hours, minutes = divmod(minutes, 60)
return f"{hours:02}:{minutes:02}:{seconds:02}"
return f"{minutes:02}:{seconds:02}"
def create_bar(position, duration):
n = int(position * 15 // duration)
return "█" * n + "▒" * (15 - n)
@bot.event
async def setup_hook():
nodes = [wavelink.Node(uri="...", password="...")]
await wavelink.Pool.connect(nodes=nodes, client=bot)
@bot.event
async def on_wavelink_track_end(payload: wavelink.TrackEndEventPayload):
player = payload.player
if player.queue:
await player.play(player.queue.get())
else:
await player.disconnect()
@bot.command()
async def play(ctx: commands.Context, query: str):
tracks = await wavelink.Pool.fetch_tracks(f"scsearch:{query}")
if not tracks:
return await ctx.send("Nothing found.")
if ctx.author.voice is None:
return await ctx.send("You are not connected to the voice channel!")
if ctx.voice_client is None:
player = await ctx.author.voice.channel.connect(cls=wavelink.Player)
else:
player = ctx.voice_client
track = tracks[0]
if player.playing:
player.queue.put(track)
embed = discord.Embed(title="Song added to playlist", description=f"[{track.title}]({track.uri})")
else:
await player.play(track)
embed = discord.Embed(title="Started playing", description=f"[{track.title}]({track.uri})")
await ctx.send(embed=embed)
@bot.command()
async def np(ctx: commands.Context):
player = ctx.voice_client
if not player or not player.playing:
return await ctx.send("I am not playing anything now")
track = player.current
embed = discord.Embed(
title=f"Now Playing {['▶️', '⏸️'][player.paused]}",
description=f"[{track.title}]({track.uri})\n{format_duration(player.position)} {create_bar(player.position, track.length)} {format_duration(track.length)}")
embed.set_thumbnail(url=track.artwork)
await ctx.send(embed=embed)
@bot.command()
async def pause(ctx: commands.Context):
if not ctx.voice_client:
return await ctx.send("I am not playing anything now")
await ctx.voice_client.pause(True)
@bot.command()
async def resume(ctx: commands.Context):
if not ctx.voice_client:
return await ctx.send("I am not playing anything now")
await ctx.voice_client.pause(False)
@bot.command()
async def skip(ctx: commands.Context):
if not ctx.voice_client:
return await ctx.send("I am not playing anything now")
await ctx.voice_client.skip()
@bot.command()
async def volume(ctx: commands.Context, volume: int):
if not ctx.voice_client:
return await ctx.send("I am not playing anything now")
ctx.voice_client.set_volume(volume)
await ctx.send(f"Changed volume to {volume}%")
bot.run("token")