or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

application-commands.mdchannels.mdclient.mdcommands.mderrors.mdevents.mdguild.mdindex.mdmessages.mdpermissions.mdtasks.mdui.mdusers.mdutilities.mdvoice.mdwebhooks.md

voice.mddocs/

0

# Nextcord Voice and Audio

1

2

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

3

4

## Voice Client

5

6

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

7

8

### VoiceClient Class { .api }

9

10

```python

11

import nextcord

12

from nextcord import VoiceClient, AudioSource, PCMVolumeTransformer

13

from typing import Optional, Callable, Any

14

import asyncio

15

16

class VoiceClient:

17

"""Represents a Discord voice connection.

18

19

This is used to manage voice connections and audio playback in Discord voice channels.

20

21

Attributes

22

----------

23

token: str

24

The voice connection token.

25

guild: Guild

26

The guild this voice client is connected to.

27

channel: Optional[VoiceChannel]

28

The voice channel this client is connected to.

29

endpoint: str

30

The voice server endpoint.

31

endpoint_ip: str

32

The voice server IP address.

33

port: int

34

The voice server port.

35

ssrc: int

36

The synchronization source identifier.

37

secret_key: bytes

38

The secret key for voice encryption.

39

sequence: int

40

The current sequence number.

41

timestamp: int

42

The current timestamp.

43

mode: str

44

The voice encryption mode.

45

user: ClientUser

46

The bot user associated with this voice client.

47

"""

48

49

@property

50

def latency(self) -> float:

51

"""float: The latency of the voice connection in seconds."""

52

...

53

54

@property

55

def average_latency(self) -> float:

56

"""float: The average latency over the last 20 heartbeats."""

57

...

58

59

def is_connected(self) -> bool:

60

"""bool: Whether the voice client is connected to a voice channel."""

61

...

62

63

def is_playing(self) -> bool:

64

"""bool: Whether audio is currently being played."""

65

...

66

67

def is_paused(self) -> bool:

68

"""bool: Whether audio playback is currently paused."""

69

...

70

71

async def connect(

72

self,

73

*,

74

timeout: float = 60.0,

75

reconnect: bool = True,

76

self_deaf: bool = False,

77

self_mute: bool = False

78

) -> None:

79

"""Connect to the voice channel.

80

81

Parameters

82

----------

83

timeout: float

84

The timeout for connecting to voice.

85

reconnect: bool

86

Whether to reconnect on disconnection.

87

self_deaf: bool

88

Whether to deafen the bot upon connection.

89

self_mute: bool

90

Whether to mute the bot upon connection.

91

"""

92

...

93

94

async def disconnect(self, *, force: bool = False) -> None:

95

"""Disconnect from the voice channel.

96

97

Parameters

98

----------

99

force: bool

100

Whether to forcefully disconnect even if audio is playing.

101

"""

102

...

103

104

async def move_to(self, channel: nextcord.VoiceChannel) -> None:

105

"""Move the voice client to a different voice channel.

106

107

Parameters

108

----------

109

channel: nextcord.VoiceChannel

110

The voice channel to move to.

111

"""

112

...

113

114

def play(

115

self,

116

source: AudioSource,

117

*,

118

after: Optional[Callable[[Optional[Exception]], Any]] = None

119

) -> None:

120

"""Play an audio source.

121

122

Parameters

123

----------

124

source: AudioSource

125

The audio source to play.

126

after: Optional[Callable]

127

A callback function called after the audio finishes playing.

128

"""

129

...

130

131

def stop(self) -> None:

132

"""Stop the currently playing audio."""

133

...

134

135

def pause(self) -> None:

136

"""Pause the currently playing audio."""

137

...

138

139

def resume(self) -> None:

140

"""Resume the currently paused audio."""

141

...

142

143

@property

144

def source(self) -> Optional[AudioSource]:

145

"""Optional[AudioSource]: The currently playing audio source."""

146

...

147

148

# Basic voice connection example

149

@bot.command()

150

async def join(ctx):

151

"""Join the user's voice channel."""

152

if not ctx.author.voice:

153

await ctx.send("❌ You are not connected to a voice channel.")

154

return

155

156

channel = ctx.author.voice.channel

157

158

if ctx.voice_client is not None:

159

# Already connected, move to new channel

160

await ctx.voice_client.move_to(channel)

161

await ctx.send(f"πŸ”Š Moved to {channel.name}")

162

else:

163

# Connect to the channel

164

voice_client = await channel.connect()

165

await ctx.send(f"πŸ”Š Connected to {channel.name}")

166

167

@bot.command()

168

async def leave(ctx):

169

"""Leave the voice channel."""

170

if ctx.voice_client is None:

171

await ctx.send("❌ Not connected to a voice channel.")

172

return

173

174

await ctx.voice_client.disconnect()

175

await ctx.send("πŸ‘‹ Disconnected from voice channel.")

176

177

# Voice channel connection with error handling

178

async def connect_to_voice_channel(

179

channel: nextcord.VoiceChannel,

180

timeout: float = 10.0

181

) -> Optional[VoiceClient]:

182

"""Connect to a voice channel with proper error handling."""

183

try:

184

voice_client = await channel.connect(timeout=timeout)

185

print(f"Connected to {channel.name} in {channel.guild.name}")

186

return voice_client

187

188

except asyncio.TimeoutError:

189

print(f"Timeout connecting to {channel.name}")

190

return None

191

192

except nextcord.ClientException as e:

193

print(f"Already connected to voice: {e}")

194

return None

195

196

except nextcord.Forbidden:

197

print(f"No permission to connect to {channel.name}")

198

return None

199

200

except Exception as e:

201

print(f"Unexpected error connecting to voice: {e}")

202

return None

203

```

