CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-nextcord

A Python wrapper for the Discord API forked from discord.py

Pending
Overview
Eval results
Files

voice.mddocs/

Nextcord Voice and Audio

Voice connection management, audio playbook, and voice state handling for Discord voice features with comprehensive audio processing capabilities.

Voice Client

Core voice connection functionality for joining voice channels and managing audio playback.

VoiceClient Class { .api }

import nextcord
from nextcord import VoiceClient, AudioSource, PCMVolumeTransformer
from typing import Optional, Callable, Any
import asyncio

class VoiceClient:
    """Represents a Discord voice connection.
    
    This is used to manage voice connections and audio playback in Discord voice channels.
    
    Attributes
    ----------
    token: str
        The voice connection token.
    guild: Guild
        The guild this voice client is connected to.
    channel: Optional[VoiceChannel]
        The voice channel this client is connected to.
    endpoint: str
        The voice server endpoint.
    endpoint_ip: str
        The voice server IP address.
    port: int
        The voice server port.
    ssrc: int
        The synchronization source identifier.
    secret_key: bytes
        The secret key for voice encryption.
    sequence: int
        The current sequence number.
    timestamp: int
        The current timestamp.
    mode: str
        The voice encryption mode.
    user: ClientUser
        The bot user associated with this voice client.
    """
    
    @property
    def latency(self) -> float:
        """float: The latency of the voice connection in seconds."""
        ...
    
    @property
    def average_latency(self) -> float:
        """float: The average latency over the last 20 heartbeats."""
        ...
    
    def is_connected(self) -> bool:
        """bool: Whether the voice client is connected to a voice channel."""
        ...
    
    def is_playing(self) -> bool:
        """bool: Whether audio is currently being played."""
        ...
    
    def is_paused(self) -> bool:
        """bool: Whether audio playback is currently paused."""
        ...
    
    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: float
            The timeout for connecting to voice.
        reconnect: bool
            Whether to reconnect on disconnection.
        self_deaf: bool
            Whether to deafen the bot upon connection.
        self_mute: bool
            Whether to mute the bot upon connection.
        """
        ...
    
    async def disconnect(self, *, force: bool = False) -> None:
        """Disconnect from the voice channel.
        
        Parameters
        ----------
        force: bool
            Whether to forcefully disconnect even if audio is playing.
        """
        ...
    
    async def move_to(self, channel: nextcord.VoiceChannel) -> None:
        """Move the voice client to a different voice channel.
        
        Parameters
        ----------
        channel: nextcord.VoiceChannel
            The voice channel to move to.
        """
        ...
    
    def play(
        self,
        source: AudioSource,
        *,
        after: Optional[Callable[[Optional[Exception]], Any]] = None
    ) -> None:
        """Play an audio source.
        
        Parameters
        ----------
        source: AudioSource
            The audio source to play.
        after: Optional[Callable]
            A callback function called after the audio finishes playing.
        """
        ...
    
    def stop(self) -> None:
        """Stop the currently playing audio."""
        ...
    
    def pause(self) -> None:
        """Pause the currently playing audio."""
        ...
    
    def resume(self) -> None:
        """Resume the currently paused audio."""
        ...
    
    @property
    def source(self) -> Optional[AudioSource]:
        """Optional[AudioSource]: The currently playing audio source."""
        ...

# Basic voice connection example
@bot.command()
async def join(ctx):
    """Join the user's voice channel."""
    if not ctx.author.voice:
        await ctx.send("❌ You are not connected to a voice channel.")
        return
    
    channel = ctx.author.voice.channel
    
    if ctx.voice_client is not None:
        # Already connected, move to new channel
        await ctx.voice_client.move_to(channel)
        await ctx.send(f"🔊 Moved to {channel.name}")
    else:
        # Connect to the channel
        voice_client = await channel.connect()
        await ctx.send(f"🔊 Connected to {channel.name}")

