or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

app-commands.mdcommands-framework.mdcore-objects.mdevent-handling.mdindex.mduser-interface.mdutilities.mdvoice-audio.mdwebhooks.md

voice-audio.mddocs/

0

# Voice & Audio

1

2

Voice connection management and audio playback capabilities for Discord bots. Discord.py provides comprehensive voice support including connection management, audio sources, and real-time voice data processing with opus encoding.

3

4

## Capabilities

5

6

### Voice Connections

7

8

Voice clients manage connections to Discord voice channels with audio playback and recording capabilities.

9

10

```python { .api }

11

class VoiceClient:

12

"""

13

Voice connection to a Discord voice channel.

14

"""

15

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

16

17

# Connection properties

18

client: Client # Associated Discord client

19

channel: VoiceChannel # Connected voice channel

20

guild: Guild # Guild containing the voice channel

21

user: ClientUser # Bot user

22

session_id: str # Voice session ID

23

token: str # Voice token

24

endpoint: str # Voice server endpoint

25

26

# Connection state

27

is_connected: bool # Whether connected to voice

28

is_playing: bool # Whether currently playing audio

29

is_paused: bool # Whether audio is paused

30

source: Optional[AudioSource] # Current audio source

31

32

# Connection management

33

async def connect(self, *, timeout: float = 60.0, reconnect: bool = True, self_deaf: bool = False, self_mute: bool = False) -> None:

34

"""

35

Connect to the voice channel.

36

37

Parameters:

38

- timeout: Connection timeout in seconds

39

- reconnect: Whether to attempt reconnection on disconnect

40

- self_deaf: Whether to deafen the bot

41

- self_mute: Whether to mute the bot

42

"""

43

44

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

45

"""

46

Disconnect from voice channel.

47

48

Parameters:

49

- force: Whether to force disconnect immediately

50

"""

51

52

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

53

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

54

55

# Audio playback

56

def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], None]] = None) -> None:

57

"""

58

Play an audio source.

59

60

Parameters:

61

- source: Audio source to play

62

- after: Callback function called when playback finishes

63

"""

64

65

def stop(self) -> None:

66

"""Stop current audio playback."""

67

68

def pause(self) -> None:

69

"""Pause current audio playback."""

70

71

def resume(self) -> None:

72

"""Resume paused audio playback."""

73

74

# Voice state management

75

async def edit(self, *, mute: bool = None, deafen: bool = None) -> None:

76

"""Edit bot's voice state."""

77

78

# Properties

79

@property

80

def latency(self) -> float:

81

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

82

83

@property

84

def average_latency(self) -> float:

85

"""Average voice latency over recent packets."""

86

87

class VoiceProtocol:

88

"""

89

Abstract base class for voice protocol implementations.

90

"""

91

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

92

93

async def connect(self, **kwargs) -> None:

94

"""Connect to voice channel."""

95

raise NotImplementedError

96

97

async def disconnect(self, **kwargs) -> None:

98

"""Disconnect from voice channel."""

99

raise NotImplementedError

100

```

101

102

### Audio Sources

103

104

Audio sources provide audio data for playback with support for various formats and streaming.

105

106

