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