A modern, feature-rich, and async-ready API wrapper for Discord written in Python
—
Voice connection management and audio playback capabilities for Discord bots. Discord.py provides comprehensive voice support including connection management, audio sources, and real-time voice data processing with opus encoding.
Voice clients manage connections to Discord voice channels with audio playback and recording capabilities.
class VoiceClient:
"""
Voice connection to a Discord voice channel.
"""
def __init__(self, client: Client, channel: VoiceChannel): ...
# Connection properties
client: Client # Associated Discord client
channel: VoiceChannel # Connected voice channel
guild: Guild # Guild containing the voice channel
user: ClientUser # Bot user
session_id: str # Voice session ID
token: str # Voice token
endpoint: str # Voice server endpoint
# Connection state
is_connected: bool # Whether connected to voice
is_playing: bool # Whether currently playing audio
is_paused: bool # Whether audio is paused
source: Optional[AudioSource] # Current audio source
# Connection management
async def connect(self, *, timeout: float = 60.0, reconnect: bool = True, self_deaf: bool = False, self_mute: bool = False) -> None:
"""
Connect to the voice channel.
Parameters:
- timeout: Connection timeout in seconds
- reconnect: Whether to attempt reconnection on disconnect
- self_deaf: Whether to deafen the bot
- self_mute: Whether to mute the bot
"""
async def disconnect(self, *, force: bool = False) -> None:
"""
Disconnect from voice channel.
Parameters:
- force: Whether to force disconnect immediately
"""
async def move_to(self, channel: VoiceChannel) -> None:
"""Move to a different voice channel."""
# Audio playback
def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], None]] = None) -> None:
"""
Play an audio source.
Parameters:
- source: Audio source to play
- after: Callback function called when playback finishes
"""
def stop(self) -> None:
"""Stop current audio playback."""
def pause(self) -> None:
"""Pause current audio playback."""
def resume(self) -> None:
"""Resume paused audio playback."""
# Voice state management
async def edit(self, *, mute: bool = None, deafen: bool = None) -> None:
"""Edit bot's voice state."""
# Properties
@property
def latency(self) -> float:
"""Voice connection latency in seconds."""
@property
def average_latency(self) -> float:
"""Average voice latency over recent packets."""
class VoiceProtocol:
"""
Abstract base class for voice protocol implementations.
"""
def __init__(self, client: Client, channel: VoiceChannel): ...
async def connect(self, **kwargs) -> None:
"""Connect to voice channel."""
raise NotImplementedError
async def disconnect(self, **kwargs) -> None:
"""Disconnect from voice channel."""
raise NotImplementedErrorAudio sources provide audio data for playback with support for various formats and streaming.
class AudioSource:
"""
Base class for audio sources.
"""
def read(self) -> bytes:
"""
Read audio data (20ms of audio at 48kHz stereo).
Returns:
bytes: Audio data in opus format, or empty bytes to signal end
"""
raise NotImplementedError
def cleanup(self) -> None:
"""Clean up audio source resources."""
pass
@property
def is_opus(self) -> bool:
"""Whether audio source provides opus-encoded data."""
return False
class AudioPlayer:
"""
Audio player that handles audio source playback.
"""
def __init__(self, source: AudioSource, client: VoiceClient, *, after: Optional[Callable] = None): ...
source: AudioSource # Audio source
client: VoiceClient # Voice client
def start(self) -> None:
"""Start audio playback."""
def stop(self) -> None:
"""Stop audio playback."""
def pause(self) -> None:
"""Pause audio playback."""
def resume(self) -> None:
"""Resume audio playback."""
@property
def is_playing(self) -> bool:
"""Whether audio is currently playing."""
@property
def is_paused(self) -> bool:
"""Whether audio is paused."""
class PCMAudio(AudioSource):
"""
Audio source for raw PCM audio data.
Parameters:
- stream: File-like object or file path containing PCM data
- executable: FFmpeg executable path (defaults to 'ffmpeg')
"""
def __init__(self, source: Union[str, io.BufferedIOBase], *, executable: str = 'ffmpeg'): ...
def read(self) -> bytes:
"""Read PCM audio data."""
def cleanup(self) -> None:
"""Clean up PCM audio resources."""
class FFmpegAudio(AudioSource):
"""
Audio source using FFmpeg for format conversion and streaming.
Parameters:
- source: Audio source (file path, URL, or stream)
- executable: FFmpeg executable path
- pipe: Whether to use pipe for streaming
- stderr: Where to redirect stderr output
- before_options: FFmpeg options before input
- options: FFmpeg options after input
"""
def __init__(
self,
source: Union[str, io.BufferedIOBase],
*,
executable: str = 'ffmpeg',
pipe: bool = False,
stderr: Optional[io.IOBase] = None,
before_options: Optional[str] = None,
options: Optional[str] = None
): ...
def read(self) -> bytes:
"""Read audio data from FFmpeg."""
def cleanup(self) -> None:
"""Clean up FFmpeg process."""
@classmethod
def from_probe(cls, source: Union[str, io.BufferedIOBase], **kwargs) -> FFmpegAudio:
"""Create FFmpegAudio with automatic format detection."""
class FFmpegPCMAudio(FFmpegAudio):
"""
FFmpeg audio source that outputs PCM format.
"""
def __init__(self, source: Union[str, io.BufferedIOBase], **kwargs): ...
class FFmpegOpusAudio(FFmpegAudio):
"""
FFmpeg audio source that outputs Opus format.
"""
def __init__(self, source: Union[str, io.BufferedIOBase], **kwargs): ...
@property
def is_opus(self) -> bool:
"""Opus audio sources are opus-encoded."""
return True
class PCMVolumeTransformer(AudioSource):
"""
Audio source that applies volume transformation to PCM audio.
Parameters:
- original: Original audio source
- volume: Volume multiplier (0.0 to 2.0, default 1.0)
"""
def __init__(self, original: AudioSource, *, volume: float = 1.0): ...
original: AudioSource # Original audio source
volume: float # Volume level (0.0 to 2.0)
def read(self) -> bytes:
"""Read volume-adjusted audio data."""
def cleanup(self) -> None:
"""Clean up transformer resources."""
@classmethod
def from_other_source(cls, other: AudioSource, *, volume: float = 1.0) -> PCMVolumeTransformer:
"""Create volume transformer from another source."""Opus codec interface for voice encoding and decoding.
class Encoder:
"""
Opus encoder for voice data.
Parameters:
- sampling_rate: Audio sampling rate (default: 48000)
- channels: Number of audio channels (default: 2)
- application: Encoder application type
"""
def __init__(
self,
sampling_rate: int = 48000,
channels: int = 2,
application: int = None
): ...
def encode(self, pcm: bytes, frame_size: int) -> bytes:
"""
Encode PCM audio to Opus format.
Parameters:
- pcm: PCM audio data
- frame_size: Frame size in samples
Returns:
bytes: Opus-encoded audio data
"""
def set_bitrate(self, kbps: int) -> None:
"""Set encoder bitrate in kbps."""
def set_bandwidth(self, req: int) -> None:
"""Set encoder bandwidth."""
def set_signal_type(self, req: int) -> None:
"""Set signal type (voice/music)."""
# Opus utilities
def is_loaded() -> bool:
"""Check if Opus library is loaded."""
def load_opus(name: str) -> None:
"""Load Opus library from specified path."""
# Opus exceptions
class OpusError(DiscordException):
"""Base exception for Opus codec errors."""
pass
class OpusNotLoaded(OpusError):
"""Opus library is not loaded."""
passVoice state objects track user voice connection status and properties.
class VoiceState:
"""
Represents a user's voice state in a guild.
"""
def __init__(self, *, data: Dict[str, Any], channel: Optional[VoiceChannel] = None): ...
# User and guild info
user_id: int # User ID
guild: Guild # Guild containing the voice state
member: Optional[Member] # Member object
# Voice channel info
channel: Optional[VoiceChannel] # Connected voice channel
session_id: str # Voice session ID
# Voice settings
deaf: bool # Whether user is server deafened
mute: bool # Whether user is server muted
self_deaf: bool # Whether user is self deafened
self_mute: bool # Whether user is self muted
self_stream: bool # Whether user is streaming
self_video: bool # Whether user has video enabled
suppress: bool # Whether user is suppressed (priority speaker)
afk: bool # Whether user is in AFK channel
# Voice activity
requested_to_speak_at: Optional[datetime] # When user requested to speak (stage channels)
@property
def is_afk(self) -> bool:
"""Whether user is in the AFK channel."""import discord
from discord.ext import commands
import asyncio
bot = commands.Bot(command_prefix='!', intents=discord.Intents.all())
@bot.command()
async def join(ctx):
"""Join the user's voice channel."""
if ctx.author.voice is None:
await ctx.send("You're not in a voice channel!")
return
channel = ctx.author.voice.channel
if ctx.voice_client is not None:
await ctx.voice_client.move_to(channel)
await ctx.send(f"Moved to {channel.name}")
else:
await channel.connect()
await ctx.send(f"Joined {channel.name}")
@bot.command()
async def leave(ctx):
"""Leave the voice channel."""
if ctx.voice_client is None:
await ctx.send("I'm not connected to a voice channel!")
return
await ctx.voice_client.disconnect()
await ctx.send("Disconnected from voice channel")
@bot.command()
async def play(ctx, *, url):
"""Play audio from a URL."""
if ctx.voice_client is None:
await ctx.send("I'm not connected to a voice channel!")
return
if ctx.voice_client.is_playing():
await ctx.send("Already playing audio!")
return
try:
# Create audio source
source = discord.FFmpegPCMAudio(url)
# Play audio
ctx.voice_client.play(source, after=lambda e: print(f'Player error: {e}') if e else None)
await ctx.send(f"Now playing: {url}")
except Exception as e:
await ctx.send(f"Error playing audio: {e}")
@bot.command()
async def stop(ctx):
"""Stop audio playback."""
if ctx.voice_client is None or not ctx.voice_client.is_playing():
await ctx.send("Not playing any audio!")
return
ctx.voice_client.stop()
await ctx.send("Stopped audio playback")
@bot.command()
async def pause(ctx):
"""Pause audio playback."""
if ctx.voice_client is None or not ctx.voice_client.is_playing():
await ctx.send("Not playing any audio!")
return
ctx.voice_client.pause()
await ctx.send("Paused audio playback")
@bot.command()
async def resume(ctx):
"""Resume audio playback."""
if ctx.voice_client is None or not ctx.voice_client.is_paused():
await ctx.send("Audio is not paused!")
return
ctx.voice_client.resume()
await ctx.send("Resumed audio playback")import asyncio
import youtube_dl
from collections import deque
# Suppress noise about console usage from errors
youtube_dl.utils.bug_reports_message = lambda: ''
ytdl_format_options = {
'format': 'bestaudio/best',
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
'restrictfilenames': True,
'noplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': False,
'logtostderr': False,
'quiet': True,
'no_warnings': True,
'default_search': 'auto',
'source_address': '0.0.0.0'
}
ffmpeg_options = {
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
'options': '-vn'
}
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
class YTDLSource(discord.PCMVolumeTransformer):
def __init__(self, source, *, data, volume=0.5):
super().__init__(source, volume=volume)
self.data = data
self.title = data.get('title')
self.url = data.get('url')
self.duration = data.get('duration')
self.uploader = data.get('uploader')
@classmethod
async def from_url(cls, url, *, loop=None, stream=False):
loop = loop or asyncio.get_event_loop()
data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))
if 'entries' in data:
data = data['entries'][0]
filename = data['url'] if stream else ytdl.prepare_filename(data)
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
class MusicBot(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.queue = deque()
self.current = None
self.volume = 0.5
def get_queue_embed(self):
if not self.queue:
return discord.Embed(title="Queue", description="Queue is empty", color=0xff0000)
embed = discord.Embed(title="Music Queue", color=0x00ff00)
queue_list = []
for i, song in enumerate(list(self.queue)[:10], 1): # Show first 10 songs
duration = f" ({song.duration // 60}:{song.duration % 60:02d})" if song.duration else ""
queue_list.append(f"{i}. {song.title}{duration}")
embed.description = "\n".join(queue_list)
if len(self.queue) > 10:
embed.set_footer(text=f"... and {len(self.queue) - 10} more songs")
return embed
@commands.command()
async def play(self, ctx, *, url):
"""Add a song to the queue and play it."""
async with ctx.typing():
try:
source = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
source.volume = self.volume
self.queue.append(source)
if not ctx.voice_client.is_playing():
await self.play_next(ctx)
else:
embed = discord.Embed(
title="Added to Queue",
description=f"**{source.title}**\nby {source.uploader}",
color=0x00ff00
)
embed.set_footer(text=f"Position in queue: {len(self.queue)}")
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(f"Error: {e}")
async def play_next(self, ctx):
"""Play the next song in the queue."""
if not self.queue:
self.current = None
return
source = self.queue.popleft()
self.current = source
def after_playing(error):
if error:
print(f'Player error: {error}')
# Schedule next song
coro = self.play_next(ctx)
fut = asyncio.run_coroutine_threadsafe(coro, self.bot.loop)
try:
fut.result()
except:
pass
ctx.voice_client.play(source, after=after_playing)
embed = discord.Embed(
title="Now Playing",
description=f"**{source.title}**\nby {source.uploader}",
color=0x0099ff
)
if source.duration:
embed.add_field(name="Duration", value=f"{source.duration // 60}:{source.duration % 60:02d}")
await ctx.send(embed=embed)
@commands.command()
async def queue(self, ctx):
"""Show the current queue."""
embed = self.get_queue_embed()
if self.current:
embed.add_field(
name="Currently Playing",
value=f"**{self.current.title}**",
inline=False
)
await ctx.send(embed=embed)
@commands.command()
async def skip(self, ctx):
"""Skip the current song."""
if ctx.voice_client and ctx.voice_client.is_playing():
ctx.voice_client.stop()
await ctx.send("⏭️ Skipped!")
else:
await ctx.send("Nothing is playing!")
@commands.command()
async def volume(self, ctx, volume: int = None):
"""Change or show the current volume."""
if volume is None:
await ctx.send(f"Current volume: {int(self.volume * 100)}%")
return
if not 0 <= volume <= 100:
await ctx.send("Volume must be between 0 and 100")
return
self.volume = volume / 100
if self.current:
self.current.volume = self.volume
await ctx.send(f"🔊 Volume set to {volume}%")
@commands.command()
async def clear(self, ctx):
"""Clear the queue."""
self.queue.clear()
await ctx.send("🗑️ Queue cleared!")
@commands.command()
async def nowplaying(self, ctx):
"""Show information about the currently playing song."""
if not self.current:
await ctx.send("Nothing is playing!")
return
embed = discord.Embed(
title="Now Playing",
description=f"**{self.current.title}**",
color=0x0099ff
)
embed.add_field(name="Uploader", value=self.current.uploader)
embed.add_field(name="Volume", value=f"{int(self.current.volume * 100)}%")
if self.current.duration:
embed.add_field(name="Duration", value=f"{self.current.duration // 60}:{self.current.duration % 60:02d}")
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(MusicBot(bot))@bot.event
async def on_voice_state_update(member, before, after):
"""Monitor voice state changes."""
# User joined a voice channel
if before.channel is None and after.channel is not None:
print(f"{member} joined {after.channel}")
# Welcome message for specific channel
if after.channel.name == "General Voice":
channel = discord.utils.get(member.guild.text_channels, name="general")
if channel:
await channel.send(f"👋 {member.mention} joined voice chat!")
# User left a voice channel
elif before.channel is not None and after.channel is None:
print(f"{member} left {before.channel}")
# If bot is alone in voice channel, disconnect
if before.channel.guild.voice_client:
voice_client = before.channel.guild.voice_client
if len(voice_client.channel.members) == 1: # Only bot left
await voice_client.disconnect()
channel = discord.utils.get(member.guild.text_channels, name="general")
if channel:
await channel.send("🔇 Left voice channel (nobody else listening)")
# User moved between voice channels
elif before.channel != after.channel:
print(f"{member} moved from {before.channel} to {after.channel}")
# Voice state changes (mute, deafen, etc.)
if before.self_mute != after.self_mute:
if after.self_mute:
print(f"{member} muted themselves")
else:
print(f"{member} unmuted themselves")
if before.self_deaf != after.self_deaf:
if after.self_deaf:
print(f"{member} deafened themselves")
else:
print(f"{member} undeafened themselves")Low-level opus audio encoding and decoding functionality for advanced voice applications.
# Opus Module Functions
def load_opus(name: str) -> None:
"""Load a specific opus library by name."""
def is_loaded() -> bool:
"""Check if opus library is loaded and available."""
# Opus Encoder/Decoder Classes
class Encoder:
"""Opus audio encoder for voice data."""
def __init__(self, sampling_rate: int = 48000, channels: int = 2): ...
def encode(self, pcm: bytes, frame_size: int) -> bytes: ...
class Decoder:
"""Opus audio decoder for voice data."""
def __init__(self, sampling_rate: int = 48000, channels: int = 2): ...
def decode(self, data: bytes, *, decode_fec: bool = False) -> bytes: ...
# Opus Exceptions
class OpusError(DiscordException):
"""Exception raised when opus operation fails."""
class OpusNotLoaded(DiscordException):
"""Exception raised when opus library is not loaded."""Install with Tessl CLI
npx tessl i tessl/pypi-discord-py