or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

application-commands.mdautomod.mdchannels-messaging.mdclient-bot.mdcommand-framework.mderror-handling.mdevents-gateway.mdguild-management.mdindex.mdinteractions-ui.mdlocalization-i18n.mdpermissions-security.mdpolls.mdusers-members.mdvoice-audio.md

voice-audio.mddocs/

0

# Voice and Audio

1

2

Voice channel connection, audio streaming, and voice-related functionality for music bots and voice applications with comprehensive audio handling, connection management, and voice client operations for Discord voice features.

3

4

## Capabilities

5

6

### Voice Connections

7

8

Voice channel connection and management for audio streaming and voice functionality.

9

10

```python { .api }

11

class VoiceClient:

12

"""Voice connection to a Discord voice channel."""

13

14

def __init__(self, client: Client, channel: VoiceChannel):

15

"""

16

Initialize voice client.

17

18

Parameters:

19

- client: Bot client

20

- channel: Voice channel to connect to

21

"""

22

23

channel: VoiceChannel

24

guild: Guild

25

user: ClientUser

26

session_id: str

27

token: str

28

endpoint: str

29

socket: VoiceWebSocket

30

loop: asyncio.AbstractEventLoop

31

_runner: asyncio.Task

32

_player: Optional[AudioPlayer]

33

34

@property

35

def latency(self) -> float:

36

"""Voice connection latency in seconds."""

37

38

@property

39

def average_latency(self) -> float:

40

"""Average voice connection latency."""

41

42

def is_connected(self) -> bool:

43

"""

44

Check if voice client is connected.

45

46

Returns:

47

True if connected to voice

48

"""

49

50

def is_playing(self) -> bool:

51

"""

52

Check if audio is currently playing.

53

54

Returns:

55

True if audio is playing

56

"""

57

58

def is_paused(self) -> bool:

59

"""

60

Check if audio playback is paused.

61

62

Returns:

63

True if audio is paused

64

"""

65

66

async def connect(

67

self,

68

*,

69

timeout: float = 60.0,

70

reconnect: bool = True,

71

self_deaf: bool = False,

72

self_mute: bool = False

73

) -> None:

74

"""

75

Connect to the voice channel.

76

77

Parameters:

78

- timeout: Connection timeout

79

- reconnect: Whether to reconnect on disconnection

80

- self_deaf: Whether to self-deafen

81

- self_mute: Whether to self-mute

82

"""

83

84

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

85

"""

86

Disconnect from voice channel.

87

88

Parameters:

89

- force: Force disconnection without cleanup

90

"""

91

92

async def move_to(self, channel: Optional[VoiceChannel]) -> None:

93

"""

94

Move to a different voice channel.

95

96

Parameters:

97

- channel: New voice channel (None to disconnect)

98

"""

99

100

def play(

101

self,

102

source: AudioSource,

103

*,

104

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

105

) -> None:

106

"""

107

Play audio from source.

108

109

Parameters:

110

- source: Audio source to play

111

- after: Callback when playback finishes

112

"""

113

114

def stop(self) -> None:

115

"""Stop audio playback."""

116

117

def pause(self) -> None:

118

"""Pause audio playback."""

119

120

def resume(self) -> None:

121

"""Resume audio playback."""

122

123

@property

124

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

125

"""Currently playing audio source."""

126

127

def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None:

128

"""

129

Send raw audio packet.

130

131

Parameters:

132

- data: Audio data

133

- encode: Whether to encode with Opus

134

"""

135

136

async def ws_connect(self, host: str, port: int) -> VoiceWebSocket:

137

"""

138

Connect to voice WebSocket.

139

140

Parameters:

141

- host: Voice server host

142

- port: Voice server port

143

144

Returns:

145

Voice WebSocket connection

146

"""

147

148

class VoiceProtocol:

149

"""Base protocol for voice connections."""

150

151

def __init__(self, client: Client):

152

self.client = client

153

154

async def connect(self, channel: VoiceChannel) -> VoiceClient:

155

"""Connect to voice channel."""

156

157

async def disconnect(self) -> None:

158

"""Disconnect from voice."""

159

```

160

161

### Audio Sources

162

163

Audio source classes for different types of audio input and streaming.

164

165