```python { .api }

107

class AudioSource:

108

"""

109

Base class for audio sources.

110

"""

111

def read(self) -> bytes:

112

"""

113

Read audio data (20ms of audio at 48kHz stereo).

114

115

Returns:

116

bytes: Audio data in opus format, or empty bytes to signal end

117

"""

118

raise NotImplementedError

119

120

def cleanup(self) -> None:

121

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

122

pass

123

124

@property

125

def is_opus(self) -> bool:

126

"""Whether audio source provides opus-encoded data."""

127

return False

128

129

class AudioPlayer:

130

"""

131

Audio player that handles audio source playback.

132

"""

133

def __init__(self, source: AudioSource, client: VoiceClient, *, after: Optional[Callable] = None): ...

134

135

source: AudioSource # Audio source

136

client: VoiceClient # Voice client

137

138

def start(self) -> None:

139

"""Start audio playback."""

140

141

def stop(self) -> None:

142

"""Stop audio playback."""

143

144

def pause(self) -> None:

145

"""Pause audio playback."""

146

147

def resume(self) -> None:

148

"""Resume audio playback."""

149

150

@property

151

def is_playing(self) -> bool:

152

"""Whether audio is currently playing."""

153

154

@property

155

def is_paused(self) -> bool:

156

"""Whether audio is paused."""

157

158

class PCMAudio(AudioSource):

159

"""

160

Audio source for raw PCM audio data.

161

162

Parameters:

163

- stream: File-like object or file path containing PCM data

164

- executable: FFmpeg executable path (defaults to 'ffmpeg')

165

"""

166

def __init__(self, source: Union[str, io.BufferedIOBase], *, executable: str = 'ffmpeg'): ...

167

168

def read(self) -> bytes:

169

"""Read PCM audio data."""

170

171

def cleanup(self) -> None:

172

"""Clean up PCM audio resources."""

173

174

class FFmpegAudio(AudioSource):

175

"""

176

Audio source using FFmpeg for format conversion and streaming.

177

178

Parameters:

179

- source: Audio source (file path, URL, or stream)

180

- executable: FFmpeg executable path

181

- pipe: Whether to use pipe for streaming

182

- stderr: Where to redirect stderr output

183

- before_options: FFmpeg options before input

184

- options: FFmpeg options after input

185

"""

186

def __init__(

187

self,

188

source: Union[str, io.BufferedIOBase],

189

*,

190

executable: str = 'ffmpeg',

191

pipe: bool = False,

192

stderr: Optional[io.IOBase] = None,

193

before_options: Optional[str] = None,

194

options: Optional[str] = None

195

): ...

196

197

def read(self) -> bytes:

198

"""Read audio data from FFmpeg."""

199

200

def cleanup(self) -> None:

201

"""Clean up FFmpeg process."""

202

203

@classmethod

204

def from_probe(cls, source: Union[str, io.BufferedIOBase], **kwargs) -> FFmpegAudio:

205

"""Create FFmpegAudio with automatic format detection."""

206

207

class FFmpegPCMAudio(FFmpegAudio):

208

"""

209

FFmpeg audio source that outputs PCM format.

210

"""

211

def __init__(self, source: Union[str, io.BufferedIOBase], **kwargs): ...

212

213

class FFmpegOpusAudio(FFmpegAudio):

214

"""

215

FFmpeg audio source that outputs Opus format.

216

"""

217

def __init__(self, source: Union[str, io.BufferedIOBase], **kwargs): ...

218

219

@property

220

def is_opus(self) -> bool:

221

"""Opus audio sources are opus-encoded."""

222

return True

223

224

class PCMVolumeTransformer(AudioSource):

225

"""

226

Audio source that applies volume transformation to PCM audio.

227

228

Parameters:

229

- original: Original audio source

230

- volume: Volume multiplier (0.0 to 2.0, default 1.0)

231

"""

232

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

233

234

original: AudioSource # Original audio source

235

volume: float # Volume level (0.0 to 2.0)

236

237

def read(self) -> bytes:

238

"""Read volume-adjusted audio data."""

239

240

def cleanup(self) -> None:

241

"""Clean up transformer resources."""

242

243

@classmethod

244

def from_other_source(cls, other: AudioSource, *, volume: float = 1.0) -> PCMVolumeTransformer:

245

"""Create volume transformer from another source."""

246

```

247

248

### Opus Codec

249

250

Opus codec interface for voice encoding and decoding.

251

252

