diff --git a/docs/voice/advanced-playing.md b/docs/voice/advanced-playing.md
new file mode 100644
index 00000000..5ea61d18
--- /dev/null
+++ b/docs/voice/advanced-playing.md
@@ -0,0 +1,227 @@
+---
+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
+
+In the [previous guide](./playing.md), standard audio sources that Pycord provides were covered. 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.
+
+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`).
+
+```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
+ # 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.
+
+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,
+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:443", # 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
+```
+
+
+
+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(song)
+ await ctx.respond(f"Now playing: `{track.title}`")
+```
+
+
+
+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.listen()
+async def on_connect() -> None:
+ await sl_client.start() # Starting the client & connect all nodes
+
+@bot.event
+async def on_sonolink_node_ready(payload: sonolink.gateway.ReadyEvent) -> None:
+ print(f"Node with ID {payload.node.id!r} has connected!")
+ print(f"Resumed session: {payload.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.
+
+!!! info "Related Topics"
+
+ - [Rules and Common Practices](../getting-started/rules-and-common-practices.md)
diff --git a/docs/voice/index.md b/docs/voice/index.md
index 88b27eba..7c111a63 100644
--- a/docs/voice/index.md
+++ b/docs/voice/index.md
@@ -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.
diff --git a/docs/voice/playing.md b/docs/voice/playing.md
index 839ea115..70f16e47 100644
--- a/docs/voice/playing.md
+++ b/docs/voice/playing.md
@@ -1,132 +1,169 @@
---
-title: Wavelink Audio Player
+title: Playing Audio in Voice Channels
---
# About
-Pycord and Wavelink try to keep the playing of audio as simple and easy as possible, to keep making Discord
-bots of any kind easy for all audiences. This guide provides simple and easy examples of using the
-audio playing feature.
+Pycord offers multiple ways to play audio streams in a voice channel keeping it as simple
+and easy as possible, so making any kind of Discord bot is easy for all audiences. This
+guide provides simple and easy examples of using the multiple ways the library allows
+you to play audio.
For users that want extra examples, you can find some in Pycord's
-[GitHub repository](https://github.com/Pycord-Development/pycord/blob/master/examples/).
+[Github repository](https://github.com/Pycord-Development/pycord/blob/master/examples/).
## Starting out
-First you need to run a [Lavalink Server](https://github.com/freyacodes/Lavalink) to connect with.
-There a multiple documentations to do this, so we are not covering that here.
+Pycord natively provides an [`discord.AudioSource`](https://docs.pycord.dev/en/stable/api/voice.html#discord.AudioSource)
+object. This object defines the structure audio sources must follow in order to be accepted by
+the library. For more information on this structure, check the respective documentation.
-You also need to install the [wavelink](https://github.com/PythonistaGuild/Wavelink) library.
+You usually do not need to manually create subclasses of this object as the library also provides you
+with the most common audio source types.
-```py title="Installing wavelink"
-python3 -m pip install wavelink
-```
+### [`PCMAudio`](https://docs.pycord.dev/en/stable/api/voice.html#discord.PCMAudio)
+
+`PCMAudio` is the base class designed to read raw, uncompressed 16-bit 48kHz stereo PCM bytes
+directly from an existing byte stream or file. This essentially means that common audio formats
+such as `mp3`, `wav`, or `ogg` are not valid, as they are not PCM streams (check `FFmpegPCMAudio`).
-You will now want to connect to your server via a node.
+You should only use `PCMAudio` in specialized or advanced scenarios where you have raw PCM bytes
+(such as live audio synthesis or reading from pre-recorded `.pcm` data) without needing external
+tools.
-```py title="Connect Node with Lavalink"
+```py title="Using PCMAudio"
+import io
import discord
-import wavelink
bot = discord.Bot()
+@bot.command()
+async def play(ctx: discord.ApplicationContext) -> None:
+ if not ctx.voice_client:
+ await ctx.author.voice.channel.connect()
-async def connect_nodes():
- """Connect to our Lavalink nodes."""
- await bot.wait_until_ready() # wait until the bot is ready
-
- nodes = [
- wavelink.Node(
- identifier="Node1", # This identifier must be unique for all the nodes you are going to use
- uri="http://0.0.0.0:443", # Protocol (http/s) is required, port must be 443 as it is the one lavalink uses
- password="youshallnotpass",
- )
- ]
+ with open("raw_audio.pcm", "rb") as f:
+ # we use io.BytesIO as a container for the bytes we are reading
+ pcm_data = io.BytesIO(f.read())
- await wavelink.Pool.connect(nodes=nodes, client=bot) # Connect our nodes
+ source = discord.PCMAudio(pcm_data)
+ ctx.voice_client.play(source)
+ await ctx.respond("Playing PCM audio.")
```
-
+### [`FFmpegPCMAudio`](https://docs.pycord.dev/en/stable/api/voice.html#discord.FFmpegPCMAudio)
-Now you are finished making your node! Next, you will want to:
+Unlike `PCMAudio`, `FFmpegPCMAudio` allows you to pass different audio file formats and automatically
+decode it to PCM. This is done by using the `ffmpeg` (or `avconv`) executables to convert these audio
+streams into manipulable PCM data in a dedicated sub-process.
-1. Making a play command
-1. Adding connect events
+This intermediate PCM step is slightly more CPU-intensive, as it has to encode the raw audio again into
+Opus in order to be transmitted to Discord. However, having access to the raw PCM is necessary if you
+want to manipulate or modify the audio.
-### Making a play command
+```py title="Using FFmpegPCMAudio"
+import discord
-To make a play command, you will need to make a function to connect and play audio in a voice channel.
+bot = discord.Bot()
-```py title="Play Command Example"
-import typing
+@bot.command()
+async def play(ctx: discord.ApplicationContext) -> None:
+ if not ctx.voice_client:
+ await ctx.author.voice.channel.connect()
+
+ # unlike PCMAudio, you do not need to manually open and read
+ # the file, the library will do it for you.
+ # FFmpegPCMAudio takes a path to a valid audio file as its first parameter
+ # this means that both relative and absolute paths are allowed
+ source = discord.FFmpegPCMAudio("audio_file.mp3")
+ ctx.voice_client.play(source)
+ await ctx.respond("Playing FFmpeg PCM audio.")
+```
+### [`PCMVolumeTransformer`](https://docs.pycord.dev/en/stable/api/voice.html#discord.PCMVolumeTransformer)
-@bot.slash_command(name="play")
-async def play(ctx, search: str):
- # First we may define our voice client,
- # for this, we are going to use typing.cast()
- # function just for the type checker know that
- # `ctx.voice_client` is going to be from type
- # `wavelink.Player`
- vc = typing.cast(wavelink.Player, ctx.voice_client)
+`PCMVolumeTransformer` is an audio source that simplifies the volume changing process in PCM sources
+(such as `PCMAudio` or `FFmpegPCMAudio`) by doing it for you.
- if not vc: # We firstly check if there is a voice client
- vc = await ctx.author.voice.channel.connect(
- cls=wavelink.Player
- ) # If there isn't, we connect it to the channel
+This also allows you to dynamically change the volume during playback, by just setting a new value to the
+`volume` property.
- # Now we are going to check if the invoker of the command
- # is in the same voice channel than the voice client, when defined.
- # If not, we return an error message.
- if ctx.author.voice.channel.id != vc.channel.id:
- return await ctx.respond("You must be in the same voice channel as the bot.")
+```py title="Using PCMVolumeTransformer"
+import discord
- # Now we search for the song. You can optionally
- # pass the "source" keyword, of type "wavelink.TrackSource"
- song = await wavelink.Playable.search(search)
+bot = discord.Bot()
- if not song: # In case the song is not found
- return await ctx.respond("No song found.") # we return an error message
+@bot.command()
+async def play(ctx: discord.ApplicationContext, volume: float = 1.0) -> None:
+ if not ctx.voice_client:
+ await ctx.author.voice.channel.connect()
- await vc.play(song) # Else, we play it
- await ctx.respond(f"Now playing: `{song.title}`") # and return a success message
-```
+ # this can be any non-opus source (such as discord.PCMAudio or discord.FFmpegPCMAudio)
+ original_source = ...
-```
-# link with playing-now-playing.json
+ source = discord.PCMVolumeTransformer(original_source, volume=volume)
+ ctx.voice_client.play(source)
+ await ctx.respond("Playing PCM volume transformed audio.")
+
+
+@bot.command()
+async def volume(ctx: discord.ApplicationContext, *, volume: float) -> None:
+ if not ctx.voice_client or not ctx.voice_client.source:
+ await ctx.respond("Connect to a voice channel and play something first!")
+ return
+
+ ctx.voice_client.source.volume = volume
+ await ctx.respond(f"Changed the audio volume to {volume}")
```
-
+### [`FFmpegOpusAudio`](https://docs.pycord.dev/en/stable/api/voice.html#discord.FFmpegOpusAudio)
-Now that you've done this, the only thing left to do is make your connect events.
+This is similar to `FFmpegPCMAudio`, but, as the name suggests, this does not produce manipulable PCM
+streams and directly encodes audio streams to Opus.
-### Adding connect events
+This is the most efficient and recommended way to play local media files or web streams directly into
+a voice channel **only when no extra processing (such as changing volume) is required**.
-The final step to this guide is connecting the node to your server when the bot goes online.
+`FFmpegOpusAudio` also uses `ffmpeg` (or `avconv`) to encode common audio formats to Opus, which is the
+native audio format Discord requires for voice transmission.
-To make it, you will want to do the following:
+Unlike other sources, this one has to be initialized by using the `FFmpegOpusAudio.from_probe` classmethod.
+This analyzes the audio to obtain the codec and bitrate of the audio, so it uses the fastest and
+most efficient way to encode it to Opus, this is done by using `ffprobe` (or `avprobe`).
-```py title="Adding connect events"
-@bot.event
-async def on_ready():
- await connect_nodes() # connect to the server
+```py title="Initializing a FFmpegOpusAudio"
+source = await discord.FFmpegOpusAudio.from_probe("audio.webm")
+voice_client.play(source)
+```
+`FFmpegOpusAudio.from_probe` also allows you to pass a custom `method`, used to determine the codec and
+bitrate of the audio source. This can be a string defining whether to use the `native` probe
+(`ffprobe` / `avprobe`), or `fallback`, which falls back to use `ffmpeg` / `avconv`. The latter
+may be used by Windows users when none of `ffprobe` or `avprobe` is installed.
-@bot.event
-async def on_wavelink_node_ready(payload: wavelink.NodeReadyEventPayload):
- # Everytime a node is successfully connected, we
- # will print a message letting it know.
- print(f"Node with ID {payload.session_id} has connected")
- print(f"Resumed session: {payload.resumed}")
+This can also take custom functions which take two parameters, the `source` and the `executable`, and
+should return a tuple of `(codec, bitrate)`.
+
+```py title="Using FFmpegOpusAudio"
+import discord
+
+bot = discord.Bot()
+@bot.command()
+async def play(ctx: discord.ApplicationContext) -> None:
+ if not ctx.voice_client:
+ await ctx.author.voice.channel.connect()
-bot.run("token")
+ source = await discord.FFmpegOpusAudio.from_probe("audio.wav")
+ ctx.voice_client.play(source)
+ await ctx.respond("Playing FFmpeg Opus audio.")
```
-Congratulations! You have now implemented voice playing into your bot! 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 even the most advanced of ideas down.
+And... congratulations! You now know how to implement audio sources for voice channel playback into your
+bot! Some audio sources may look more complex than others, but all of them keep an easy and simple design
+so you can make complex bots and get even the most advanced of ideas down.
!!! info "Related Topics"
- [Rules and Common Practices](../getting-started/rules-and-common-practices.md)
+ - [Advanced Audio Playback](./advanced-playing.md)
diff --git a/zensical.toml b/zensical.toml
index d3a7fecc..f9c2efc4 100644
--- a/zensical.toml
+++ b/zensical.toml
@@ -20,7 +20,7 @@ nav = [
{ "Getting Started" = ["getting-started/index.md", { "Creating Your First Bot" = "getting-started/creating-your-first-bot.md" }, { "Rules and Common Practices" = "getting-started/rules-and-common-practices.md" }, { "More Features" = "getting-started/more-features.md" }, { "Hosting Your Pycord Bot" = "getting-started/hosting-your-bot.md" }] },
{ "Interactions" = ["interactions/index.md", { "Application Commands" = [{ "Slash Commands" = "interactions/application-commands/slash-commands.md" }, { "Context Menus" = "interactions/application-commands/context-menus.md" }, { "Localizations" = "interactions/application-commands/localizations.md" }] }, { "UI Components" = [{ "Buttons" = "interactions/ui-components/buttons.md" }, { "Select Menus" = "interactions/ui-components/dropdowns.md" }, { "Modal Dialogs" = "interactions/ui-components/modal-dialogs.md" }] }] },
{ "Extensions" = ["extensions/index.md", { "Commands" = [{ "Prefixed Commands" = "extensions/commands/prefixed-commands.md" }, { "Command Groups" = "extensions/commands/groups.md" }, { "Help Command" = "extensions/commands/help-command.md" }] }, { "Pages" = [{ "Paginator Basics" = "extensions/pages/paginator-basics.md" }, { "Paginator FAQ" = "extensions/pages/paginator-faq.md" }] }, { "Bridge" = "extensions/bridge.md" }, { "Tasks" = "extensions/tasks/tasks.md" }] },
- { "Voice" = ["voice/index.md", { "Wavelink Audio Player" = "voice/playing.md" }, { "Receiving Voice Samples" = "voice/receiving.md" }] },
+ { "Voice" = ["voice/index.md", { "Playing Audio in Voice" = "voice/playing.md" }, { "Advanced Audio Playing" = "voice/advanced-playing.md" }, { "Receiving Voice Samples" = "voice/receiving.md" }] },
{ "Popular Topics" = ["popular-topics/index.md", { "Cogs" = "popular-topics/cogs.md" }, { "Error Handling" = "popular-topics/error-handling.md" }, { "Intents" = "popular-topics/intents.md" }, { "Sharding" = "popular-topics/sharding.md" }, { "Subclassing Bots" = "popular-topics/subclassing-bots.md" }, { "Threads" = "popular-topics/threads.md" }] },
{ "More" = ["more/index.md", { "Community Resources" = "more/community-resources.md" }, { "Contributing to the Guide" = "more/contributing.md" }, { "Installing Git" = "more/git.md" }, { "Virtual Environments" = "more/virtual-environments.md" }] },
]