```python { .api }

166

class AudioSource:

167

"""Base class for audio sources."""

168

169

def read(self) -> bytes:

170

"""

171

Read audio data.

172

173

Returns:

174

Audio frame data (20ms of audio)

175

"""

176

177

def cleanup(self) -> None:

178

"""Clean up audio source resources."""

179

180

def is_opus(self) -> bool:

181

"""

182

Check if source provides Opus-encoded audio.

183

184

Returns:

185

True if Opus-encoded

186

"""

187

188

class FFmpegAudio(AudioSource):

189

"""Audio source using FFmpeg for processing."""

190

191

def __init__(

192

self,

193

source: Union[str, io.BufferedIOBase],

194

*,

195

executable: str = 'ffmpeg',

196

pipe: bool = False,

197

stderr: Optional[io.TextIOBase] = None,

198

before_options: Optional[str] = None,

199

options: Optional[str] = None

200

):

201

"""

202

Initialize FFmpeg audio source.

203

204

Parameters:

205

- source: Audio file path or stream

206

- executable: FFmpeg executable path

207

- pipe: Whether to use pipe input

208

- stderr: Stderr stream for FFmpeg

209

- before_options: FFmpeg input options

210

- options: FFmpeg output options

211

"""

212

213

@classmethod

214

def from_probe(

215

cls,

216

source: Union[str, io.BufferedIOBase],

217

**kwargs

218

) -> FFmpegAudio:

219

"""

220

Create FFmpeg source with automatic format detection.

221

222

Parameters:

223

- source: Audio source

224

- kwargs: Additional FFmpeg options

225

226

Returns:

227

FFmpeg audio source

228

"""

229

230

class FFmpegPCMAudio(FFmpegAudio):

231

"""FFmpeg audio source outputting PCM audio."""

232

233

def __init__(

234

self,

235

source: Union[str, io.BufferedIOBase],

236

**kwargs

237

):

238

"""

239

Initialize FFmpeg PCM audio source.

240

241

Parameters:

242

- source: Audio source

243

- kwargs: FFmpeg options

244

"""

245

246

class FFmpegOpusAudio(FFmpegAudio):

247

"""FFmpeg audio source outputting Opus audio."""

248

249

def __init__(

250

self,

251

source: Union[str, io.BufferedIOBase],

252

**kwargs

253

):

254

"""

255

Initialize FFmpeg Opus audio source.

256

257

Parameters:

258

- source: Audio source

259

- kwargs: FFmpeg options

260

"""

261

262

class PCMAudio(AudioSource):

263

"""Raw PCM audio source."""

264

265

def __init__(self, stream: io.BufferedIOBase):

266

"""

267

Initialize PCM audio source.

268

269

Parameters:

270

- stream: PCM audio stream

271

"""

272

273

class PCMVolumeTransformer(AudioSource):

274

"""Audio source with volume transformation."""

275

276

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

277

"""

278

Initialize volume transformer.

279

280

Parameters:

281

- original: Original audio source

282

- volume: Volume multiplier (0.0-2.0)

283

"""

284

285

@property

286

def volume(self) -> float:

287

"""Current volume level."""

288

289

@volume.setter

290

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

291

"""Set volume level."""

292

293

class AudioPlayer:

294

"""Audio player for managing playback."""

295

296

def __init__(

297

self,

298

source: AudioSource,

299

client: VoiceClient,

300

*,

301

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

302

):

303

"""

304

Initialize audio player.

305

306

Parameters:

307

- source: Audio source to play

308

- client: Voice client

309

- after: Callback when playback finishes

310

"""

311

312

def start(self) -> None:

313

"""Start audio playback."""

314

315

def stop(self) -> None:

316

"""Stop audio playback."""

317

318

def pause(self, *, update_resume: bool = True) -> None:

319

"""

320

Pause audio playback.

321

322

Parameters:

323

- update_resume: Whether to update resume timestamp

324

"""

325

326

def resume(self, *, update_pause: bool = True) -> None:

327

"""

328

Resume audio playback.

329

330

Parameters:

331

- update_pause: Whether to update pause timestamp

332

"""

333

334

def is_playing(self) -> bool:

335

"""Check if audio is playing."""

336

337

def is_paused(self) -> bool:

338

"""Check if audio is paused."""

339

340

@property

341

def source(self) -> AudioSource:

342

"""Audio source being played."""

343

```

344

345

### Opus Audio Codec

346

347

Opus codec utilities for voice audio encoding and decoding.

348

349