```python { .api }

253

class Encoder:

254

"""

255

Opus encoder for voice data.

256

257

Parameters:

258

- sampling_rate: Audio sampling rate (default: 48000)

259

- channels: Number of audio channels (default: 2)

260

- application: Encoder application type

261

"""

262

def __init__(

263

self,

264

sampling_rate: int = 48000,

265

channels: int = 2,

266

application: int = None

267

): ...

268

269

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

270

"""

271

Encode PCM audio to Opus format.

272

273

Parameters:

274

- pcm: PCM audio data

275

- frame_size: Frame size in samples

276

277

Returns:

278

bytes: Opus-encoded audio data

279

"""

280

281

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

282

"""Set encoder bitrate in kbps."""

283

284

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

285

"""Set encoder bandwidth."""

286

287

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

288

"""Set signal type (voice/music)."""

289

290

# Opus utilities

291

def is_loaded() -> bool:

292

"""Check if Opus library is loaded."""

293

294

def load_opus(name: str) -> None:

295

"""Load Opus library from specified path."""

296

297

# Opus exceptions

298

class OpusError(DiscordException):

299

"""Base exception for Opus codec errors."""

300

pass

301

302

class OpusNotLoaded(OpusError):

303

"""Opus library is not loaded."""

304

pass

305

```

306

307

### Voice State Management

308

309

Voice state objects track user voice connection status and properties.

310

311

```python { .api }

312

class VoiceState:

313

"""

314

Represents a user's voice state in a guild.

315

"""

316

def __init__(self, *, data: Dict[str, Any], channel: Optional[VoiceChannel] = None): ...

317

318

# User and guild info

319

user_id: int # User ID

320

guild: Guild # Guild containing the voice state

321

member: Optional[Member] # Member object

322

323

# Voice channel info

324

channel: Optional[VoiceChannel] # Connected voice channel

325

session_id: str # Voice session ID

326

327

# Voice settings

328

deaf: bool # Whether user is server deafened

329

mute: bool # Whether user is server muted

330

self_deaf: bool # Whether user is self deafened

331

self_mute: bool # Whether user is self muted

332

self_stream: bool # Whether user is streaming

333

self_video: bool # Whether user has video enabled

334

suppress: bool # Whether user is suppressed (priority speaker)

335

afk: bool # Whether user is in AFK channel

336

337

# Voice activity

338

requested_to_speak_at: Optional[datetime] # When user requested to speak (stage channels)

339

340

@property

341

def is_afk(self) -> bool:

342

"""Whether user is in the AFK channel."""

343

```

344

345

## Usage Examples

346

347

### Basic Voice Bot

348

349

```python

350

import discord

351

from discord.ext import commands

352

import asyncio

353

354

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

355

356

@bot.command()

357

async def join(ctx):

358

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

359

if ctx.author.voice is None:

360

await ctx.send("You're not in a voice channel!")

361

return

362

363

channel = ctx.author.voice.channel

364

if ctx.voice_client is not None:

365

await ctx.voice_client.move_to(channel)

366

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

367

else:

368

await channel.connect()

369

await ctx.send(f"Joined {channel.name}")

370

371

@bot.command()

372

async def leave(ctx):

373

"""Leave the voice channel."""

374

if ctx.voice_client is None:

375

await ctx.send("I'm not connected to a voice channel!")

376

return

377

378

await ctx.voice_client.disconnect()

379

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

380

381

@bot.command()

382

async def play(ctx, *, url):

383

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

384

if ctx.voice_client is None:

385

await ctx.send("I'm not connected to a voice channel!")

386

return

387

388

if ctx.voice_client.is_playing():

389

await ctx.send("Already playing audio!")

390

return

391

392

try:

393

# Create audio source

394

source = discord.FFmpegPCMAudio(url)

395

396

# Play audio

397

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

398

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

399

except Exception as e:

400

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

401

402

@bot.command()

403

async def stop(ctx):

404

"""Stop audio playback."""

405

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

406

await ctx.send("Not playing any audio!")

407

return

408

409

ctx.voice_client.stop()

410

await ctx.send("Stopped audio playback")

411

412

@bot.command()

413

async def pause(ctx):

414

"""Pause audio playback."""

415

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

416

await ctx.send("Not playing any audio!")

417

return

418

419

ctx.voice_client.pause()

420

await ctx.send("Paused audio playback")

421

422

@bot.command()

423

async def resume(ctx):

424

"""Resume audio playback."""

425

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

426

await ctx.send("Audio is not paused!")

427

return

428

429

ctx.voice_client.resume()

430

await ctx.send("Resumed audio playback")

431

```

