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
```