```python { .api }

350

class Encoder:

351

"""Opus encoder for audio compression."""

352

353

def __init__(

354

self,

355

sampling_rate: int = 48000,

356

channels: int = 2,

357

application: int = Application.audio

358

):

359

"""

360

Initialize Opus encoder.

361

362

Parameters:

363

- sampling_rate: Audio sampling rate

364

- channels: Number of audio channels

365

- application: Opus application type

366

"""

367

368

def encode(self, pcm: bytes, frame_size: int) -> bytes:

369

"""

370

Encode PCM audio to Opus.

371

372

Parameters:

373

- pcm: PCM audio data

374

- frame_size: Frame size in samples

375

376

Returns:

377

Opus-encoded audio data

378

"""

379

380

def set_bitrate(self, kbps: int) -> None:

381

"""

382

Set encoder bitrate.

383

384

Parameters:

385

- kbps: Bitrate in kilobits per second

386

"""

387

388

def set_bandwidth(self, req: int) -> None:

389

"""

390

Set encoder bandwidth.

391

392

Parameters:

393

- req: Bandwidth setting

394

"""

395

396

def set_signal_type(self, req: int) -> None:

397

"""

398

Set signal type.

399

400

Parameters:

401

- req: Signal type (voice/music)

402

"""

403

404

class Decoder:

405

"""Opus decoder for audio decompression."""

406

407

def __init__(self, sampling_rate: int = 48000, channels: int = 2):

408

"""

409

Initialize Opus decoder.

410

411

Parameters:

412

- sampling_rate: Audio sampling rate

413

- channels: Number of audio channels

414

"""

415

416

def decode(self, opus: bytes, *, decode_fec: bool = False) -> bytes:

417

"""

418

Decode Opus audio to PCM.

419

420

Parameters:

421

- opus: Opus-encoded audio data

422

- decode_fec: Whether to decode FEC data

423

424

Returns:

425

PCM audio data

426

"""

427

428

@staticmethod

429

def packet_get_bandwidth(data: bytes) -> int:

430

"""Get packet bandwidth."""

431

432

@staticmethod

433

def packet_get_nb_channels(data: bytes) -> int:

434

"""Get packet channel count."""

435

436

@staticmethod

437

def packet_get_nb_frames(data: bytes, frame_size: int) -> int:

438

"""Get packet frame count."""

439

440

@staticmethod

441

def packet_get_samples_per_frame(data: bytes, sampling_rate: int) -> int:

442

"""Get samples per frame."""

443

444

def is_loaded() -> bool:

445

"""

446

Check if Opus library is loaded.

447

448

Returns:

449

True if Opus is available

450

"""

451

452

def load_opus(name: str) -> None:

453

"""

454

Load Opus library.

455

456

Parameters:

457

- name: Library name or path

458

"""

459

```

460

461

### Voice Regions and Quality

462

463

Voice server regions and quality settings for optimal voice performance.

464

465

```python { .api }

466

class VoiceRegion:

467

"""Voice server region information."""

468

469

def __init__(self): ...

470

471

id: str

472

name: str

473

vip: bool

474

optimal: bool

475

deprecated: bool

476

custom: bool

477

478

def __str__(self) -> str:

479

return self.name

480

481

class VideoQualityMode(enum.Enum):

482

"""Video quality modes for voice channels."""

483

484

auto = 1

485

full = 2

486

487

async def discover_voice_regions(guild_id: int) -> List[VoiceRegion]:

488

"""

489

Discover available voice regions for a guild.

490

491

Parameters:

492

- guild_id: Guild ID

493

494

Returns:

495

List of available voice regions

496

"""

497

```

498

499

### Voice Channel Effects

500

501

Voice channel effects and audio modifications.

502

503

```python { .api }

504

class VoiceChannelEffect:

505

"""Voice channel effect configuration."""

506

507

def __init__(self): ...

508

509

emoji: Optional[PartialEmoji]

510

animation_type: Optional[VoiceChannelEffectAnimationType]

511

animation_id: Optional[int]

512

user_id: Optional[int]

513

514

class VoiceChannelEffectAnimationType(enum.Enum):

515

"""Voice channel effect animation types."""

516

517

premium = 0

518

basic = 1

519

520

async def send_voice_channel_effect(

521

channel: VoiceChannel,

522

emoji: Union[str, Emoji, PartialEmoji],

523

*,

524

animation_type: VoiceChannelEffectAnimationType = VoiceChannelEffectAnimationType.premium

525

) -> None:

526

"""

527

Send voice channel effect.

528

529

Parameters:

530

- channel: Voice channel

531

- emoji: Effect emoji

532

- animation_type: Animation type

533

"""

534

```

535

536

### Voice Events

537

538

Voice-related events for monitoring voice activity and connections.

539

540

```python { .api }

541

@bot.event

542

async def on_voice_state_update(member: Member, before: VoiceState, after: VoiceState):

543

"""

544

Called when member voice state changes.

545

546

Parameters:

547

- member: Member whose voice state changed

548

- before: Previous voice state

549

- after: New voice state

550

"""

551

552

@bot.event

553

async def on_voice_channel_effect(effect: VoiceChannelEffect):

554

"""

555

Called when voice channel effect is sent.

556

557

Parameters:

558

- effect: Voice channel effect data

559

"""

560

561

# Voice client events (when using voice)

562

async def on_voice_ready():

563

"""Called when voice connection is ready."""

564

565

async def on_voice_disconnect(error: Optional[Exception]):

566

"""

567

Called when voice connection disconnects.

568

569

Parameters:

570

- error: Disconnection error if any

571

"""

572

```

573

574

## Usage Examples

575

576

### Basic Music Bot

577

578