432

433

### Advanced Music Bot with Queue

434

435

```python

436

import asyncio

437

import youtube_dl

438

from collections import deque

439

440

# Suppress noise about console usage from errors

441

youtube_dl.utils.bug_reports_message = lambda: ''

442

443

ytdl_format_options = {

444

'format': 'bestaudio/best',

445

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

446

'restrictfilenames': True,

447

'noplaylist': True,

448

'nocheckcertificate': True,

449

'ignoreerrors': False,

450

'logtostderr': False,

451

'quiet': True,

452

'no_warnings': True,

453

'default_search': 'auto',

454

'source_address': '0.0.0.0'

455

}

456

457

ffmpeg_options = {

458

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

459

'options': '-vn'

460

}

461

462

ytdl = youtube_dl.YoutubeDL(ytdl_format_options)

463

464

class YTDLSource(discord.PCMVolumeTransformer):

465

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

466

super().__init__(source, volume=volume)

467

self.data = data

468

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

469

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

470

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

471

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

472

473

@classmethod

474

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

475

loop = loop or asyncio.get_event_loop()

476

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

477

478

if 'entries' in data:

479

data = data['entries'][0]

480

481

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

482

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

483

484

class MusicBot(commands.Cog):

485

def __init__(self, bot):

486

self.bot = bot

487

self.queue = deque()

488

self.current = None

489

self.volume = 0.5

490

491

def get_queue_embed(self):

492

if not self.queue:

493

return discord.Embed(title="Queue", description="Queue is empty", color=0xff0000)

494

495

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

496

queue_list = []

497

498

for i, song in enumerate(list(self.queue)[:10], 1): # Show first 10 songs

499

duration = f" ({song.duration // 60}:{song.duration % 60:02d})" if song.duration else ""

500

queue_list.append(f"{i}. {song.title}{duration}")

501

502

embed.description = "\n".join(queue_list)

503

504

if len(self.queue) > 10:

505

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

506

507

return embed

508

509

@commands.command()

510

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

511

"""Add a song to the queue and play it."""

512

async with ctx.typing():

513

try:

514

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

515

source.volume = self.volume

516

517

self.queue.append(source)

518

519

if not ctx.voice_client.is_playing():

520

await self.play_next(ctx)

521

else:

522

embed = discord.Embed(

523

title="Added to Queue",

524

description=f"**{source.title}**\nby {source.uploader}",

525

color=0x00ff00

526

)

527

embed.set_footer(text=f"Position in queue: {len(self.queue)}")

528

await ctx.send(embed=embed)

529

530

except Exception as e:

531

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

532

533

async def play_next(self, ctx):

534

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

535

if not self.queue:

536

self.current = None

537

return

538

539

source = self.queue.popleft()

540

self.current = source

541

542

def after_playing(error):

543

if error:

544

print(f'Player error: {error}')

545

546

# Schedule next song

547

coro = self.play_next(ctx)

548

fut = asyncio.run_coroutine_threadsafe(coro, self.bot.loop)

549

try:

550

fut.result()

551

except:

552

pass

553

554

ctx.voice_client.play(source, after=after_playing)

555

556

embed = discord.Embed(

557

title="Now Playing",

558

description=f"**{source.title}**\nby {source.uploader}",

559

color=0x0099ff

560

)

561

if source.duration:

562

embed.add_field(name="Duration", value=f"{source.duration // 60}:{source.duration % 60:02d}")

563

564

await ctx.send(embed=embed)

565

566

@commands.command()

567

async def queue(self, ctx):

568

"""Show the current queue."""

569

embed = self.get_queue_embed()

570

571

if self.current:

572

embed.add_field(

573

name="Currently Playing",

574

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

575

inline=False

576

)

577

578

await ctx.send(embed=embed)

579

580

@commands.command()

581

async def skip(self, ctx):

582

"""Skip the current song."""

583

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

584

ctx.voice_client.stop()

585

await ctx.send("⏭️ Skipped!")

586

else:

587

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

588

589

@commands.command()

590

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

591

"""Change or show the current volume."""

592

if volume is None:

593

await ctx.send(f"Current volume: {int(self.volume * 100)}%")

594

return

595

596

if not 0 <= volume <= 100:

597

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

598

return

599

600

self.volume = volume / 100

601

602

if self.current:

603

self.current.volume = self.volume

604

605

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

606

607

@commands.command()

608

async def clear(self, ctx):

609

"""Clear the queue."""

610

self.queue.clear()

611

await ctx.send("πŸ—‘οΈ Queue cleared!")

612

613

@commands.command()

614

async def nowplaying(self, ctx):

615

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

616

if not self.current:

617

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

618

return

619

620

embed = discord.Embed(

621

title="Now Playing",

622

description=f"**{self.current.title}**",

623

color=0x0099ff

624

)

625

embed.add_field(name="Uploader", value=self.current.uploader)

626

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

627

628

if self.current.duration:

629

embed.add_field(name="Duration", value=f"{self.current.duration // 60}:{self.current.duration % 60:02d}")

630

631

await ctx.send(embed=embed)

632

633

async def setup(bot):

634

await bot.add_cog(MusicBot(bot))

635

```