@bot.command()
async def leave(ctx):
    """Leave the voice channel."""
    if ctx.voice_client is None:
        await ctx.send("❌ Not connected to a voice channel.")
        return
    
    await ctx.voice_client.disconnect()
    await ctx.send("👋 Disconnected from voice channel.")

# Voice channel connection with error handling
async def connect_to_voice_channel(
    channel: nextcord.VoiceChannel,
    timeout: float = 10.0
) -> Optional[VoiceClient]:
    """Connect to a voice channel with proper error handling."""
    try:
        voice_client = await channel.connect(timeout=timeout)
        print(f"Connected to {channel.name} in {channel.guild.name}")
        return voice_client
        
    except asyncio.TimeoutError:
        print(f"Timeout connecting to {channel.name}")
        return None
        
    except nextcord.ClientException as e:
        print(f"Already connected to voice: {e}")
        return None
        
    except nextcord.Forbidden:
        print(f"No permission to connect to {channel.name}")
        return None
        
    except Exception as e:
        print(f"Unexpected error connecting to voice: {e}")
        return None

Audio Sources

Audio source classes for playing various types of audio content.

AudioSource Classes { .api }

class AudioSource:
    """Base class for audio sources.
    
    All audio sources must inherit from this class and implement the read method.
    """
    
    def read(self) -> bytes:
        """Read audio data.
        
        Returns
        -------
        bytes
            20ms of audio data in PCM format, or empty bytes to signal end.
        """
        ...
    
    def cleanup(self) -> None:
        """Clean up any resources used by the audio source."""
        ...
    
    def is_opus(self) -> bool:
        """bool: Whether this source provides Opus-encoded audio."""
        return False

class FFmpegPCMAudio(AudioSource):
    """An audio source that uses FFmpeg to convert audio to PCM.
    
    This is the most common audio source for playing files or streams.
    """
    
    def __init__(
        self,
        source: str,
        *,
        executable: str = 'ffmpeg',
        pipe: bool = False,
        stderr: Optional[Any] = None,
        before_options: Optional[str] = None,
        options: Optional[str] = None
    ):
        """Initialize FFmpeg PCM audio source.
        
        Parameters
        ----------
        source: str
            The audio source (file path or URL).
        executable: str
            The FFmpeg executable path.
        pipe: bool
            Whether to pipe the audio through stdin.
        stderr: Optional[Any]
            Where to redirect stderr output.
        before_options: Optional[str]
            FFmpeg options to use before the input source.
        options: Optional[str]
            FFmpeg options to use after the input source.
        """
        ...

class FFmpegOpusAudio(AudioSource):
    """An audio source that uses FFmpeg to provide Opus-encoded audio.
    
    This is more efficient than PCM audio as it doesn't require re-encoding.
    """
    
    def __init__(
        self,
        source: str,
        *,
        bitrate: int = 128,
        **kwargs
    ):
        """Initialize FFmpeg Opus audio source.
        
        Parameters
        ----------
        source: str
            The audio source (file path or URL).
        bitrate: int
            The audio bitrate in kbps.
        **kwargs
            Additional arguments passed to FFmpegPCMAudio.
        """
        ...
    
    def is_opus(self) -> bool:
        """bool: Always returns True for Opus sources."""
        return True

class PCMVolumeTransformer(AudioSource):
    """A volume transformer for PCM audio sources.
    
    This allows you to control the volume of audio playback.
    """
    
    def __init__(self, original: AudioSource, volume: float = 0.5):
        """Initialize volume transformer.
        
        Parameters
        ----------
        original: AudioSource
            The original audio source to transform.
        volume: float
            The volume level (0.0 to 1.0).
        """
        ...
    
    @property
    def volume(self) -> float:
        """float: The current volume level."""
        ...
    
    @volume.setter
    def volume(self, value: float) -> None:
        """Set the volume level.
        
        Parameters
        ----------
        value: float
            The volume level (0.0 to 1.0).
        """
        ...