204

205

## Audio Sources

206

207

Audio source classes for playing various types of audio content.

208

209

### AudioSource Classes { .api }

210

211

```python

212

class AudioSource:

213

"""Base class for audio sources.

214

215

All audio sources must inherit from this class and implement the read method.

216

"""

217

218

def read(self) -> bytes:

219

"""Read audio data.

220

221

Returns

222

-------

223

bytes

224

20ms of audio data in PCM format, or empty bytes to signal end.

225

"""

226

...

227

228

def cleanup(self) -> None:

229

"""Clean up any resources used by the audio source."""

230

...

231

232

def is_opus(self) -> bool:

233

"""bool: Whether this source provides Opus-encoded audio."""

234

return False

235

236

class FFmpegPCMAudio(AudioSource):

237

"""An audio source that uses FFmpeg to convert audio to PCM.

238

239

This is the most common audio source for playing files or streams.

240

"""

241

242

def __init__(

243

self,

244

source: str,

245

*,

246

executable: str = 'ffmpeg',

247

pipe: bool = False,

248

stderr: Optional[Any] = None,

249

before_options: Optional[str] = None,

250

options: Optional[str] = None

251

):

252

"""Initialize FFmpeg PCM audio source.

253

254

Parameters

255

----------

256

source: str

257

The audio source (file path or URL).

258

executable: str

259

The FFmpeg executable path.

260

pipe: bool

261

Whether to pipe the audio through stdin.

262

stderr: Optional[Any]

263

Where to redirect stderr output.

264

before_options: Optional[str]

265

FFmpeg options to use before the input source.

266

options: Optional[str]

267

FFmpeg options to use after the input source.

268

"""

269

...

270

271

class FFmpegOpusAudio(AudioSource):

272

"""An audio source that uses FFmpeg to provide Opus-encoded audio.

273

274

This is more efficient than PCM audio as it doesn't require re-encoding.

275

"""

276

277

def __init__(

278

self,

279

source: str,

280

*,

281

bitrate: int = 128,

282

**kwargs

283

):

284

"""Initialize FFmpeg Opus audio source.

285

286

Parameters

287

----------

288

source: str

289

The audio source (file path or URL).

290

bitrate: int

291

The audio bitrate in kbps.

292

**kwargs

293

Additional arguments passed to FFmpegPCMAudio.

294

"""

295

...

296

297

def is_opus(self) -> bool:

298

"""bool: Always returns True for Opus sources."""

299

return True

300

301

class PCMVolumeTransformer(AudioSource):

302

"""A volume transformer for PCM audio sources.

303

304

This allows you to control the volume of audio playback.

305

"""

306

307

def __init__(self, original: AudioSource, volume: float = 0.5):

308

"""Initialize volume transformer.

309

310

Parameters

311

----------

312

original: AudioSource

313

The original audio source to transform.

314

volume: float

315

The volume level (0.0 to 1.0).

316

"""

317

...

318

319

@property

320

def volume(self) -> float:

321

"""float: The current volume level."""

322

...

323

324

@volume.setter

325

def volume(self, value: float) -> None:

326

"""Set the volume level.

327

328

Parameters

329

----------

330

value: float

331

The volume level (0.0 to 1.0).

332

"""

333

...

334

335

# Audio source examples

336

@bot.command()

337

async def play_file(ctx, *, filename: str):

338

"""Play an audio file."""

339

if not ctx.voice_client:

340

await ctx.send("❌ Not connected to a voice channel. Use `!join` first.")

341

return

342

343

try:

344

# Create audio source from file

345

source = nextcord.FFmpegPCMAudio(filename)

346

347

# Play the audio

348

ctx.voice_client.play(source, after=lambda e: print(f'Player error: {e}') if e else None)

349

350

await ctx.send(f"🎡 Now playing: {filename}")

351

352

except Exception as e:

353

await ctx.send(f"❌ Error playing file: {e}")

354

355

@bot.command()

356

async def play_url(ctx, *, url: str):

357

"""Play audio from a URL."""

358

if not ctx.voice_client:

359

await ctx.send("❌ Not connected to a voice channel.")

360

return

361

362

try:

363

# Use FFmpeg to stream from URL

364

# Common options for streaming

365

ffmpeg_options = {

366

'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',

367

'options': '-vn' # Disable video

368

}

369

370

source = nextcord.FFmpegPCMAudio(url, **ffmpeg_options)

371

372

ctx.voice_client.play(source)

373

await ctx.send(f"🎡 Now playing from URL")

374

375

except Exception as e:

376

await ctx.send(f"❌ Error playing from URL: {e}")

377

378

@bot.command()

379

async def play_with_volume(ctx, volume: float, *, filename: str):

380

"""Play an audio file with specified volume."""

381

if not ctx.voice_client:

382

await ctx.send("❌ Not connected to a voice channel.")

383

return

384

385

if not 0.0 <= volume <= 1.0:

386

await ctx.send("❌ Volume must be between 0.0 and 1.0")

387

return

388

389

try:

390

# Create audio source with volume control

391

original_source = nextcord.FFmpegPCMAudio(filename)

392

source = nextcord.PCMVolumeTransformer(original_source, volume=volume)

393

394

ctx.voice_client.play(source)

395

await ctx.send(f"🎡 Now playing {filename} at {volume:.0%} volume")

396

397

except Exception as e:

398

await ctx.send(f"❌ Error: {e}")

399

400

@bot.command()

401

async def volume(ctx, new_volume: float):

402

"""Change the volume of currently playing audio."""

403

if not ctx.voice_client or not ctx.voice_client.source:

404

await ctx.send("❌ Nothing is currently playing.")

405

return

406

407

if not isinstance(ctx.voice_client.source, nextcord.PCMVolumeTransformer):

408

await ctx.send("❌ Current audio source doesn't support volume control.")

409

return

410

411

if not 0.0 <= new_volume <= 1.0:

412

await ctx.send("❌ Volume must be between 0.0 and 1.0")

413

return

414

415

ctx.voice_client.source.volume = new_volume

416

await ctx.send(f"πŸ”Š Volume set to {new_volume:.0%}")

417

```

