Skip to content

Commit 97ae0cd

Browse files
DA-344vmphasePaillat-dev
authored
feat: Voice guide rewrite (#556)
Co-authored-by: Michael <vmphase.dev@gmail.com> Co-authored-by: Paillat-dev <me@paillat.dev> Co-authored-by: Paillat <paillat@pycord.dev> Signed-off-by: DA344 <108473820+DA-344@users.noreply.github.com>
1 parent e743e31 commit 97ae0cd

4 files changed

Lines changed: 343 additions & 81 deletions

File tree

‎docs/voice/advanced-playing.md‎

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
---
2+
title: Advanced Audio Playing
3+
---
4+
5+
# About
6+
7+
Pycord provides built-in audio sources for common use cases, but complex bots often require greater
8+
control over audio streams or need to handle voice playback across hundreds of servers simultaneously.
9+
This guide covers advanced audio concepts, such as writing custom audio source implementations
10+
and offloading voice processing to standalone audio nodes, using Lavalink.
11+
12+
## Starting out
13+
14+
[Playing Audio in Voice Channels](./playing.md) has covered Pycord-provided standard audio sources. While
15+
these built-in classes cover most general needs, [`discord.AudioSource`](https://docs.pycord.dev/en/stable/api/voice.html#discord.AudioSource)
16+
can be subclassed to create custom audio pipelines, synthesizers, or memory stream handlers.
17+
18+
However, as a bot grows larger, handling multiple local FFmpeg processes and real-time encoding on the
19+
same machine your bot is running on can quickly bottleneck CPU performance. To solve this, advanced
20+
bot architectures decouple playback entirely by relying on external audio servers such as Lavalink and
21+
interacting with them through lightweight client libraries like SonoLink.
22+
23+
### Custom `AudioSource`s
24+
25+
All audio sources covered in the previous guide derive from `discord.AudioSource`, and implement
26+
at least a custom `read` method.
27+
28+
This method should return 20ms worth of audio, following the 16-bit 48kHz stereo PCM requirement.
29+
You may override `is_opus` when the audio returned by the `read` method is already Opus encoded.
30+
31+
When no more audio is available, you can return an empty bytes-like object to tell the library the
32+
audio reading is over.
33+
34+
You may also override `cleanup` when extra clean-up processes are needed if the reading is complete.
35+
36+
A common use case for subclassing an `AudioSource` is when you want to implement an audio player that extracts audio from an online source, such as YouTube (using `youtube_dl`/`yt_dlp`).
37+
38+
```py title="Implementing a custom audio source"
39+
import asyncio
40+
41+
import discord
42+
import youtube_dl # third-party library that requires installing
43+
44+
ytdl = youtube_dl.YoutubeDL()
45+
46+
47+
# we will implement a basic YTDLSource
48+
class YTDLSource(discord.AudioSource):
49+
# we will take a processed audio source, to simplify our handling
50+
def __init__(self, processed_source: discord.AudioSource, *, data: dict) -> None:
51+
self.processed_source: discord.AudioSource = processed_source
52+
self.data: dict = data
53+
54+
self.title = data.get("title")
55+
self.url = data.get("url")
56+
57+
# reading from this source is essentially reading from the processed source
58+
def read(self) -> bytes:
59+
return self.processed_source.read()
60+
61+
def is_opus(self) -> bool:
62+
return self.processed_source.is_opus()
63+
64+
def cleanup(self) -> None:
65+
self.processed_source.cleanup()
66+
67+
@classmethod
68+
async def from_url(cls, url: str) -> YTDLSource:
69+
# here, we will implement our logic for obtaining the youtube audio from an url
70+
# we will be using asyncio.to_thread, because extracting the info is a sync operation
71+
# and may block our bot for the duration it takes to finish
72+
data = await asyncio.to_thread(ytdl.extract_info, url, download=True)
73+
74+
# playlists have an "entries" key, so grab the first item
75+
if "entries" in data:
76+
data = data["entries"][0]
77+
78+
filename = ytdl.prepare_filename(data)
79+
return cls(discord.FFmpegPCMAudio(filename), data=data)
80+
```
81+
82+
For a more advanced explanation and implementation of this source, check
83+
[the `basic_voice.py` example](https://github.com/Pycord-Development/pycord/blob/master/examples/basic_voice.py).
84+
85+
### Using Lavalink
86+
87+
Lavalink is the ultimate audio playback server tool, as it can handle both remote and local audio
88+
playing while being easy to handle and set up, and low-usage.
89+
90+
First, you need to run a [Lavalink Server](https://github.com/lavalink-devs/Lavalink) to connect with.
91+
In case you do not know how, there are multiple documentation to do so that will not be covered here,
92+
but we recommend you the [SonoLink Lavalink Setup Guide](https://sonolink.readthedocs.io/en/latest/guides/lavalink-setup.html).
93+
94+
To interact with your Lavalink server you must send HTTP requests, but here we will be using
95+
[SonoLink](https://github.com/sonolink/sonolink), an API wrapper for Lavalink.
96+
97+
To install it, you can simply run:
98+
99+
```sh title="Installing sonolink"
100+
pip install -U sonolink
101+
```
102+
103+
Now, you will need to connect to your Lavalink server using a Node:
104+
105+
```py title="Connecting to Lavalink"
106+
import discord
107+
import sonolink
108+
109+
bot = discord.Bot()
110+
sl_client = sonolink.Client(bot)
111+
112+
sl_client.create_node(
113+
id="main-node", # the unique ID for the node being created
114+
uri="http://0.0.0.0:2333", # HTTP(S) protocol is required when passing URI
115+
password="youshallnotpass"
116+
)
117+
118+
@bot.listen()
119+
async def on_connect() -> None:
120+
await sl_client.start() # starts connection to the created nodes
121+
print("SonoLink nodes connected successfully!")
122+
```
123+
124+
<br />
125+
126+
Now you are finished making your node! Next, you will want to:
127+
128+
1. Make a `play` command
129+
1. Add connection-handling events
130+
131+
#### Making a `play` command
132+
133+
The core functionality for a music bot using SonoLink is a `play` command, as it allows users to reproduce
134+
their own songs as they please.
135+
136+
To do this, you need to create a command that ensures a `sonolink.Player` instance is connected
137+
and available to play audio.
138+
139+
```py title="Creating a play command"
140+
@bot.slash_command()
141+
async def play(ctx: discord.ApplicationContext, *, search: str) -> None:
142+
# Before proceeding with the logic, we must check the ctx.author is
143+
# connected in a voice channel
144+
if not ctx.author.voice or not ctx.author.voice.channel:
145+
await ctx.respond("You must be in a voice channel first!")
146+
return
147+
148+
# We need to check we are connected to a voice channel AND the
149+
# voice client connected is a `sonolink.Player` instance.
150+
vc = ctx.voice_client
151+
152+
# Connect to the voice channel if we are not yet
153+
if not vc:
154+
vc = await ctx.author.voice.channel.connect(
155+
cls=sonolink.Player,
156+
)
157+
elif not isinstance(vc, sonolink.Player):
158+
# And here, if there is a voice client connected, check it is
159+
# a sonolink.Player instance.
160+
# If not, reconnect with a sonolink.Player instance
161+
await vc.disconnect(force=True)
162+
vc = await ctx.author.voice.channel.connect(
163+
cls=sonolink.Player,
164+
)
165+
166+
# This check is not required, but creates a better experience for final users
167+
# We will check that the channel the Player is connected is the same the ctx.author
168+
# is in.
169+
if ctx.author.voice.channel.id != vc.channel.id:
170+
await ctx.respond("You must be in the same voice channel as the bot.")
171+
return
172+
173+
# We will now search for the song the user provided in the `search` parameter
174+
# We can optionally pass a `source` keyword argument to reduce the locations
175+
# the song will be search from.
176+
result = await sl_client.search_track(search)
177+
178+
# SonoLink returns a SearchResult instance when searching tracks
179+
# so we will need to check that the result is not empty and is not an error
180+
if result.is_empty() or result.is_error() or not result.result:
181+
await ctx.respond("Song not found!")
182+
return
183+
184+
# The fetched result can be a list of tracks, a playlist or a single track
185+
# so we must check against all those possibilities
186+
if isinstance(result.result, list):
187+
track = result.result[0]
188+
elif isinstance(result.result, sonolink.models.Playlist):
189+
track = result.result.tracks[0]
190+
else:
191+
track = result.result
192+
193+
# And finally... we play the track we obtained
194+
await vc.play(track)
195+
await ctx.respond(f"Now playing: `{track.title}`")
196+
```
197+
198+
<br />
199+
200+
Now that this is done, the only thing left to do is make your connect events.
201+
202+
#### Adding connect events
203+
204+
The final step of this guide is connecting to the node to your server when the bot goes online.
205+
206+
To make it, you will want to do the following:
207+
208+
```py title="Adding connect events"
209+
@bot.event
210+
async def on_sonolink_node_ready(event: sonolink.gateway.ReadyEvent) -> None:
211+
print(f"Node with ID {event.node.id!r} has connected!")
212+
print(f"Resumed session: {event.resumed}")
213+
214+
bot.run("token")
215+
```
216+
217+
Congratulations! You are now able to create custom audio sources for advanced playback, and to offload
218+
the playback to an external node using Lavalink and interacting with it using SonoLink! Most bots and
219+
Discord API wrappers don't have this as a feature, so this is quite an accomplishment. Thankfully,
220+
Pycord makes it easy to make complex bots so that you can get the most advanced of ideas down.
221+
222+
!!! info "Related Topics"
223+
224+
- [Rules and Common Practices](../getting-started/rules-and-common-practices.md)

‎docs/voice/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ The following features are optional:
2020
Some hosts may not come with it, though, so remember to always check if required dependencies are installed.
2121

2222
- [`FFmpeg`](https://ffmpeg.org) - used for sending/receiving files other than `.pcm` and `.wav`
23-
- [`Pycord.Wavelink`](https://github.com/Pycord-Development/Pycord.Wavelink),
23+
- [`SonoLink`](https://github.com/sonolink/sonolink) - used for advanced audio playback
2424
[`Lavalink.py`](https://github.com/Devoxin/Lavalink.py),
2525
[`Wavelink`](https://github.com/PythonistaGuild/Wavelink) or any other Python LavaLink library for music playback.

0 commit comments

Comments
 (0)