# Audio source examples
@bot.command()
async def play_file(ctx, *, filename: str):
    """Play an audio file."""
    if not ctx.voice_client:
        await ctx.send("❌ Not connected to a voice channel. Use `!join` first.")
        return
    
    try:
        # Create audio source from file
        source = nextcord.FFmpegPCMAudio(filename)
        
        # Play the audio
        ctx.voice_client.play(source, after=lambda e: print(f'Player error: {e}') if e else None)
        
        await ctx.send(f"🎵 Now playing: {filename}")
        
    except Exception as e:
        await ctx.send(f"❌ Error playing file: {e}")

@bot.command()
async def play_url(ctx, *, url: str):
    """Play audio from a URL."""
    if not ctx.voice_client:
        await ctx.send("❌ Not connected to a voice channel.")
        return
    
    try:
        # Use FFmpeg to stream from URL
        # Common options for streaming
        ffmpeg_options = {
            'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
            'options': '-vn'  # Disable video
        }
        
        source = nextcord.FFmpegPCMAudio(url, **ffmpeg_options)
        
        ctx.voice_client.play(source)
        await ctx.send(f"🎵 Now playing from URL")
        
    except Exception as e:
        await ctx.send(f"❌ Error playing from URL: {e}")

@bot.command()
async def play_with_volume(ctx, volume: float, *, filename: str):
    """Play an audio file with specified volume."""
    if not ctx.voice_client:
        await ctx.send("❌ Not connected to a voice channel.")
        return
    
    if not 0.0 <= volume <= 1.0:
        await ctx.send("❌ Volume must be between 0.0 and 1.0")
        return
    
    try:
        # Create audio source with volume control
        original_source = nextcord.FFmpegPCMAudio(filename)
        source = nextcord.PCMVolumeTransformer(original_source, volume=volume)
        
        ctx.voice_client.play(source)
        await ctx.send(f"🎵 Now playing {filename} at {volume:.0%} volume")
        
    except Exception as e:
        await ctx.send(f"❌ Error: {e}")

@bot.command()
async def volume(ctx, new_volume: float):
    """Change the volume of currently playing audio."""
    if not ctx.voice_client or not ctx.voice_client.source:
        await ctx.send("❌ Nothing is currently playing.")
        return
    
    if not isinstance(ctx.voice_client.source, nextcord.PCMVolumeTransformer):
        await ctx.send("❌ Current audio source doesn't support volume control.")
        return
    
    if not 0.0 <= new_volume <= 1.0:
        await ctx.send("❌ Volume must be between 0.0 and 1.0")
        return
    
    ctx.voice_client.source.volume = new_volume
    await ctx.send(f"🔊 Volume set to {new_volume:.0%}")

Music Bot Implementation

Complete music bot implementation with queue management and playback controls.

Music Bot System { .api }

import asyncio
import youtube_dl
from collections import deque
from typing import Dict, List, Optional, Any

class Song:
    """Represents a song in the music queue."""
    
    def __init__(
        self,
        title: str,
        url: str,
        duration: Optional[int] = None,
        thumbnail: Optional[str] = None,
        requester: Optional[nextcord.Member] = None
    ):
        self.title = title
        self.url = url
        self.duration = duration
        self.thumbnail = thumbnail
        self.requester = requester
    
    def __str__(self) -> str:
        return self.title
    
    @property
    def duration_formatted(self) -> str:
        """Get formatted duration string."""
        if not self.duration:
            return "Unknown"
        
        minutes, seconds = divmod(self.duration, 60)
        hours, minutes = divmod(minutes, 60)
        
        if hours:
            return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
        else:
            return f"{minutes:02d}:{seconds:02d}"