```python

579

import disnake

580

from disnake.ext import commands

581

import asyncio

582

import youtube_dl

583

import os

584

585

# Suppress noise about console usage from errors

586

youtube_dl.utils.bug_reports_message = lambda: ''

587

588

ytdl_format_options = {

589

'format': 'bestaudio/best',

590

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

591

'restrictfilenames': True,

592

'noplaylist': True,

593

'nocheckcertificate': True,

594

'ignoreerrors': False,

595

'logtostderr': False,

596

'quiet': True,

597

'no_warnings': True,

598

'default_search': 'auto',

599

'source_address': '0.0.0.0'

600

}

601

602

ffmpeg_options = {

603

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

604

'options': '-vn'

605

}

606

607

ytdl = youtube_dl.YoutubeDL(ytdl_format_options)

608

609

class YTDLSource(disnake.PCMVolumeTransformer):

610

def __init__(self, source, *, data, volume=0.5):

611

super().__init__(source, volume)

612

self.data = data

613

self.title = data.get('title')

614

self.url = data.get('url')

615

616

@classmethod

617

async def from_url(cls, url, *, loop=None, stream=False):

618

loop = loop or asyncio.get_event_loop()

619

data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))

620

621

if 'entries' in data:

622

# Take first item from a playlist

623

data = data['entries'][0]

624

625

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

626

return cls(disnake.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)

627

628

class Music(commands.Cog):

629

def __init__(self, bot):

630

self.bot = bot

631

self.queue = {}

632

self.current = {}

633

634

@commands.command()

635

async def join(self, ctx, *, channel: disnake.VoiceChannel = None):

636

"""Join a voice channel."""

637

if channel is None:

638

if ctx.author.voice:

639

channel = ctx.author.voice.channel

640

else:

641

return await ctx.send("You need to specify a channel or be in one.")

642

643

if ctx.voice_client is not None:

644

return await ctx.voice_client.move_to(channel)

645

646

await channel.connect()

647

await ctx.send(f"Connected to {channel}")

648

649

@commands.command()

650

async def play(self, ctx, *, url):

651

"""Play audio from a URL or search query."""

652

if not ctx.voice_client:

653

if ctx.author.voice:

654

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

655

else:

656

return await ctx.send("You need to be in a voice channel!")

657

658

async with ctx.typing():

659

try:

660

player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)

661

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

662

663

self.current[ctx.guild.id] = player

664

await ctx.send(f'**Now playing:** {player.title}')

665

666

except Exception as e:

667

await ctx.send(f'An error occurred: {e}')

668

669

@commands.command()

670

async def volume(self, ctx, volume: int):

671

"""Change the player volume (0-100)."""

672

if ctx.voice_client is None:

673

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

674

675

if not 0 <= volume <= 100:

676

return await ctx.send("Volume must be between 0 and 100.")

677

678

ctx.voice_client.source.volume = volume / 100

679

await ctx.send(f"Changed volume to {volume}%")

680

681

@commands.command()

682

async def stop(self, ctx):

683

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

684

if ctx.voice_client:

685

ctx.voice_client.stop()

686

await ctx.send("⏹️ Stopped playback")

687

688

@commands.command()

689

async def pause(self, ctx):

690

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

691

if ctx.voice_client and ctx.voice_client.is_playing():

692

ctx.voice_client.pause()

693

await ctx.send("⏸️ Paused playback")

694

695

@commands.command()

696

async def resume(self, ctx):

697

"""Resume paused audio."""

698

if ctx.voice_client and ctx.voice_client.is_paused():

699

ctx.voice_client.resume()

700

await ctx.send("▶️ Resumed playback")

701

702

@commands.command()

703

async def leave(self, ctx):

704

"""Disconnect from voice channel."""

705

if ctx.voice_client:

706

await ctx.voice_client.disconnect()

707

await ctx.send("👋 Disconnected from voice channel")

708

709

@commands.command()

710

async def now_playing(self, ctx):

711

"""Show currently playing track."""

712

if ctx.guild.id in self.current:

713

player = self.current[ctx.guild.id]

714

embed = disnake.Embed(title="🎵 Now Playing", description=player.title)

715

embed.add_field(name="Volume", value=f"{int(player.volume * 100)}%")

716

717

if ctx.voice_client:

718

if ctx.voice_client.is_playing():

719

embed.color = 0x00ff00

720

embed.set_footer(text="Playing")

721

elif ctx.voice_client.is_paused():

722

embed.color = 0xffaa00

723

embed.set_footer(text="Paused")

724

725

await ctx.send(embed=embed)

726

else:

727

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

728

729

@play.before_invoke

730

async def ensure_voice(self, ctx):

731

"""Ensure voice connection before playing."""

732

if ctx.voice_client is None:

733

if ctx.author.voice:

734

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

735

else:

736

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

737

raise commands.CommandError("Author not connected to a voice channel.")

738

elif ctx.voice_client.is_playing():

739

ctx.voice_client.stop()

740

741

bot = commands.Bot(command_prefix='!', intents=disnake.Intents.all())

742

bot.add_cog(Music(bot))

743

744

bot.run('YOUR_BOT_TOKEN')

745

```