418

419

## Music Bot Implementation

420

421

Complete music bot implementation with queue management and playback controls.

422

423

### Music Bot System { .api }

424

425

```python

426

import asyncio

427

import youtube_dl

428

from collections import deque

429

from typing import Dict, List, Optional, Any

430

431

class Song:

432

"""Represents a song in the music queue."""

433

434

def __init__(

435

self,

436

title: str,

437

url: str,

438

duration: Optional[int] = None,

439

thumbnail: Optional[str] = None,

440

requester: Optional[nextcord.Member] = None

441

):

442

self.title = title

443

self.url = url

444

self.duration = duration

445

self.thumbnail = thumbnail

446

self.requester = requester

447

448

def __str__(self) -> str:

449

return self.title

450

451

@property

452

def duration_formatted(self) -> str:

453

"""Get formatted duration string."""

454

if not self.duration:

455

return "Unknown"

456

457

minutes, seconds = divmod(self.duration, 60)

458

hours, minutes = divmod(minutes, 60)

459

460

if hours:

461

return f"{hours:02d}:{minutes:02d}:{seconds:02d}"

462

else:

463

return f"{minutes:02d}:{seconds:02d}"

464

465

class MusicQueue:

466

"""Manages a music queue for a guild."""

467

468

def __init__(self):

469

self.queue = deque()

470

self.current_song = None

471

self.loop_mode = "off" # "off", "song", "queue"

472

self.shuffle = False

473

474

def add(self, song: Song) -> None:

475

"""Add a song to the queue."""

476

self.queue.append(song)

477

478

def next(self) -> Optional[Song]:

479

"""Get the next song from the queue."""

480

if self.loop_mode == "song" and self.current_song:

481

return self.current_song

482

483

if not self.queue:

484

if self.loop_mode == "queue" and self.current_song:

485

# Add current song back to queue for looping

486

self.queue.append(self.current_song)

487

else:

488

return None

489

490

if self.shuffle:

491

import random

492

song = random.choice(self.queue)

493

self.queue.remove(song)

494

else:

495

song = self.queue.popleft()

496

497

self.current_song = song

498

return song

499

500

def clear(self) -> int:

501

"""Clear the queue and return number of songs removed."""

502

count = len(self.queue)

503

self.queue.clear()

504

return count

505

506

def remove(self, index: int) -> Optional[Song]:

507

"""Remove a song by index."""

508

try:

509

return self.queue.popleft() if index == 0 else self.queue.remove(list(self.queue)[index])

510

except (IndexError, ValueError):

511

return None

512

513

def get_queue_list(self, limit: int = 10) -> List[Song]:

514

"""Get a list of songs in the queue."""

515

return list(self.queue)[:limit]

516

517

class YouTubeSource:

518

"""YouTube audio source with metadata extraction."""

519

520

ytdl_format_options = {

521

'format': 'bestaudio/best',

522

'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',

523

'restrictfilenames': True,

524

'noplaylist': True,

525

'nocheckcertificate': True,

526

'ignoreerrors': False,

527

'logtostderr': False,

528

'quiet': True,

529

'no_warnings': True,

530

'default_search': 'auto',

531

'source_address': '0.0.0.0'

532

}

533

534

ffmpeg_options = {

535

'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',

536

'options': '-vn'

537

}

538

539

ytdl = youtube_dl.YoutubeDL(ytdl_format_options)

540

541

@classmethod

542

async def from_url(

543

cls,

544

url: str,

545

*,

546

loop: Optional[asyncio.AbstractEventLoop] = None,

547

stream: bool = False

548

) -> Dict[str, Any]:

549

"""Extract song information from YouTube URL."""

550

loop = loop or asyncio.get_event_loop()

551

552

# Run youtube-dl extraction in thread pool

553

data = await loop.run_in_executor(

554

None,

555

lambda: cls.ytdl.extract_info(url, download=not stream)

556

)

557

558

if 'entries' in data:

559

# Playlist - take first item

560

data = data['entries'][0]

561

562

filename = data['url'] if stream else cls.ytdl.prepare_filename(data)

563

564

return {

565

'title': data.get('title'),

566

'url': data.get('webpage_url'),

567

'stream_url': data.get('url'),

568

'duration': data.get('duration'),

569

'thumbnail': data.get('thumbnail'),

570

'filename': filename

571

}

572

573

@classmethod

574

def get_audio_source(cls, data: Dict[str, Any]) -> nextcord.AudioSource:

575

"""Get audio source from extracted data."""

576

return nextcord.FFmpegPCMAudio(data['filename'], **cls.ffmpeg_options)

577

578

class MusicBot:

579

"""Complete music bot implementation."""

580

581

def __init__(self, bot: commands.Bot):

582

self.bot = bot

583

self.guilds: Dict[int, MusicQueue] = {}

584

585

def get_queue(self, guild_id: int) -> MusicQueue:

586

"""Get or create a music queue for a guild."""

587

if guild_id not in self.guilds:

588

self.guilds[guild_id] = MusicQueue()

589

return self.guilds[guild_id]

590

591

async def play_next(self, ctx: commands.Context) -> None:

592

"""Play the next song in the queue."""

593

queue = self.get_queue(ctx.guild.id)

594

595

if not ctx.voice_client:

596

return

597

598

next_song = queue.next()

599

if not next_song:

600

# Queue is empty

601

embed = nextcord.Embed(

602

title="🎡 Queue Finished",

603

description="No more songs in the queue.",

604

color=nextcord.Color.blue()

605

)

606

await ctx.send(embed=embed)

607

return

608

609

try:

610

# Extract audio source

611

data = await YouTubeSource.from_url(next_song.url, stream=True)

612

source = YouTubeSource.get_audio_source(data)

613

614

# Play the song

615

ctx.voice_client.play(

616

source,

617

after=lambda e: self.bot.loop.create_task(self.play_next(ctx)) if not e else print(f'Player error: {e}')

618

)

619

620

# Send now playing message

621

embed = nextcord.Embed(

622

title="🎡 Now Playing",

623

description=f"**{next_song.title}**",

624

color=nextcord.Color.green()

625

)

626

627

if next_song.duration:

628

embed.add_field(name="Duration", value=next_song.duration_formatted, inline=True)

629

630

if next_song.requester:

631

embed.add_field(name="Requested by", value=next_song.requester.mention, inline=True)

632

633

if next_song.thumbnail:

634

embed.set_thumbnail(url=next_song.thumbnail)

635

636

await ctx.send(embed=embed)

637

638

except Exception as e:

639

await ctx.send(f"❌ Error playing {next_song.title}: {e}")

640

# Try to play next song

641

await self.play_next(ctx)

642

643

# Music bot commands

644

music_bot = MusicBot(bot)

645

646

@bot.command(aliases=['p'])

647

async def play(ctx, *, search: str):

648

"""Play a song from YouTube."""

649

if not ctx.author.voice:

650

await ctx.send("❌ You must be in a voice channel to use this command.")

651

return

652

653

# Connect to voice if not already connected

654

if not ctx.voice_client:

655

await ctx.author.voice.channel.connect()

656

elif ctx.voice_client.channel != ctx.author.voice.channel:

657

await ctx.voice_client.move_to(ctx.author.voice.channel)

658

659

# Show processing message

660

processing_msg = await ctx.send("πŸ” Searching...")

661

662

try:

663

# Extract song information

664

data = await YouTubeSource.from_url(search, loop=bot.loop, stream=True)

665

666

# Create song object

667

song = Song(

668

title=data['title'],

669

url=data['url'],

670

duration=data['duration'],

671

thumbnail=data['thumbnail'],

672

requester=ctx.author

673

)

674

675

# Add to queue

676

queue = music_bot.get_queue(ctx.guild.id)

677

queue.add(song)

678

679

# Edit processing message

680

if ctx.voice_client.is_playing():

681

embed = nextcord.Embed(

682

title="πŸ“ Added to Queue",

683

description=f"**{song.title}**",

684

color=nextcord.Color.blue()

685

)

686

embed.add_field(name="Position in queue", value=len(queue.queue), inline=True)

687

embed.add_field(name="Duration", value=song.duration_formatted, inline=True)

688

embed.add_field(name="Requested by", value=ctx.author.mention, inline=True)

689

690

if song.thumbnail:

691

embed.set_thumbnail(url=song.thumbnail)

692

693

await processing_msg.edit(content=None, embed=embed)

694

else:

695

# Start playing immediately

696

await processing_msg.delete()

697

await music_bot.play_next(ctx)

698

699

except Exception as e:

700

await processing_msg.edit(content=f"❌ Error: {e}")

701

702

@bot.command()

703

async def skip(ctx):

704

"""Skip the current song."""

705

if not ctx.voice_client or not ctx.voice_client.is_playing():

706

await ctx.send("❌ Nothing is currently playing.")

707

return

708

709

queue = music_bot.get_queue(ctx.guild.id)

710

skipped_song = queue.current_song

711

712

ctx.voice_client.stop() # This will trigger play_next

713

714

embed = nextcord.Embed(

715

title="⏭️ Song Skipped",

716

description=f"Skipped **{skipped_song.title if skipped_song else 'Unknown'}**",

717

color=nextcord.Color.orange()

718

)

719

await ctx.send(embed=embed)

720

721

@bot.command()

722

async def queue(ctx, page: int = 1):

723

"""Show the music queue."""

724

music_queue = music_bot.get_queue(ctx.guild.id)

725

726

if not music_queue.current_song and not music_queue.queue:

727

await ctx.send("❌ The queue is empty.")

728

return

729

730

embed = nextcord.Embed(

731

title="🎡 Music Queue",

732

color=nextcord.Color.blue()

733

)

734

735

# Current song

736

if music_queue.current_song:

737

embed.add_field(

738

name="🎡 Now Playing",

739

value=f"**{music_queue.current_song.title}**\nRequested by {music_queue.current_song.requester.mention if music_queue.current_song.requester else 'Unknown'}",

740

inline=False

741

)

742

743

# Queue

744

queue_list = music_queue.get_queue_list(10)

745

if queue_list:

746

queue_text = []

747

for i, song in enumerate(queue_list, 1):

748

queue_text.append(f"{i}. **{song.title}** ({song.duration_formatted})")

749

750

embed.add_field(

751

name=f"πŸ“ Up Next ({len(music_queue.queue)} songs)",

752

value="\n".join(queue_text),

753

inline=False

754

)

755

756

# Queue stats

757

if music_queue.queue:

758

total_duration = sum(song.duration for song in music_queue.queue if song.duration)

759

hours, remainder = divmod(total_duration, 3600)

760

minutes, _ = divmod(remainder, 60)

761

762

embed.set_footer(text=f"Total queue time: {hours}h {minutes}m | Loop: {music_queue.loop_mode}")

763

764

await ctx.send(embed=embed)

765

766

@bot.command()

767

async def pause(ctx):

768

"""Pause the current song."""

769

if not ctx.voice_client or not ctx.voice_client.is_playing():

770

await ctx.send("❌ Nothing is currently playing.")

771

return

772

773

ctx.voice_client.pause()

774

await ctx.send("⏸️ Paused the music.")

775

776

@bot.command()

777

async def resume(ctx):

778

"""Resume the paused song."""

779

if not ctx.voice_client or not ctx.voice_client.is_paused():

780

await ctx.send("❌ Nothing is currently paused.")

781

return

782

783

ctx.voice_client.resume()

784

await ctx.send("▢️ Resumed the music.")

785

786

@bot.command()

787

async def stop(ctx):

788

"""Stop the music and clear the queue."""

789

if not ctx.voice_client:

790

await ctx.send("❌ Not connected to a voice channel.")

791

return

792

793

queue = music_bot.get_queue(ctx.guild.id)

794

cleared_count = queue.clear()

795

queue.current_song = None

796

797

ctx.voice_client.stop()

798

799

embed = nextcord.Embed(

800

title="⏹️ Music Stopped",

801

description=f"Cleared {cleared_count} songs from the queue.",

802

color=nextcord.Color.red()

803

)

804

await ctx.send(embed=embed)

805

806

@bot.command()

807

async def loop(ctx, mode: str = None):

808

"""Set loop mode (off, song, queue)."""

809

queue = music_bot.get_queue(ctx.guild.id)

810

811

if mode is None:

812

await ctx.send(f"Current loop mode: **{queue.loop_mode}**")

813

return

814

815

if mode.lower() not in ['off', 'song', 'queue']:

816

await ctx.send("❌ Invalid loop mode. Use: `off`, `song`, or `queue`")

817

return

818

819

queue.loop_mode = mode.lower()

820

821

loop_emojis = {

822

'off': '❌',

823

'song': 'πŸ”‚',

824

'queue': 'πŸ”'

825

}

826

827

await ctx.send(f"{loop_emojis[mode.lower()]} Loop mode set to: **{mode}**")

828

829

@bot.command()

830

async def shuffle(ctx):

831

"""Toggle shuffle mode."""

832

queue = music_bot.get_queue(ctx.guild.id)

833

queue.shuffle = not queue.shuffle

834

835

status = "enabled" if queue.shuffle else "disabled"

836

emoji = "πŸ”€" if queue.shuffle else "➑️"

837

838

await ctx.send(f"{emoji} Shuffle {status}")

839

840

@bot.command()

841

async def nowplaying(ctx):

842

"""Show information about the currently playing song."""

843

queue = music_bot.get_queue(ctx.guild.id)

844

845

if not ctx.voice_client or not queue.current_song:

846

await ctx.send("❌ Nothing is currently playing.")

847

return

848

849

song = queue.current_song

850

851

embed = nextcord.Embed(

852

title="🎡 Now Playing",

853

description=f"**{song.title}**",

854

color=nextcord.Color.green(),

855

url=song.url

856

)

857

858

if song.duration:

859

embed.add_field(name="Duration", value=song.duration_formatted, inline=True)

860

861

if song.requester:

862

embed.add_field(name="Requested by", value=song.requester.mention, inline=True)

863

864

embed.add_field(name="Status", value="Playing ▢️" if ctx.voice_client.is_playing() else "Paused ⏸️", inline=True)

865

866

if song.thumbnail:

867

embed.set_thumbnail(url=song.thumbnail)

868

869

# Queue info

870

if queue.queue:

871

embed.add_field(name="Next in queue", value=f"{len(queue.queue)} songs", inline=True)

872

873

embed.add_field(name="Loop", value=queue.loop_mode.title(), inline=True)

874

embed.add_field(name="Shuffle", value="On" if queue.shuffle else "Off", inline=True)

875

876

await ctx.send(embed=embed)

877

```