class MusicQueue:
    """Manages a music queue for a guild."""
    
    def __init__(self):
        self.queue = deque()
        self.current_song = None
        self.loop_mode = "off"  # "off", "song", "queue"
        self.shuffle = False
    
    def add(self, song: Song) -> None:
        """Add a song to the queue."""
        self.queue.append(song)
    
    def next(self) -> Optional[Song]:
        """Get the next song from the queue."""
        if self.loop_mode == "song" and self.current_song:
            return self.current_song
        
        if not self.queue:
            if self.loop_mode == "queue" and self.current_song:
                # Add current song back to queue for looping
                self.queue.append(self.current_song)
            else:
                return None
        
        if self.shuffle:
            import random
            song = random.choice(self.queue)
            self.queue.remove(song)
        else:
            song = self.queue.popleft()
        
        self.current_song = song
        return song
    
    def clear(self) -> int:
        """Clear the queue and return number of songs removed."""
        count = len(self.queue)
        self.queue.clear()
        return count
    
    def remove(self, index: int) -> Optional[Song]:
        """Remove a song by index."""
        try:
            return self.queue.popleft() if index == 0 else self.queue.remove(list(self.queue)[index])
        except (IndexError, ValueError):
            return None
    
    def get_queue_list(self, limit: int = 10) -> List[Song]:
        """Get a list of songs in the queue."""
        return list(self.queue)[:limit]

class YouTubeSource:
    """YouTube audio source with metadata extraction."""
    
    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)
    
    @classmethod
    async def from_url(
        cls,
        url: str,
        *,
        loop: Optional[asyncio.AbstractEventLoop] = None,
        stream: bool = False
    ) -> Dict[str, Any]:
        """Extract song information from YouTube URL."""
        loop = loop or asyncio.get_event_loop()
        
        # Run youtube-dl extraction in thread pool
        data = await loop.run_in_executor(
            None,
            lambda: cls.ytdl.extract_info(url, download=not stream)
        )
        
        if 'entries' in data:
            # Playlist - take first item
            data = data['entries'][0]
        
        filename = data['url'] if stream else cls.ytdl.prepare_filename(data)
        
        return {
            'title': data.get('title'),
            'url': data.get('webpage_url'),
            'stream_url': data.get('url'),
            'duration': data.get('duration'),
            'thumbnail': data.get('thumbnail'),
            'filename': filename
        }
    
    @classmethod
    def get_audio_source(cls, data: Dict[str, Any]) -> nextcord.AudioSource:
        """Get audio source from extracted data."""
        return nextcord.FFmpegPCMAudio(data['filename'], **cls.ffmpeg_options)

class MusicBot:
    """Complete music bot implementation."""
    
    def __init__(self, bot: commands.Bot):
        self.bot = bot
        self.guilds: Dict[int, MusicQueue] = {}
    
    def get_queue(self, guild_id: int) -> MusicQueue:
        """Get or create a music queue for a guild."""
        if guild_id not in self.guilds:
            self.guilds[guild_id] = MusicQueue()
        return self.guilds[guild_id]
    
    async def play_next(self, ctx: commands.Context) -> None:
        """Play the next song in the queue."""
        queue = self.get_queue(ctx.guild.id)
        
        if not ctx.voice_client:
            return
        
        next_song = queue.next()
        if not next_song:
            # Queue is empty
            embed = nextcord.Embed(
                title="🎵 Queue Finished",
                description="No more songs in the queue.",
                color=nextcord.Color.blue()
            )
            await ctx.send(embed=embed)
            return
        
        try:
            # Extract audio source
            data = await YouTubeSource.from_url(next_song.url, stream=True)
            source = YouTubeSource.get_audio_source(data)
            
            # Play the song
            ctx.voice_client.play(
                source,
                after=lambda e: self.bot.loop.create_task(self.play_next(ctx)) if not e else print(f'Player error: {e}')
            )
            
            # Send now playing message
            embed = nextcord.Embed(
                title="🎵 Now Playing",
                description=f"**{next_song.title}**",
                color=nextcord.Color.green()
            )
            
            if next_song.duration:
                embed.add_field(name="Duration", value=next_song.duration_formatted, inline=True)
            
            if next_song.requester:
                embed.add_field(name="Requested by", value=next_song.requester.mention, inline=True)
            
            if next_song.thumbnail:
                embed.set_thumbnail(url=next_song.thumbnail)
            
            await ctx.send(embed=embed)
            
        except Exception as e:
            await ctx.send(f"❌ Error playing {next_song.title}: {e}")
            # Try to play next song
            await self.play_next(ctx)