746

747

### Advanced Music Bot with Queue

748

749

```python

750

import asyncio

751

from collections import deque

752

from typing import Optional, Dict, List

753

754

class MusicQueue:

755

"""Music queue management."""

756

757

def __init__(self):

758

self.queue = deque()

759

self.history = deque(maxlen=10)

760

self.repeat_mode = 0 # 0=off, 1=track, 2=queue

761

self.shuffle = False

762

self.volume = 0.5

763

764

def add(self, item):

765

"""Add item to queue."""

766

self.queue.append(item)

767

768

def next(self):

769

"""Get next item from queue."""

770

if not self.queue:

771

return None

772

773

if self.repeat_mode == 2 and len(self.queue) == 1:

774

# Queue repeat with single item

775

return self.queue[0]

776

777

item = self.queue.popleft()

778

779

if self.repeat_mode == 2:

780

# Add back to end for queue repeat

781

self.queue.append(item)

782

783

return item

784

785

def clear(self):

786

"""Clear the queue."""

787

self.queue.clear()

788

789

def remove(self, index: int):

790

"""Remove item at index."""

791

if 0 <= index < len(self.queue):

792

del self.queue[index]

793

794

def __len__(self):

795

return len(self.queue)

796

797

def __iter__(self):

798

return iter(self.queue)

799

800

class AdvancedMusic(commands.Cog):

801

def __init__(self, bot):

802

self.bot = bot

803

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

804

self.current_players: Dict[int, YTDLSource] = {}

805

806

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

807

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

808

if guild_id not in self.queues:

809

self.queues[guild_id] = MusicQueue()

810

return self.queues[guild_id]

811

812

async def play_next(self, ctx):

813

"""Play next track in queue."""

814

guild_id = ctx.guild.id

815

queue = self.get_queue(guild_id)

816

817

if not ctx.voice_client:

818

return

819

820

# Handle repeat single

821

if queue.repeat_mode == 1 and guild_id in self.current_players:

822

current = self.current_players[guild_id]

823

player = await YTDLSource.from_url(current.data['webpage_url'], loop=self.bot.loop, stream=True)

824

player.volume = queue.volume

825

else:

826

next_item = queue.next()

827

if not next_item:

828

await ctx.send("Queue is empty. Playback finished.")

829

return

830

831

player = await YTDLSource.from_url(next_item['url'], loop=self.bot.loop, stream=True)

832

player.volume = queue.volume

833

834

# Add to history

835

if guild_id in self.current_players:

836

queue.history.append(self.current_players[guild_id])

837

838

self.current_players[guild_id] = player

839

840

ctx.voice_client.play(

841

player,

842

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

843

)

844

845

embed = disnake.Embed(title="🎵 Now Playing", description=player.title, color=0x00ff00)

846

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

847

embed.add_field(name="Volume", value=f"{int(player.volume * 100)}%", inline=True)

848

849

repeat_text = ["Off", "Track", "Queue"][queue.repeat_mode]

850

embed.add_field(name="Repeat", value=repeat_text, inline=True)

851

852

await ctx.send(embed=embed)

853

854

@commands.command()

855

async def queue(self, ctx, *, query):

856

"""Add a track to the queue."""

857

if not ctx.voice_client:

858

if ctx.author.voice:

859

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

860

else:

861

return await ctx.send("You need to be in a voice channel!")

862

863

async with ctx.typing():

864

try:

865

# Extract info without downloading

866

loop = asyncio.get_event_loop()

867

data = await loop.run_in_executor(None, lambda: ytdl.extract_info(query, download=False))

868

869

if 'entries' in data:

870

# Playlist

871

entries = data['entries'][:10] # Limit to 10 tracks

872

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

873

874

for entry in entries:

875

queue.add({

876

'url': entry['webpage_url'],

877

'title': entry['title'],

878

'duration': entry.get('duration', 0),

879

'requester': ctx.author.id

880

})

881

882

await ctx.send(f"Added {len(entries)} tracks to queue")

883

884

else:

885

# Single track

886

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

887

queue.add({

888

'url': data['webpage_url'],

889

'title': data['title'],

890

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

891

'requester': ctx.author.id

892

})

893

894

await ctx.send(f"Added **{data['title']}** to queue (position {len(queue)})")

895

896

# Start playing if nothing is playing

897

if not ctx.voice_client.is_playing() and not ctx.voice_client.is_paused():

898

await self.play_next(ctx)

899

900

except Exception as e:

901

await ctx.send(f'An error occurred: {e}')

902

903

@commands.command(name='queue_list', aliases=['q', 'list'])

904

async def queue_list(self, ctx):

905

"""Show current queue."""

906

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

907

908

if len(queue) == 0:

909

return await ctx.send("Queue is empty.")

910

911

embed = disnake.Embed(title="🎵 Music Queue", color=0x00ff00)

912

913

# Show current track

914

if ctx.guild.id in self.current_players:

915

current = self.current_players[ctx.guild.id]

916

embed.add_field(

917

name="Now Playing",

918

value=f"**{current.title}**",

919

inline=False

920

)

921

922

# Show next tracks

923

queue_text = ""

924

for i, track in enumerate(list(queue)[:10]): # Show first 10

925

duration = f"{track['duration'] // 60}:{track['duration'] % 60:02d}" if track['duration'] else "Unknown"

926

queue_text += f"`{i+1}.` **{track['title']}** `[{duration}]`\n"

927

928

if queue_text:

929

embed.add_field(name="Up Next", value=queue_text, inline=False)

930

931

if len(queue) > 10:

932

embed.set_footer(text=f"... and {len(queue) - 10} more tracks")

933

934

await ctx.send(embed=embed)

935

936

@commands.command()

937

async def skip(self, ctx, amount: int = 1):

938

"""Skip current track or multiple tracks."""

939

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

940

return await ctx.send("Nothing is playing.")

941

942

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

943

944

# Skip multiple tracks by removing from queue

945

for _ in range(amount - 1):

946

if len(queue) > 0:

947

queue.next()

948

949

ctx.voice_client.stop()

950

await ctx.send(f"⏭️ Skipped {amount} track(s)")

951

952

@commands.command()

953

async def remove(self, ctx, index: int):

954

"""Remove track from queue by index."""

955

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

956

957

if not 1 <= index <= len(queue):

958

return await ctx.send(f"Invalid index. Queue has {len(queue)} tracks.")

959

960

removed_track = list(queue)[index - 1]

961

queue.remove(index - 1)

962

963

await ctx.send(f"Removed **{removed_track['title']}** from queue")

964

965

@commands.command()

966

async def clear_queue(self, ctx):

967

"""Clear the entire queue."""

968

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

969

queue.clear()

970

await ctx.send("🗑️ Queue cleared")

971

972

@commands.command()

973

async def repeat(self, ctx, mode: str = None):

974

"""Set repeat mode (off/track/queue)."""

975

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

976

977

if mode is None:

978

modes = ["off", "track", "queue"]

979

current_mode = modes[queue.repeat_mode]

980

return await ctx.send(f"Current repeat mode: **{current_mode}**")

981

982

mode = mode.lower()

983

if mode in ['off', '0', 'none']:

984

queue.repeat_mode = 0

985

await ctx.send("🔁 Repeat mode: **Off**")

986

elif mode in ['track', '1', 'song']:

987

queue.repeat_mode = 1

988

await ctx.send("🔂 Repeat mode: **Track**")

989

elif mode in ['queue', '2', 'all']:

990

queue.repeat_mode = 2

991

await ctx.send("🔁 Repeat mode: **Queue**")

992

else:

993

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

994

995

@commands.command()

996

async def shuffle(self, ctx):

997

"""Toggle shuffle mode."""

998

import random

999

1000

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

1001

queue.shuffle = not queue.shuffle

1002

1003

if queue.shuffle:

1004

# Shuffle current queue

1005

queue_list = list(queue.queue)

1006

random.shuffle(queue_list)

1007

queue.queue = deque(queue_list)

1008

await ctx.send("🔀 Shuffle: **On**")

1009

else:

1010

await ctx.send("🔀 Shuffle: **Off**")

1011

1012

@commands.command()

1013

async def seek(self, ctx, timestamp: str):

1014

"""Seek to timestamp (MM:SS format)."""

1015

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

1016

return await ctx.send("Nothing is playing.")

1017

1018

try:

1019

parts = timestamp.split(':')

1020

if len(parts) == 2:

1021

minutes, seconds = map(int, parts)

1022

total_seconds = minutes * 60 + seconds

1023

else:

1024

total_seconds = int(parts[0])

1025

1026

# Note: This is simplified - actual seeking requires more complex FFmpeg handling

1027

await ctx.send(f"⏩ Seeking to {timestamp} (restart with timestamp)")

1028

1029

# In a real implementation, you'd restart playback from the timestamp

1030

1031

except ValueError:

1032

await ctx.send("Invalid timestamp format. Use MM:SS or seconds.")

1033

1034

@commands.command()

1035

async def lyrics(self, ctx, *, query: str = None):

1036

"""Get lyrics for current or specified song."""

1037

if query is None:

1038

if ctx.guild.id in self.current_players:

1039

query = self.current_players[ctx.guild.id].title

1040

else:

1041

return await ctx.send("No song is playing. Specify a song name.")

1042

1043

# This would integrate with a lyrics API like Genius

1044

await ctx.send(f"🎤 Searching lyrics for: **{query}**\n*Lyrics API integration needed*")

1045

1046

@commands.command()

1047

async def history(self, ctx):

1048

"""Show recently played tracks."""

1049

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

1050

1051

if not queue.history:

1052

return await ctx.send("No recent tracks.")

1053

1054

embed = disnake.Embed(title="🕐 Recently Played", color=0x0099ff)

1055

1056

history_text = ""

1057

for i, track in enumerate(reversed(list(queue.history))):

1058

history_text += f"`{i+1}.` **{track.title}**\n"

1059

1060

embed.description = history_text

1061

await ctx.send(embed=embed)

1062

1063

# Error handling for voice

1064

@bot.event

1065

async def on_voice_state_update(member, before, after):

1066

"""Handle voice state changes."""

1067

# Auto-disconnect if bot is alone in voice channel

1068

if member == bot.user:

1069

return

1070

1071

voice_client = member.guild.voice_client

1072

if voice_client and voice_client.channel:

1073

# Check if bot is alone (only bot in voice channel)

1074

members_in_voice = [m for m in voice_client.channel.members if not m.bot]

1075

1076

if len(members_in_voice) == 0:

1077

# Wait a bit before disconnecting

1078

await asyncio.sleep(30)

1079

1080

# Check again after delay

1081

members_in_voice = [m for m in voice_client.channel.members if not m.bot]

1082

if len(members_in_voice) == 0:

1083

await voice_client.disconnect()

1084

1085

bot.add_cog(AdvancedMusic(bot))

1086

```