636

637

### Voice State Monitoring

638

639

```python

640

@bot.event

641

async def on_voice_state_update(member, before, after):

642

"""Monitor voice state changes."""

643

644

# User joined a voice channel

645

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

646

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

647

648

# Welcome message for specific channel

649

if after.channel.name == "General Voice":

650

channel = discord.utils.get(member.guild.text_channels, name="general")

651

if channel:

652

await channel.send(f"πŸ‘‹ {member.mention} joined voice chat!")

653

654

# User left a voice channel

655

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

656

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

657

658

# If bot is alone in voice channel, disconnect

659

if before.channel.guild.voice_client:

660

voice_client = before.channel.guild.voice_client

661

if len(voice_client.channel.members) == 1: # Only bot left

662

await voice_client.disconnect()

663

664

channel = discord.utils.get(member.guild.text_channels, name="general")

665

if channel:

666

await channel.send("πŸ”‡ Left voice channel (nobody else listening)")

667

668

# User moved between voice channels

669

elif before.channel != after.channel:

670

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

671

672

# Voice state changes (mute, deafen, etc.)

673

if before.self_mute != after.self_mute:

674

if after.self_mute:

675

print(f"{member} muted themselves")

676

else:

677

print(f"{member} unmuted themselves")

678

679

if before.self_deaf != after.self_deaf:

680

if after.self_deaf:

681

print(f"{member} deafened themselves")

682

else:

683

print(f"{member} undeafened themselves")

684

```

685

686

### Opus Audio Processing

687

688

Low-level opus audio encoding and decoding functionality for advanced voice applications.

689

690

```python { .api }

691

# Opus Module Functions

692

def load_opus(name: str) -> None:

693

"""Load a specific opus library by name."""

694

695

def is_loaded() -> bool:

696

"""Check if opus library is loaded and available."""

697

698

# Opus Encoder/Decoder Classes

699

class Encoder:

700

"""Opus audio encoder for voice data."""

701

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

702

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

703

704

class Decoder:

705

"""Opus audio decoder for voice data."""

706

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

707

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

708

709

# Opus Exceptions

710

class OpusError(DiscordException):

711

"""Exception raised when opus operation fails."""

712

713

class OpusNotLoaded(DiscordException):

714

"""Exception raised when opus library is not loaded."""

715

```