# Music bot commands
music_bot = MusicBot(bot)

@bot.command(aliases=['p'])
async def play(ctx, *, search: str):
    """Play a song from YouTube."""
    if not ctx.author.voice:
        await ctx.send("❌ You must be in a voice channel to use this command.")
        return
    
    # Connect to voice if not already connected
    if not ctx.voice_client:
        await ctx.author.voice.channel.connect()
    elif ctx.voice_client.channel != ctx.author.voice.channel:
        await ctx.voice_client.move_to(ctx.author.voice.channel)
    
    # Show processing message
    processing_msg = await ctx.send("🔍 Searching...")
    
    try:
        # Extract song information
        data = await YouTubeSource.from_url(search, loop=bot.loop, stream=True)
        
        # Create song object
        song = Song(
            title=data['title'],
            url=data['url'],
            duration=data['duration'],
            thumbnail=data['thumbnail'],
            requester=ctx.author
        )
        
        # Add to queue
        queue = music_bot.get_queue(ctx.guild.id)
        queue.add(song)
        
        # Edit processing message
        if ctx.voice_client.is_playing():
            embed = nextcord.Embed(
                title="📝 Added to Queue",
                description=f"**{song.title}**",
                color=nextcord.Color.blue()
            )
            embed.add_field(name="Position in queue", value=len(queue.queue), inline=True)
            embed.add_field(name="Duration", value=song.duration_formatted, inline=True)
            embed.add_field(name="Requested by", value=ctx.author.mention, inline=True)
            
            if song.thumbnail:
                embed.set_thumbnail(url=song.thumbnail)
            
            await processing_msg.edit(content=None, embed=embed)
        else:
            # Start playing immediately
            await processing_msg.delete()
            await music_bot.play_next(ctx)
            
    except Exception as e:
        await processing_msg.edit(content=f"❌ Error: {e}")

@bot.command()
async def skip(ctx):
    """Skip the current song."""
    if not ctx.voice_client or not ctx.voice_client.is_playing():
        await ctx.send("❌ Nothing is currently playing.")
        return
    
    queue = music_bot.get_queue(ctx.guild.id)
    skipped_song = queue.current_song
    
    ctx.voice_client.stop()  # This will trigger play_next
    
    embed = nextcord.Embed(
        title="⏭️ Song Skipped",
        description=f"Skipped **{skipped_song.title if skipped_song else 'Unknown'}**",
        color=nextcord.Color.orange()
    )
    await ctx.send(embed=embed)

@bot.command()
async def queue(ctx, page: int = 1):
    """Show the music queue."""
    music_queue = music_bot.get_queue(ctx.guild.id)
    
    if not music_queue.current_song and not music_queue.queue:
        await ctx.send("❌ The queue is empty.")
        return
    
    embed = nextcord.Embed(
        title="🎵 Music Queue",
        color=nextcord.Color.blue()
    )
    
    # Current song
    if music_queue.current_song:
        embed.add_field(
            name="🎵 Now Playing",
            value=f"**{music_queue.current_song.title}**\nRequested by {music_queue.current_song.requester.mention if music_queue.current_song.requester else 'Unknown'}",
            inline=False
        )
    
    # Queue
    queue_list = music_queue.get_queue_list(10)
    if queue_list:
        queue_text = []
        for i, song in enumerate(queue_list, 1):
            queue_text.append(f"{i}. **{song.title}** ({song.duration_formatted})")
        
        embed.add_field(
            name=f"📝 Up Next ({len(music_queue.queue)} songs)",
            value="\n".join(queue_text),
            inline=False
        )
    
    # Queue stats
    if music_queue.queue:
        total_duration = sum(song.duration for song in music_queue.queue if song.duration)
        hours, remainder = divmod(total_duration, 3600)
        minutes, _ = divmod(remainder, 60)
        
        embed.set_footer(text=f"Total queue time: {hours}h {minutes}m | Loop: {music_queue.loop_mode}")
    
    await ctx.send(embed=embed)

