From 092ebbb09237177e369d321c4303161fb63d1268 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:06:12 +0200 Subject: [PATCH 1/5] Start voice guides rewriting --- docs/voice/advanced-playing.md | 0 docs/voice/playing-rewritten.md | 166 ++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 docs/voice/advanced-playing.md create mode 100644 docs/voice/playing-rewritten.md diff --git a/docs/voice/advanced-playing.md b/docs/voice/advanced-playing.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/voice/playing-rewritten.md b/docs/voice/playing-rewritten.md new file mode 100644 index 00000000..f1eaef34 --- /dev/null +++ b/docs/voice/playing-rewritten.md @@ -0,0 +1,166 @@ +--- +title: Playing Audio in Voice Channels +--- + +# About + +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. + +## Starting out + +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 usually do not need to manually create subclasses of this object as the library also provides you +with the most common audio source types. + +### [`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 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="Using PCMAudio" +import io +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() + + 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()) + + 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) + +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. + +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. + +```py title="Using FFmpegPCMAudio" +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() + + # 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) + +`PCMVolumeTransformer` is an audio source that simplifies the volume changing process in PCM sources +(such as `PCMAudio` or `FFmpegPCMAudio`) by doing it for you. + +This also allows you to dynamically change the volume during playback, by just setting a new value to the +`volume` property. + +```py title="Using PCMVolumeTransformer" +import discord + +bot = discord.Bot() + +@bot.command() +async def play(ctx: discord.ApplicationContext, volume: float = 1.0) -> None: + if not ctx.voice_client: + await ctx.author.voice.channel.connect() + + # this can be any non-opus source (such as discord.PCMAudio or discord.FFmpegPCMAudio) + original_source = ... + + 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) + +This is similar to `FFmpegPCMAudio`, but, as the name suggests, this does not produce manipulable PCM +streams and directly encodes audio streams to Opus. + +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**. + +`FFmpegOpusAudio` also uses `ffmpeg` (or `avconv`) to encode common audio formats to Opus, which is the +native audio format Discord requires for voice transmission. + +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="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. + +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() + + source = await discord.FFmpegOpusAudio.from_probe("audio.wav") + ctx.voice_client.play(source) + await ctx.respond("Playing FFmpeg Opus audio.") +``` + +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) From 147fd64575be6a481e86e62bf7fa77dfa269e016 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:15:54 +0200 Subject: [PATCH 2/5] chore: update voice index, and finish advanced playing --- docs/voice/advanced-playing.md | 227 ++++++++++++++++++++++++++++++++ docs/voice/index.md | 2 +- docs/voice/playing-old.md | 132 +++++++++++++++++++ docs/voice/playing-rewritten.md | 166 ----------------------- docs/voice/playing.md | 197 ++++++++++++++++----------- 5 files changed, 477 insertions(+), 247 deletions(-) create mode 100644 docs/voice/playing-old.md delete mode 100644 docs/voice/playing-rewritten.md diff --git a/docs/voice/advanced-playing.md b/docs/voice/advanced-playing.md index e69de29b..1b73084e 100644 --- a/docs/voice/advanced-playing.md +++ 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 Practises](../getting-started/rules-and-common-practises.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-old.md b/docs/voice/playing-old.md new file mode 100644 index 00000000..839ea115 --- /dev/null +++ b/docs/voice/playing-old.md @@ -0,0 +1,132 @@ +--- +title: Wavelink Audio Player +--- + +# 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. + +For users that want extra examples, you can find some in Pycord's +[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. + +You also need to install the [wavelink](https://github.com/PythonistaGuild/Wavelink) library. + +```py title="Installing wavelink" +python3 -m pip install wavelink +``` + +You will now want to connect to your server via a node. + +```py title="Connect Node with Lavalink" +import discord +import wavelink + +bot = discord.Bot() + + +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", + ) + ] + + await wavelink.Pool.connect(nodes=nodes, client=bot) # Connect our nodes +``` + +
+ +Now you are finished making your node! Next, you will want to: + +1. Making a play command +1. Adding connect events + +### Making a play command + +To make a play command, you will need to make a function to connect and play audio in a voice channel. + +```py title="Play Command Example" +import typing + + +@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) + + 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 + + # 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.") + + # Now we search for the song. You can optionally + # pass the "source" keyword, of type "wavelink.TrackSource" + song = await wavelink.Playable.search(search) + + if not song: # In case the song is not found + return await ctx.respond("No song found.") # we return an error message + + await vc.play(song) # Else, we play it + await ctx.respond(f"Now playing: `{song.title}`") # and return a success message +``` + +``` +# link with playing-now-playing.json +``` + +
+ +Now that you've done this, the only thing left to do is make your connect events. + +### Adding connect events + +The final step to this guide is connecting 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_ready(): + await connect_nodes() # connect to the server + + +@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}") + + +bot.run("token") +``` + +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. + +!!! info "Related Topics" + + - [Rules and Common Practices](../getting-started/rules-and-common-practices.md) diff --git a/docs/voice/playing-rewritten.md b/docs/voice/playing-rewritten.md deleted file mode 100644 index f1eaef34..00000000 --- a/docs/voice/playing-rewritten.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Playing Audio in Voice Channels ---- - -# About - -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. - -## Starting out - -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 usually do not need to manually create subclasses of this object as the library also provides you -with the most common audio source types. - -### [`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 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="Using PCMAudio" -import io -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() - - 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()) - - 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) - -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. - -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. - -```py title="Using FFmpegPCMAudio" -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() - - # 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) - -`PCMVolumeTransformer` is an audio source that simplifies the volume changing process in PCM sources -(such as `PCMAudio` or `FFmpegPCMAudio`) by doing it for you. - -This also allows you to dynamically change the volume during playback, by just setting a new value to the -`volume` property. - -```py title="Using PCMVolumeTransformer" -import discord - -bot = discord.Bot() - -@bot.command() -async def play(ctx: discord.ApplicationContext, volume: float = 1.0) -> None: - if not ctx.voice_client: - await ctx.author.voice.channel.connect() - - # this can be any non-opus source (such as discord.PCMAudio or discord.FFmpegPCMAudio) - original_source = ... - - 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) - -This is similar to `FFmpegPCMAudio`, but, as the name suggests, this does not produce manipulable PCM -streams and directly encodes audio streams to Opus. - -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**. - -`FFmpegOpusAudio` also uses `ffmpeg` (or `avconv`) to encode common audio formats to Opus, which is the -native audio format Discord requires for voice transmission. - -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="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. - -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() - - source = await discord.FFmpegOpusAudio.from_probe("audio.wav") - ctx.voice_client.play(source) - await ctx.respond("Playing FFmpeg Opus audio.") -``` - -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/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) From 9fe8b37c3c05b8b6a61582c748f27fdb4adb5a89 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:16:24 +0200 Subject: [PATCH 3/5] chore: delete old playing.md --- docs/voice/playing-old.md | 132 -------------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 docs/voice/playing-old.md diff --git a/docs/voice/playing-old.md b/docs/voice/playing-old.md deleted file mode 100644 index 839ea115..00000000 --- a/docs/voice/playing-old.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Wavelink Audio Player ---- - -# 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. - -For users that want extra examples, you can find some in Pycord's -[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. - -You also need to install the [wavelink](https://github.com/PythonistaGuild/Wavelink) library. - -```py title="Installing wavelink" -python3 -m pip install wavelink -``` - -You will now want to connect to your server via a node. - -```py title="Connect Node with Lavalink" -import discord -import wavelink - -bot = discord.Bot() - - -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", - ) - ] - - await wavelink.Pool.connect(nodes=nodes, client=bot) # Connect our nodes -``` - -
- -Now you are finished making your node! Next, you will want to: - -1. Making a play command -1. Adding connect events - -### Making a play command - -To make a play command, you will need to make a function to connect and play audio in a voice channel. - -```py title="Play Command Example" -import typing - - -@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) - - 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 - - # 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.") - - # Now we search for the song. You can optionally - # pass the "source" keyword, of type "wavelink.TrackSource" - song = await wavelink.Playable.search(search) - - if not song: # In case the song is not found - return await ctx.respond("No song found.") # we return an error message - - await vc.play(song) # Else, we play it - await ctx.respond(f"Now playing: `{song.title}`") # and return a success message -``` - -``` -# link with playing-now-playing.json -``` - -
- -Now that you've done this, the only thing left to do is make your connect events. - -### Adding connect events - -The final step to this guide is connecting 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_ready(): - await connect_nodes() # connect to the server - - -@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}") - - -bot.run("token") -``` - -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. - -!!! info "Related Topics" - - - [Rules and Common Practices](../getting-started/rules-and-common-practices.md) From d4b60375ecc01e5c8ab4a48b1b83c9745f2b1e61 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:21:28 +0200 Subject: [PATCH 4/5] fix: links --- docs/voice/advanced-playing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/voice/advanced-playing.md b/docs/voice/advanced-playing.md index 1b73084e..5ea61d18 100644 --- a/docs/voice/advanced-playing.md +++ b/docs/voice/advanced-playing.md @@ -224,4 +224,4 @@ Pycord makes it easy to make complex bots so that you can get the most advanced !!! info "Related Topics" - - [Rules and Common Practises](../getting-started/rules-and-common-practises.md) + - [Rules and Common Practices](../getting-started/rules-and-common-practices.md) From 79036de815cbe296c5d7836ffdc005cc2c4abda7 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:29:58 +0200 Subject: [PATCH 5/5] chore: rename & add missing pages in zensical.toml --- zensical.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" }] }, ]