-
Notifications
You must be signed in to change notification settings - Fork 63
feat: Voice guide rewrite #556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
092ebbb
147fd64
9fe8b37
d4b6037
79036de
7810944
319ad52
4c9dccb
1046f50
fd66ffb
de121e8
503e76f
979d2f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,224 @@ | ||||||||||||||||||
| --- | ||||||||||||||||||
| title: Advanced Audio Playing | ||||||||||||||||||
| --- | ||||||||||||||||||
|
|
||||||||||||||||||
| # About | ||||||||||||||||||
|
|
||||||||||||||||||
| Pycord provides built-in audio sources for common use cases, but complex bots often require greater | ||||||||||||||||||
| control over audio streams or need to handle voice playback across hundreds of servers simultaneously. | ||||||||||||||||||
| This guide covers advanced audio concepts, such as writing custom audio source implementations | ||||||||||||||||||
| and offloading voice processing to standalone audio nodes, using Lavalink. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Starting out | ||||||||||||||||||
|
|
||||||||||||||||||
| [Playing Audio in Voice Channels](./playing.md) has covered Pycord-provided standard audio sources. While | ||||||||||||||||||
| these built-in classes cover most general needs, [`discord.AudioSource`](https://docs.pycord.dev/en/stable/api/voice.html#discord.AudioSource) | ||||||||||||||||||
| can be subclassed to create custom audio pipelines, synthesizers, or memory stream handlers. | ||||||||||||||||||
|
|
||||||||||||||||||
| However, as a bot grows larger, handling multiple local FFmpeg processes and real-time encoding on the | ||||||||||||||||||
| same machine as your bot's running can quickly bottleneck CPU performance. To solve this, advanced | ||||||||||||||||||
| bot architectures decouple playback entirely by relying on external audio servers such as Lavalink and | ||||||||||||||||||
| interacting with them through lightweight client libraries like SonoLink. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Custom `AudioSource`s | ||||||||||||||||||
|
|
||||||||||||||||||
| All audio sources covered in the previous guide derive from `discord.AudioSource`, and implement | ||||||||||||||||||
| at least a custom `read` method. | ||||||||||||||||||
|
|
||||||||||||||||||
| This method should return 20ms worth of audio, following the 16-bit 48kHz stereo PCM requirement. | ||||||||||||||||||
| You may override `is_opus` when the audio returned by the `read` method is already Opus encoded. | ||||||||||||||||||
|
|
||||||||||||||||||
| When no more audio is available, you can return an empty bytes-like object to tell the library the | ||||||||||||||||||
| audio reading is over. | ||||||||||||||||||
|
|
||||||||||||||||||
| You may also override `cleanup` when extra clean-up processes are needed when the reading is complete. | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| A common usecase 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`). | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| ```py title="Implementing a custom audio source" | ||||||||||||||||||
| import asyncio | ||||||||||||||||||
|
|
||||||||||||||||||
| import discord | ||||||||||||||||||
| import youtube_dl # third-party library that requires installing | ||||||||||||||||||
|
|
||||||||||||||||||
| ytdl = youtube_dl.YoutubeDL() | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| # we will implement a basic YTDLSource | ||||||||||||||||||
| class YTDLSource(discord.AudioSource): | ||||||||||||||||||
| # we will take a processed audio source, to simplify our handling | ||||||||||||||||||
| def __init__(self, processed_source: discord.AudioSource, *, data: dict) -> None: | ||||||||||||||||||
| self.processed_source: discord.AudioSource = processed_source | ||||||||||||||||||
| self.data: dict = data | ||||||||||||||||||
|
|
||||||||||||||||||
| self.title = data.get("title") | ||||||||||||||||||
| self.url = data.get("url") | ||||||||||||||||||
|
|
||||||||||||||||||
| # reading from this source is essentially reading from the processed source | ||||||||||||||||||
| def read(self) -> bytes: | ||||||||||||||||||
| return self.processed_source.read() | ||||||||||||||||||
|
|
||||||||||||||||||
| def is_opus(self) -> bool: | ||||||||||||||||||
| return self.processed_source.is_opus() | ||||||||||||||||||
|
|
||||||||||||||||||
| def cleanup(self) -> None: | ||||||||||||||||||
| self.processed_source.cleanup() | ||||||||||||||||||
|
|
||||||||||||||||||
| @classmethod | ||||||||||||||||||
| async def from_url(cls, url: str) -> YTDLSource: | ||||||||||||||||||
| # here, we will implement our logic from obtaining the youtube audio from an url | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
| # we will be using asyncio.to_thread, because extracting the info is a sync operation | ||||||||||||||||||
| # and may block our bot for the duration it takes to finish | ||||||||||||||||||
| data = await asyncio.to_thread(ytdl.extract_info, url, download=True) | ||||||||||||||||||
|
|
||||||||||||||||||
| # playlists have an "entries" key, so grab the first item | ||||||||||||||||||
| if "entries" in data: | ||||||||||||||||||
| data = data["entries"][0] | ||||||||||||||||||
|
|
||||||||||||||||||
| filename = ytdl.prepare_filename(data) | ||||||||||||||||||
| return cls(discord.FFmpegPCMAudio(filename), data=data) | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| For a more advanced explanation and implementation of this source, check | ||||||||||||||||||
| [the `basic_voice.py` example](https://github.com/Pycord-Development/pycord/blob/master/examples/basic_voice.py). | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Using Lavalink | ||||||||||||||||||
|
|
||||||||||||||||||
| Lavalink is the ultimate audio playback server tool, as it can handle both remote and local audio | ||||||||||||||||||
| playing while being easy to handle and setup, and low-usage. | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| First, you need to run a [Lavalink Server](https://github.com/lavalink-devs/Lavalink) to connect with. | ||||||||||||||||||
| In case you do not know how, there are multiple documentations to do so that will not be covered here, | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation is uncountable.
Suggested change
|
||||||||||||||||||
| but we recommend you the [SonoLink Lavalink Setup Guide](https://sonolink.readthedocs.io/en/latest/guides/lavalink-setup.html). | ||||||||||||||||||
|
|
||||||||||||||||||
| To interact with your Lavalink server you must send HTTP requests, but here we will be using | ||||||||||||||||||
| [SonoLink](https://github.com/sonolink/sonolink), an API wrapper for Lavalink. | ||||||||||||||||||
|
|
||||||||||||||||||
| To install it, you can simply run: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```sh title="Installing sonolink" | ||||||||||||||||||
| pip install -U sonolink | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| Now, you will need to connect to your Lavalink server using a Node: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```py title="Connecting to Lavalink" | ||||||||||||||||||
| import discord | ||||||||||||||||||
| import sonolink | ||||||||||||||||||
|
|
||||||||||||||||||
| bot = discord.Bot() | ||||||||||||||||||
| sl_client = sonolink.Client(bot) | ||||||||||||||||||
|
|
||||||||||||||||||
| sl_client.create_node( | ||||||||||||||||||
| id="main-node", # the unique ID for the node being created | ||||||||||||||||||
| uri="http://0.0.0.0:2333", # HTTP(S) protocol is required when passing URI | ||||||||||||||||||
| password="youshallnotpass" | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| @bot.listen() | ||||||||||||||||||
| async def on_connect() -> None: | ||||||||||||||||||
| await sl_client.start() # starts connection to the created nodes | ||||||||||||||||||
|
DA-344 marked this conversation as resolved.
|
||||||||||||||||||
| print("SonoLink nodes connected successfully!") | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| <br /> | ||||||||||||||||||
|
|
||||||||||||||||||
| Now you are finished making your node! Next, you will want to: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. Make a `play` command | ||||||||||||||||||
| 2. Add connection-handling events | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Making a `play` command | ||||||||||||||||||
|
|
||||||||||||||||||
| The core functionality for a music bot using SonoLink is a `play` command, as it allows users to reproduce | ||||||||||||||||||
| their own songs as they please. | ||||||||||||||||||
|
|
||||||||||||||||||
| To do this, you need to create a command that ensures a `sonolink.Player` instance is connected | ||||||||||||||||||
| and available to play audio. | ||||||||||||||||||
|
|
||||||||||||||||||
| ```py title="Creating a play command" | ||||||||||||||||||
| @bot.slash_command() | ||||||||||||||||||
| async def play(ctx: discord.ApplicationContext, *, search: str) -> None: | ||||||||||||||||||
| # Before proceeding with the logic, we must check the ctx.author is | ||||||||||||||||||
| # connected in a voice channel | ||||||||||||||||||
| if not ctx.author.voice or not ctx.author.voice.channel: | ||||||||||||||||||
| await ctx.respond("You must be in a voice channel first!") | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| # We need to check we are connected to a voice channel AND the | ||||||||||||||||||
| # voice client connected is a `sonolink.Player` instance. | ||||||||||||||||||
| vc = ctx.voice_client | ||||||||||||||||||
|
|
||||||||||||||||||
| # Connect to the voice channel if we are not yet | ||||||||||||||||||
| if not vc: | ||||||||||||||||||
| vc = await ctx.author.voice.channel.connect( | ||||||||||||||||||
| cls=sonolink.Player, | ||||||||||||||||||
| ) | ||||||||||||||||||
| elif not isinstance(vc, sonolink.Player): | ||||||||||||||||||
| # And here, if there is a voice client connected, check it is | ||||||||||||||||||
| # a sonolink.Player instance. | ||||||||||||||||||
| # If not, reconnect with a sonolink.Player instance | ||||||||||||||||||
| await vc.disconnect(force=True) | ||||||||||||||||||
| vc = await ctx.author.voice.channel.connect( | ||||||||||||||||||
| cls=sonolink.Player, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| # This check is not required, but creates a better experience for final users | ||||||||||||||||||
| # We will check that the channel the Player is connected is the same the ctx.author | ||||||||||||||||||
| # is in. | ||||||||||||||||||
| if ctx.author.voice.channel.id != vc.channel.id: | ||||||||||||||||||
| await ctx.respond("You must be in the same voice channel as the bot.") | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| # We will now search for the song the user provided in the `search` parameter | ||||||||||||||||||
| # We can optionally pass a `source` keyword argument to reduce the locations | ||||||||||||||||||
| # the song will be search from. | ||||||||||||||||||
| result = await sl_client.search_track(search) | ||||||||||||||||||
|
|
||||||||||||||||||
| # SonoLink returns a SearchResult instance when searching tracks | ||||||||||||||||||
| # so we will need to check that the result is not empty and is not an error | ||||||||||||||||||
| if result.is_empty() or result.is_error() or not result.result: | ||||||||||||||||||
| await ctx.respond("Song not found!") | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| # The fetched result can be a list of tracks, a playlist or a single track | ||||||||||||||||||
| # so we must check against all those possibilities | ||||||||||||||||||
| if isinstance(result.result, list): | ||||||||||||||||||
| track = result.result[0] | ||||||||||||||||||
| elif isinstance(result.result, sonolink.models.Playlist): | ||||||||||||||||||
| track = result.result.tracks[0] | ||||||||||||||||||
| else: | ||||||||||||||||||
| track = result.result | ||||||||||||||||||
|
|
||||||||||||||||||
| # And finally... we play the track we obtained | ||||||||||||||||||
| await vc.play(track) | ||||||||||||||||||
| await ctx.respond(f"Now playing: `{track.title}`") | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| <br /> | ||||||||||||||||||
|
|
||||||||||||||||||
| Now that this is done, the only thing left to do is make your connect events. | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Adding connect events | ||||||||||||||||||
|
|
||||||||||||||||||
| The final step of this guide is connecting to the node to your server when the bot goes online. | ||||||||||||||||||
|
|
||||||||||||||||||
| To make it, you will want to do the following: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```py title="Adding connect events" | ||||||||||||||||||
| @bot.event | ||||||||||||||||||
| async def on_sonolink_node_ready(event: sonolink.gateway.ReadyEvent) -> None: | ||||||||||||||||||
| print(f"Node with ID {event.node.id!r} has connected!") | ||||||||||||||||||
| print(f"Resumed session: {event.resumed}") | ||||||||||||||||||
|
|
||||||||||||||||||
| bot.run("token") | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| Congratulations! You are now able to create custom audio sources for advanced playback, and offloading | ||||||||||||||||||
| the playback to an external node using Lavalink and interacting with it using Sonolink! Most bots and | ||||||||||||||||||
| Discord API wrappers don't have this as a feature, so this is quite an accomplishment. Thankfully, | ||||||||||||||||||
| Pycord makes it easy to make complex bots so that you can get the most advanced of ideas down. | ||||||||||||||||||
|
Comment on lines
+217
to
+220
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent parallel structure (infinitive + gerund).
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| !!! info "Related Topics" | ||||||||||||||||||
|
|
||||||||||||||||||
| - [Rules and Common Practices](../getting-started/rules-and-common-practices.md) | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,6 @@ The following features are optional: | |
| Some hosts may not come with it, though, so remember to always check if required dependencies are installed. | ||
|
|
||
| - [`FFmpeg`](https://ffmpeg.org) - used for sending/receiving files other than `.pcm` and `.wav` | ||
| - [`Pycord.Wavelink`](https://github.com/Pycord-Development/Pycord.Wavelink), | ||
| - [`SonoLink`](https://github.com/sonolink/sonolink) - used for advanced audio playback | ||
| [`Lavalink.py`](https://github.com/Devoxin/Lavalink.py), | ||
| [`Wavelink`](https://github.com/PythonistaGuild/Wavelink) or any other Python LavaLink library for music playback. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wavelink does not yet provide an explicit support for pycord, redundant. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.