878

879

## Voice Events and State Management

880

881

Voice state tracking and event handling for advanced voice features.

882

883

### Voice State Events { .api }

884

885

```python

886

# Voice state event handlers

887

@bot.event

888

async def on_voice_state_update(

889

member: nextcord.Member,

890

before: nextcord.VoiceState,

891

after: nextcord.VoiceState

892

):

893

"""Handle voice state updates."""

894

895

# Member joined a voice channel

896

if before.channel is None and after.channel is not None:

897

print(f"{member} joined {after.channel.name}")

898

899

# Log join to a channel

900

log_channel = nextcord.utils.get(member.guild.channels, name="voice-logs")

901

if log_channel:

902

embed = nextcord.Embed(

903

title="πŸ”Š Voice Channel Joined",

904

description=f"{member.mention} joined {after.channel.mention}",

905

color=nextcord.Color.green(),

906

timestamp=datetime.now()

907

)

908

await log_channel.send(embed=embed)

909

910

# Member left a voice channel

911

elif before.channel is not None and after.channel is None:

912

print(f"{member} left {before.channel.name}")

913

914

# Check if bot should leave empty channel

915

if before.channel and len(before.channel.members) == 1:

916

# Only bot left in channel

917

bot_member = before.channel.guild.me

918

if bot_member in before.channel.members:

919

voice_client = nextcord.utils.get(bot.voice_clients, guild=member.guild)

920

if voice_client and voice_client.channel == before.channel:

921

await voice_client.disconnect()

922

print(f"Left {before.channel.name} - no other members")

923

924

# Log leave to a channel

925

log_channel = nextcord.utils.get(member.guild.channels, name="voice-logs")

926

if log_channel:

927

embed = nextcord.Embed(

928

title="πŸ”‡ Voice Channel Left",

929

description=f"{member.mention} left {before.channel.mention}",

930

color=nextcord.Color.red(),

931

timestamp=datetime.now()

932

)

933

await log_channel.send(embed=embed)

934

935

# Member moved between channels

936

elif before.channel != after.channel:

937

print(f"{member} moved from {before.channel.name} to {after.channel.name}")

938

939

# Follow the user if they're the only one listening

940

voice_client = nextcord.utils.get(bot.voice_clients, guild=member.guild)

941

if voice_client and voice_client.channel == before.channel:

942

# Check if there are other non-bot members in the old channel

943

human_members = [m for m in before.channel.members if not m.bot]

944

if len(human_members) == 0 and member in after.channel.members:

945

await voice_client.move_to(after.channel)

946

print(f"Followed {member} to {after.channel.name}")

947

948

# Member mute/unmute status changed

949

if before.self_mute != after.self_mute:

950

status = "muted" if after.self_mute else "unmuted"

951

print(f"{member} {status} themselves")

952

953

# Member deaf/undeaf status changed

954

if before.self_deaf != after.self_deaf:

955

status = "deafened" if after.self_deaf else "undeafened"

956

print(f"{member} {status} themselves")

957

958

# Server mute/unmute

959

if before.mute != after.mute:

960

status = "server muted" if after.mute else "server unmuted"

961

print(f"{member} was {status}")

962

963

# Server deaf/undeaf

964

if before.deaf != after.deaf:

965

status = "server deafened" if after.deaf else "server undeafened"

966

print(f"{member} was {status}")

967

968

# Voice channel management commands

969

@bot.command()

970

async def voice_info(ctx, member: nextcord.Member = None):

971

"""Get voice information about a member."""

972

target = member or ctx.author

973

974

if not target.voice:

975

await ctx.send(f"❌ {target.display_name} is not in a voice channel.")

976

return

977

978

voice = target.voice

979

channel = voice.channel

980

981

embed = nextcord.Embed(

982

title=f"πŸ”Š Voice Info: {target.display_name}",

983

color=nextcord.Color.blue()

984

)

985

986

embed.add_field(name="Channel", value=channel.mention, inline=True)

987

embed.add_field(name="Channel Type", value=channel.type.name.title(), inline=True)

988

embed.add_field(name="Members", value=len(channel.members), inline=True)

989

990

if hasattr(channel, 'bitrate'):

991

embed.add_field(name="Bitrate", value=f"{channel.bitrate // 1000} kbps", inline=True)

992

993

if hasattr(channel, 'user_limit') and channel.user_limit:

994

embed.add_field(name="User Limit", value=channel.user_limit, inline=True)

995

996

# Voice state info

997

states = []

998

if voice.self_mute:

999

states.append("πŸ”‡ Self Muted")

1000

if voice.self_deaf:

1001

states.append("πŸ”‡ Self Deafened")

1002

if voice.mute:

1003

states.append("πŸ”‡ Server Muted")

1004

if voice.deaf:

1005

states.append("πŸ”‡ Server Deafened")

1006

if voice.self_stream:

1007

states.append("πŸ“Ί Streaming")

1008

if voice.self_video:

1009

states.append("πŸ“Ή Video On")

1010

1011

if states:

1012

embed.add_field(name="Status", value="\n".join(states), inline=False)

1013

1014

await ctx.send(embed=embed)

1015

1016

@bot.command()

1017

async def voice_stats(ctx):

1018

"""Show voice channel statistics for the server."""

1019

voice_channels = [c for c in ctx.guild.channels if isinstance(c, nextcord.VoiceChannel)]

1020

1021

if not voice_channels:

1022

await ctx.send("❌ This server has no voice channels.")

1023

return

1024

1025

embed = nextcord.Embed(

1026

title=f"πŸ”Š Voice Statistics - {ctx.guild.name}",

1027

color=nextcord.Color.blue()

1028

)

1029

1030

total_members = 0

1031

active_channels = 0

1032

1033

channel_info = []

1034

for channel in voice_channels:

1035

member_count = len(channel.members)

1036

total_members += member_count

1037

1038

if member_count > 0:

1039

active_channels += 1

1040

# Show members in channel

1041

member_names = [m.display_name for m in channel.members[:5]] # Show first 5

1042

member_text = ", ".join(member_names)

1043

if len(channel.members) > 5:

1044

member_text += f" and {len(channel.members) - 5} more"

1045

1046

channel_info.append(f"πŸ”Š **{channel.name}** ({member_count})\n{member_text}")

1047

1048

embed.add_field(

1049

name="Overview",

1050

value=f"**Total Channels:** {len(voice_channels)}\n"

1051

f"**Active Channels:** {active_channels}\n"

1052

f"**Total Members:** {total_members}",

1053

inline=False

1054

)

1055

1056

if channel_info:

1057

# Show first 5 active channels

1058

embed.add_field(

1059

name="Active Channels",

1060

value="\n\n".join(channel_info[:5]) +

1061

(f"\n\n... and {len(channel_info) - 5} more" if len(channel_info) > 5 else ""),

1062

inline=False

1063

)

1064

1065

await ctx.send(embed=embed)

1066

1067

# Voice channel moderation

1068

@bot.command()

1069

async def voice_kick(ctx, member: nextcord.Member, *, reason: str = None):

1070

"""Kick a member from their voice channel."""

1071

if not ctx.author.guild_permissions.move_members:

1072

await ctx.send("❌ You don't have permission to move members.")

1073

return

1074

1075

if not member.voice or not member.voice.channel:

1076

await ctx.send(f"❌ {member.mention} is not in a voice channel.")

1077

return

1078

1079

try:

1080

channel_name = member.voice.channel.name

1081

await member.move_to(None, reason=reason or f"Voice kicked by {ctx.author}")

1082

1083

embed = nextcord.Embed(

1084

title="πŸ”‡ Voice Kick",

1085

description=f"{member.mention} has been disconnected from {channel_name}",

1086

color=nextcord.Color.orange()

1087

)

1088

1089

if reason:

1090

embed.add_field(name="Reason", value=reason, inline=False)

1091

1092

embed.set_footer(text=f"Action by {ctx.author}", icon_url=ctx.author.display_avatar.url)

1093

1094

await ctx.send(embed=embed)

1095

1096

except nextcord.Forbidden:

1097

await ctx.send("❌ I don't have permission to disconnect this member.")

1098

except nextcord.HTTPException as e:

1099

await ctx.send(f"❌ Failed to disconnect member: {e}")

1100

```

1101

1102

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.