@bot.command()
async def pause(ctx):
    """Pause the current song."""
    if not ctx.voice_client or not ctx.voice_client.is_playing():
        await ctx.send("❌ Nothing is currently playing.")
        return
    
    ctx.voice_client.pause()
    await ctx.send("⏸️ Paused the music.")

@bot.command()
async def resume(ctx):
    """Resume the paused song."""
    if not ctx.voice_client or not ctx.voice_client.is_paused():
        await ctx.send("❌ Nothing is currently paused.")
        return
    
    ctx.voice_client.resume()
    await ctx.send("▶️ Resumed the music.")

@bot.command()
async def stop(ctx):
    """Stop the music and clear the queue."""
    if not ctx.voice_client:
        await ctx.send("❌ Not connected to a voice channel.")
        return
    
    queue = music_bot.get_queue(ctx.guild.id)
    cleared_count = queue.clear()
    queue.current_song = None
    
    ctx.voice_client.stop()
    
    embed = nextcord.Embed(
        title="⏹️ Music Stopped",
        description=f"Cleared {cleared_count} songs from the queue.",
        color=nextcord.Color.red()
    )
    await ctx.send(embed=embed)

@bot.command()
async def loop(ctx, mode: str = None):
    """Set loop mode (off, song, queue)."""
    queue = music_bot.get_queue(ctx.guild.id)
    
    if mode is None:
        await ctx.send(f"Current loop mode: **{queue.loop_mode}**")
        return
    
    if mode.lower() not in ['off', 'song', 'queue']:
        await ctx.send("❌ Invalid loop mode. Use: `off`, `song`, or `queue`")
        return
    
    queue.loop_mode = mode.lower()
    
    loop_emojis = {
        'off': '❌',
        'song': '🔂',
        'queue': '🔁'
    }
    
    await ctx.send(f"{loop_emojis[mode.lower()]} Loop mode set to: **{mode}**")

@bot.command()
async def shuffle(ctx):
    """Toggle shuffle mode."""
    queue = music_bot.get_queue(ctx.guild.id)
    queue.shuffle = not queue.shuffle
    
    status = "enabled" if queue.shuffle else "disabled"
    emoji = "🔀" if queue.shuffle else "➡️"
    
    await ctx.send(f"{emoji} Shuffle {status}")

@bot.command()
async def nowplaying(ctx):
    """Show information about the currently playing song."""
    queue = music_bot.get_queue(ctx.guild.id)
    
    if not ctx.voice_client or not queue.current_song:
        await ctx.send("❌ Nothing is currently playing.")
        return
    
    song = queue.current_song
    
    embed = nextcord.Embed(
        title="🎵 Now Playing",
        description=f"**{song.title}**",
        color=nextcord.Color.green(),
        url=song.url
    )
    
    if song.duration:
        embed.add_field(name="Duration", value=song.duration_formatted, inline=True)
    
    if song.requester:
        embed.add_field(name="Requested by", value=song.requester.mention, inline=True)
    
    embed.add_field(name="Status", value="Playing ▶️" if ctx.voice_client.is_playing() else "Paused ⏸️", inline=True)
    
    if song.thumbnail:
        embed.set_thumbnail(url=song.thumbnail)
    
    # Queue info
    if queue.queue:
        embed.add_field(name="Next in queue", value=f"{len(queue.queue)} songs", inline=True)
    
    embed.add_field(name="Loop", value=queue.loop_mode.title(), inline=True)
    embed.add_field(name="Shuffle", value="On" if queue.shuffle else "Off", inline=True)
    
    await ctx.send(embed=embed)

Voice Events and State Management