1087

1088

### Voice Recording and Processing

1089

1090

```python

1091

import wave

1092

import io

1093

from typing import Dict, List

1094

1095

class VoiceRecorder:

1096

"""Record voice from Discord voice channels."""

1097

1098

def __init__(self, voice_client: disnake.VoiceClient):

1099

self.voice_client = voice_client

1100

self.recordings: Dict[int, List[bytes]] = {}

1101

self.is_recording = False

1102

1103

def start_recording(self):

1104

"""Start recording all users in voice channel."""

1105

if self.is_recording:

1106

return

1107

1108

self.is_recording = True

1109

self.recordings.clear()

1110

1111

# This would require a custom voice receive implementation

1112

# Discord bots cannot currently receive audio through the official API

1113

print("Recording started (implementation needed)")

1114

1115

def stop_recording(self):

1116

"""Stop recording and return audio data."""

1117

if not self.is_recording:

1118

return

1119

1120

self.is_recording = False

1121

1122

# Process recordings into WAV files

1123

wav_files = {}

1124

for user_id, audio_data in self.recordings.items():

1125

wav_buffer = io.BytesIO()

1126

with wave.open(wav_buffer, 'wb') as wav_file:

1127

wav_file.setnchannels(2) # Stereo

1128

wav_file.setsampwidth(2) # 16-bit

1129

wav_file.setframerate(48000) # 48kHz

1130

1131

# Combine audio frames

1132

combined_audio = b''.join(audio_data)

1133

wav_file.writeframes(combined_audio)

1134

1135

wav_files[user_id] = wav_buffer.getvalue()

1136

1137

return wav_files

1138

1139

class VoiceEffects(commands.Cog):

1140

"""Voice effects and processing commands."""

1141

1142

def __init__(self, bot):

1143

self.bot = bot

1144

1145

@commands.command()

1146

async def voice_effect(self, ctx, effect: str):

1147

"""Apply voice effect to bot's audio."""

1148

if not ctx.voice_client:

1149

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

1150

1151

effects = {

1152

'robot': '-af "afftfilt=real=\'hypot(re,im)*sin(0)\':imag=\'hypot(re,im)*cos(0)\':win_size=512:overlap=0.75"',

1153

'echo': '-af "aecho=0.8:0.9:1000:0.3"',

1154

'bass': '-af "bass=g=5"',

1155

'treble': '-af "treble=g=5"',

1156

'speed': '-af "atempo=1.5"',

1157

'slow': '-af "atempo=0.75"',

1158

'nightcore': '-af "aresample=48000,asetrate=48000*1.25"',

1159

'deep': '-af "asetrate=22050,aresample=48000"'

1160

}

1161

1162

if effect not in effects:

1163

available = ', '.join(effects.keys())

1164

return await ctx.send(f"Available effects: {available}")

1165

1166

# This would modify the FFmpeg options for the current audio source

1167

await ctx.send(f"🎛️ Applied **{effect}** effect")

1168

# Implementation would require restarting playback with new FFmpeg filter

1169

1170

@commands.command()

1171

async def soundboard(self, ctx, sound: str):

1172

"""Play soundboard effects."""

1173

if not ctx.voice_client:

1174

if ctx.author.voice:

1175

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

1176

else:

1177

return await ctx.send("You need to be in a voice channel!")

1178

1179

# Soundboard files directory

1180

sound_file = f"sounds/{sound}.mp3"

1181

1182

if not os.path.exists(sound_file):

1183

return await ctx.send(f"Sound '{sound}' not found.")

1184

1185

# Play sound effect

1186

source = disnake.FFmpegPCMAudio(sound_file, **ffmpeg_options)

1187

1188

if ctx.voice_client.is_playing():

1189

ctx.voice_client.stop()

1190

1191

ctx.voice_client.play(source)

1192

await ctx.send(f"🔊 Playing sound: **{sound}**")

1193

1194

@commands.command()

1195

async def tts(self, ctx, *, text: str):

1196

"""Text-to-speech in voice channel."""

1197

if not ctx.voice_client:

1198

if ctx.author.voice:

1199

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

1200

else:

1201

return await ctx.send("You need to be in a voice channel!")

1202

1203

if len(text) > 200:

1204

return await ctx.send("Text too long (max 200 characters).")

1205

1206

# This would use a TTS service like gTTS

1207

# For demo purposes, just acknowledge

1208

await ctx.send(f"🗣️ TTS: {text[:50]}{'...' if len(text) > 50 else ''}")

1209

1210

# Implementation would:

1211

# 1. Generate TTS audio file

1212

# 2. Play through voice client

1213

# 3. Clean up temporary file

1214

1215

@commands.command()

1216

async def voice_status(self, ctx):

1217

"""Show voice connection status and stats."""

1218

if not ctx.voice_client:

1219

return await ctx.send("Not connected to voice.")

1220

1221

vc = ctx.voice_client

1222

1223

embed = disnake.Embed(title="🔊 Voice Status", color=0x00ff00)

1224

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

1225

embed.add_field(name="Latency", value=f"{vc.latency*1000:.2f}ms", inline=True)

1226

embed.add_field(name="Average Latency", value=f"{vc.average_latency*1000:.2f}ms", inline=True)

1227

1228

status_text = []

1229

if vc.is_connected():

1230

status_text.append("✅ Connected")

1231

if vc.is_playing():

1232

status_text.append("▶️ Playing")

1233

if vc.is_paused():

1234

status_text.append("⏸️ Paused")

1235

1236

embed.add_field(name="Status", value=" | ".join(status_text) or "Idle", inline=False)

1237

1238

# Voice channel members

1239

members = [m.display_name for m in vc.channel.members if not m.bot]

1240

if members:

1241

embed.add_field(name=f"Members ({len(members)})", value=", ".join(members), inline=False)

1242

1243

await ctx.send(embed=embed)

1244

1245

# Voice channel management

1246

@bot.command()

1247

async def create_voice(ctx, *, name: str):

1248

"""Create a temporary voice channel."""

1249

if not ctx.author.guild_permissions.manage_channels:

1250

return await ctx.send("You don't have permission to manage channels.")

1251

1252

# Create temporary voice channel

1253

overwrites = {

1254

ctx.guild.default_role: disnake.PermissionOverwrite(view_channel=True),

1255

ctx.author: disnake.PermissionOverwrite(manage_channels=True)

1256

}

1257

1258

channel = await ctx.guild.create_voice_channel(

1259

name=f"🔊 {name}",

1260

overwrites=overwrites,

1261

reason=f"Temporary voice channel created by {ctx.author}"

1262

)

1263

1264

await ctx.send(f"Created temporary voice channel: {channel.mention}")

1265

1266

# Auto-delete when empty (would need a background task)

1267

1268

@bot.command()

1269

async def voice_info(ctx, channel: disnake.VoiceChannel = None):

1270

"""Get information about a voice channel."""

1271

if channel is None:

1272

if ctx.author.voice:

1273

channel = ctx.author.voice.channel

1274

else:

1275

return await ctx.send("Specify a voice channel or join one.")

1276

1277

embed = disnake.Embed(title=f"🔊 {channel.name}", color=0x0099ff)

1278

embed.add_field(name="ID", value=channel.id, inline=True)

1279

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

1280

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

1281

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

1282

embed.add_field(name="Created", value=f"<t:{int(channel.created_at.timestamp())}:F>", inline=True)

1283

1284

if channel.rtc_region:

1285

embed.add_field(name="Region", value=channel.rtc_region, inline=True)

1286

1287

# List members

1288

if channel.members:

1289

member_list = []

1290

for member in channel.members:

1291

status = []

1292

if member.voice.deaf:

1293

status.append("🔇")

1294

if member.voice.mute:

1295

status.append("🤐")

1296

if member.voice.self_deaf:

1297

status.append("🙉")

1298

if member.voice.self_mute:

1299

status.append("🤫")

1300

if member.voice.streaming:

1301

status.append("📹")

1302

1303

member_list.append(f"{member.display_name} {''.join(status)}")

1304

1305

embed.add_field(name="Connected Members", value="\n".join(member_list), inline=False)

1306

1307

await ctx.send(embed=embed)

1308

1309

bot.add_cog(VoiceEffects(bot))

1310

```