Voice state tracking and event handling for advanced voice features.

Voice State Events { .api }

# Voice state event handlers
@bot.event
async def on_voice_state_update(
    member: nextcord.Member,
    before: nextcord.VoiceState,
    after: nextcord.VoiceState
):
    """Handle voice state updates."""
    
    # Member joined a voice channel
    if before.channel is None and after.channel is not None:
        print(f"{member} joined {after.channel.name}")
        
        # Log join to a channel
        log_channel = nextcord.utils.get(member.guild.channels, name="voice-logs")
        if log_channel:
            embed = nextcord.Embed(
                title="🔊 Voice Channel Joined",
                description=f"{member.mention} joined {after.channel.mention}",
                color=nextcord.Color.green(),
                timestamp=datetime.now()
            )
            await log_channel.send(embed=embed)
    
    # Member left a voice channel
    elif before.channel is not None and after.channel is None:
        print(f"{member} left {before.channel.name}")
        
        # Check if bot should leave empty channel
        if before.channel and len(before.channel.members) == 1:
            # Only bot left in channel
            bot_member = before.channel.guild.me
            if bot_member in before.channel.members:
                voice_client = nextcord.utils.get(bot.voice_clients, guild=member.guild)
                if voice_client and voice_client.channel == before.channel:
                    await voice_client.disconnect()
                    print(f"Left {before.channel.name} - no other members")
        
        # Log leave to a channel
        log_channel = nextcord.utils.get(member.guild.channels, name="voice-logs")
        if log_channel:
            embed = nextcord.Embed(
                title="🔇 Voice Channel Left",
                description=f"{member.mention} left {before.channel.mention}",
                color=nextcord.Color.red(),
                timestamp=datetime.now()
            )
            await log_channel.send(embed=embed)
    
    # Member moved between channels
    elif before.channel != after.channel:
        print(f"{member} moved from {before.channel.name} to {after.channel.name}")
        
        # Follow the user if they're the only one listening
        voice_client = nextcord.utils.get(bot.voice_clients, guild=member.guild)
        if voice_client and voice_client.channel == before.channel:
            # Check if there are other non-bot members in the old channel
            human_members = [m for m in before.channel.members if not m.bot]
            if len(human_members) == 0 and member in after.channel.members:
                await voice_client.move_to(after.channel)
                print(f"Followed {member} to {after.channel.name}")
    
    # Member mute/unmute status changed
    if before.self_mute != after.self_mute:
        status = "muted" if after.self_mute else "unmuted"
        print(f"{member} {status} themselves")
    
    # Member deaf/undeaf status changed  
    if before.self_deaf != after.self_deaf:
        status = "deafened" if after.self_deaf else "undeafened"
        print(f"{member} {status} themselves")
    
    # Server mute/unmute
    if before.mute != after.mute:
        status = "server muted" if after.mute else "server unmuted"
        print(f"{member} was {status}")
    
    # Server deaf/undeaf
    if before.deaf != after.deaf:
        status = "server deafened" if after.deaf else "server undeafened"
        print(f"{member} was {status}")

# Voice channel management commands
@bot.command()
async def voice_info(ctx, member: nextcord.Member = None):
    """Get voice information about a member."""
    target = member or ctx.author
    
    if not target.voice:
        await ctx.send(f"❌ {target.display_name} is not in a voice channel.")
        return
    
    voice = target.voice
    channel = voice.channel
    
    embed = nextcord.Embed(
        title=f"🔊 Voice Info: {target.display_name}",
        color=nextcord.Color.blue()
    )
    
    embed.add_field(name="Channel", value=channel.mention, inline=True)
    embed.add_field(name="Channel Type", value=channel.type.name.title(), inline=True)
    embed.add_field(name="Members", value=len(channel.members), inline=True)
    
    if hasattr(channel, 'bitrate'):
        embed.add_field(name="Bitrate", value=f"{channel.bitrate // 1000} kbps", inline=True)
    
    if hasattr(channel, 'user_limit') and channel.user_limit:
        embed.add_field(name="User Limit", value=channel.user_limit, inline=True)
    
    # Voice state info
    states = []
    if voice.self_mute:
        states.append("🔇 Self Muted")
    if voice.self_deaf:
        states.append("🔇 Self Deafened")
    if voice.mute:
        states.append("🔇 Server Muted")
    if voice.deaf:
        states.append("🔇 Server Deafened")
    if voice.self_stream:
        states.append("📺 Streaming")
    if voice.self_video:
        states.append("📹 Video On")
    
    if states:
        embed.add_field(name="Status", value="\n".join(states), inline=False)
    
    await ctx.send(embed=embed)

@bot.command()
async def voice_stats(ctx):
    """Show voice channel statistics for the server."""
    voice_channels = [c for c in ctx.guild.channels if isinstance(c, nextcord.VoiceChannel)]
    
    if not voice_channels:
        await ctx.send("❌ This server has no voice channels.")
        return
    
    embed = nextcord.Embed(
        title=f"🔊 Voice Statistics - {ctx.guild.name}",
        color=nextcord.Color.blue()
    )
    
    total_members = 0
    active_channels = 0
    
    channel_info = []
    for channel in voice_channels:
        member_count = len(channel.members)
        total_members += member_count
        
        if member_count > 0:
            active_channels += 1
            # Show members in channel
            member_names = [m.display_name for m in channel.members[:5]]  # Show first 5
            member_text = ", ".join(member_names)
            if len(channel.members) > 5:
                member_text += f" and {len(channel.members) - 5} more"
            
            channel_info.append(f"🔊 **{channel.name}** ({member_count})\n{member_text}")
    
    embed.add_field(
        name="Overview",
        value=f"**Total Channels:** {len(voice_channels)}\n"
              f"**Active Channels:** {active_channels}\n"
              f"**Total Members:** {total_members}",
        inline=False
    )
    
    if channel_info:
        # Show first 5 active channels
        embed.add_field(
            name="Active Channels",
            value="\n\n".join(channel_info[:5]) + 
                  (f"\n\n... and {len(channel_info) - 5} more" if len(channel_info) > 5 else ""),
            inline=False
        )
    
    await ctx.send(embed=embed)

# Voice channel moderation
@bot.command()
async def voice_kick(ctx, member: nextcord.Member, *, reason: str = None):
    """Kick a member from their voice channel."""
    if not ctx.author.guild_permissions.move_members:
        await ctx.send("❌ You don't have permission to move members.")
        return
    
    if not member.voice or not member.voice.channel:
        await ctx.send(f"❌ {member.mention} is not in a voice channel.")
        return
    
    try:
        channel_name = member.voice.channel.name
        await member.move_to(None, reason=reason or f"Voice kicked by {ctx.author}")
        
        embed = nextcord.Embed(
            title="🔇 Voice Kick",
            description=f"{member.mention} has been disconnected from {channel_name}",
            color=nextcord.Color.orange()
        )
        
        if reason:
            embed.add_field(name="Reason", value=reason, inline=False)
        
        embed.set_footer(text=f"Action by {ctx.author}", icon_url=ctx.author.display_avatar.url)
        
        await ctx.send(embed=embed)
        
    except nextcord.Forbidden:
        await ctx.send("❌ I don't have permission to disconnect this member.")
    except nextcord.HTTPException as e:
        await ctx.send(f"❌ Failed to disconnect member: {e}")

This comprehensive documentation covers all aspects of nextcord's voice and audio capabilities, providing developers with the tools needed to create sophisticated music bots and voice-enabled Discord applications.

Install with Tessl CLI

npx tessl i tessl/pypi-nextcord

docs

application-commands.md

channels.md

client.md

commands.md

errors.md

events.md

guild.md

index.md

messages.md

permissions.md

tasks.md

ui.md

users.md

utilities.md

voice.md

webhooks.md

tile.json