diff --git a/DisCatSharp.Docs/articles/basics/first_bot.md b/DisCatSharp.Docs/articles/basics/first_bot.md index 69628ee88..f1e613b5e 100644 --- a/DisCatSharp.Docs/articles/basics/first_bot.md +++ b/DisCatSharp.Docs/articles/basics/first_bot.md @@ -1,227 +1,227 @@ --- uid: basics_first_bot title: Your First Bot --- # Your First Bot >[!NOTE] > This article assumes the following: > * You have [created a bot account](xref:basics_bot_account "Creating a Bot Account") and have a bot token. > * You have [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) installed on your computer. ## Create a Project Open up Visual Studio and click on `Create a new project` towards the bottom right. ![Visual Studio Start Screen](/images/basics_first_bot_01.png)
Select `Console App (.NET Core)` then click on the `Next` button. ![New Project Screen](/images/basics_first_bot_02.png)
Next, you'll give your project a name. For this example, we'll name it `MyFirstBot`.
If you'd like, you can also change the directory that your project will be created in. Enter your desired project name, then click on the `Create` button. ![Name Project Screen](/images/basics_first_bot_03.png)
VoilĂ ! Your project has been created! ![Visual Studio IDE](/images/basics_first_bot_04.png) ## Install Package Now that you have a project created, you'll want to get DisCatSharp installed. Locate the *solution explorer* on the right side, then right click on `Dependencies` and select `Manage NuGet Packages` from the context menu. ![Dependencies Context Menu](/images/basics_first_bot_05.png)
You'll then be greeted by the NuGet package manager. Select the `Browse` tab towards the top left, then type `DisCatSharp` into the search text box with the Pre-release checkbox checked **ON**. ![NuGet Package Search](/images/basics_first_bot_06.png)
The first results should be the DisCatSharp packages. Package|Description :---: |:---: `DisCatSharp`|Main package; Discord API client. `DisCatSharp.CommandsNext`|Add-on which provides a command framework. `DisCatSharp.Common`|Common tools & converters `DisCatSharp.Interactivity`|Add-on which allows for interactive commands. `DisCatSharp.Lavalink`|Client implementation for [Lavalink](xref:modules_audio_lavalink_setup). Useful for music bots. `DisCatSharp.ApplicationCommands`|Add-on which makes dealing with application commands easier. `DisCatSharp.VoiceNext`|Add-on which enables connectivity to Discord voice channels. `DisCatSharp.VoiceNext.Natives`|Voice next natives.
We'll only need the `DisCatSharp` package for the basic bot we'll be writing in this article.
Select it from the list then click the `Install` button to the right (after verifying that you will be installing the **latest 4.0 version**). ![Install DisCatSharp](/images/basics_first_bot_08.png) You're now ready to write some code! ## First Lines of Code DisCatSharp implements [Task-based Asynchronous Pattern](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern). Because of this, the majority of DisCatSharp methods must be executed in a method marked as `async` so they can be properly `await`ed. Due to the way the compiler generates the underlying [IL](https://en.wikipedia.org/wiki/Common_Intermediate_Language) code, marking our `Main` method as `async` has the potential to cause problems. As a result, we must pass the program execution to an `async` method. Head back to your *Program.cs* tab and empty the `Main` method by deleting line 9. ![Code Editor](/images/basics_first_bot_09.png) Now, create a new `static` method named `MainAsync` beneath your `Main` method. Have it return type `Task` and mark it as `async`. After that, add `MainAsync().GetAwaiter().GetResult();` to your `Main` method. ```cs static void Main(string[] args) { MainAsync().GetAwaiter().GetResult(); } static async Task MainAsync() { } ``` If you typed this in by hand, Intellisense should have generated the required `using` directive for you.
However, if you copy-pasted the snippet above, VS will complain about being unable to find the `Task` type. Hover over `Task` with your mouse and click on `Show potential fixes` from the tooltip. ![Error Tooltip](/images/basics_first_bot_10.png) Then apply the recommended solution. ![Solution Menu](/images/basics_first_bot_11.png)
We'll now create a new `DiscordClient` instance in our brand new asynchronous method. Create a new variable in `MainAsync` and assign it a new `DiscordClient` instance, then pass an instance of `DiscordConfiguration` to its constructor. Create an object initializer for `DiscordConfiguration` and populate the `Token` property with your bot token then set the `TokenType` property to `TokenType.Bot`. Next add the `Intents` Property and Populated it with the @DisCatSharp.DiscordIntents.AllUnprivileged value. These Intents are required for certain Events to be fired. Please visit this [article](xref:beyond_basics_intents) for more information. ```cs var discord = new DiscordClient(new DiscordConfiguration() { Token = "My First Token", TokenType = TokenType.Bot, Intents = DiscordIntents.AllUnprivileged }); ``` >[!WARNING] > We hard-code the token in the above snippet to keep things simple and easy to understand. > > Hard-coding your token is *not* a smart idea, especially if you plan on distributing your source code. > Instead you should store your token in an external medium, such as a configuration file or environment variable, and read that into your program to be used with DisCatSharp. Follow that up with `await discord.ConnectAsync();` to connect and login to Discord, and `await Task.Delay(-1);` at the end of the method to prevent the console window from closing prematurely. ```cs var discord = new DiscordClient(); await discord.ConnectAsync(); await Task.Delay(-1); ``` As before, Intellisense will have auto generated the needed `using` directive for you if you typed this in by hand.
If you've copied the snippet, be sure to apply the recommended suggestion to insert the required directive. If you hit `F5` on your keyboard to compile and run your program, you'll be greeted by a happy little console with a single log message from DisCatSharp. Woo hoo! ![Program Console](/images/basics_first_bot_12.png) ## Spicing Up Your Bot Right now our bot doesn't do a whole lot. Let's bring it to life by having it respond to a message! Hook the `MessageCreated` event fired by `DiscordClient` with a [lambda](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions).
Mark it as `async` and give it two parameters: `s` and `e`. ```cs discord.MessageCreated += async (s, e) => { }; ``` Then, add an `if` statement into the body of your event lambda that will check if `e.Message.Content` starts with your desired trigger word and respond with a message using `e.Message.RespondAsync` if it does. For this example, we'll have the bot to respond with *pong!* for each message that starts with *ping*. ```cs discord.MessageCreated += async (s, e) => { if (e.Message.Content.ToLower().StartsWith("ping")) await e.Message.RespondAsync("pong!"); }; ``` ## The Finished Product Your entire program should now look like this: ```cs using System; using System.Threading.Tasks; using DisCatSharp; namespace MyFirstBot { class Program { static void Main(string[] args) { MainAsync().GetAwaiter().GetResult(); } static async Task MainAsync() { var discord = new DiscordClient(new DiscordConfiguration() { Token = "My First Token", TokenType = TokenType.Bot }); discord.MessageCreated += async (s, e) => { if (e.Message.Content.ToLower().StartsWith("ping")) await e.Message.RespondAsync("pong!"); }; await discord.ConnectAsync(); await Task.Delay(-1); } } } ``` Hit `F5` to run your bot, then send *ping* in any channel your bot account has access to.
Your bot should respond with *pong!* for each *ping* you send. Congrats, your bot now does something! ![Bot Response](/images/basics_first_bot_13.png) ## Further Reading Now that you have a basic bot up and running, you should take a look at the following: * [Events](xref:beyond_basics_events) -* [CommandsNext](xref:modules_commandsntext_intro) +* [CommandsNext](xref:modules_commandsnext_intro) * [ApplicationCommands](xref:modules_application_commands_intro) diff --git a/DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md b/DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md index b10382884..add84cb2e 100644 --- a/DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md +++ b/DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md @@ -1,338 +1,338 @@ --- uid: modules_audio_lavalink_music_commands title: Lavalink Music Commands --- # Adding Music Commands -This article assumes that you know how to use CommandsNext. If you do not, you should learn [here](xref:commands_intro) before continuing with this guide. +This article assumes that you know how to use CommandsNext. If you do not, you should learn [here](xref:modules_commandsnext_intro) before continuing with this guide. ## Prerequisites Before we start we will need to make sure CommandsNext is configured. For this we can make a simple configuration and command class: ```csharp using DisCatSharp.CommandsNext; namespace MyFirstMusicBot { public class MyLavalinkCommands : BaseCommandModule { } } ``` And be sure to register it in your program file: ```csharp CommandsNext = Discord.UseCommandsNext(new CommandsNextConfiguration { StringPrefixes = new string[] { ";;" } }); CommandsNext.RegisterCommands(); ``` ## Adding join and leave commands Your bot, and Lavalink, will need to connect to a voice channel to play music. Let's create the base for these commands: ```csharp [Command] public async Task Join(CommandContext ctx, DiscordChannel channel) { } [Command] public async Task Leave(CommandContext ctx, DiscordChannel channel) { } ``` In order to connect to a voice channel, we'll need to do a few things. 1. Get our node connection. You can either use linq or `GetIdealNodeConnection()` 2. Check if the channel is a voice channel, and tell the user if not. 3. Connect the node to the channel. And for the leave command: 1. Get the node connection, using the same process. 2. Check if the channel is a voice channel, and tell the user if not. 3. Get our existing connection. 4. Check if the connection exists, and tell the user if not. 5. Disconnect from the channel. `GetIdealNodeConnection()` will return the least affected node through load balancing, which is useful for larger bots. It can also filter nodes based on an optional voice region to use the closest nodes available. Since we only have one connection we can use linq's `.First()` method on the extensions connected nodes to get what we need. So far, your command class should look something like this: ```csharp using System.Threading.Tasks; using DisCatSharp; using DisCatSharp.Entities; using DisCatSharp.CommandsNext; using DisCatSharp.CommandsNext.Attributes; namespace MyFirstMusicBot { public class MyLavalinkCommands : BaseCommandModule { [Command] public async Task Join(CommandContext ctx, DiscordChannel channel) { var lava = ctx.Client.GetLavalink(); if (!lava.ConnectedNodes.Any()) { await ctx.RespondAsync("The Lavalink connection is not established"); return; } var node = lava.ConnectedNodes.Values.First(); if (channel.Type != ChannelType.Voice) { await ctx.RespondAsync("Not a valid voice channel."); return; } await node.ConnectAsync(channel); await ctx.RespondAsync($"Joined {channel.Name}!"); } [Command] public async Task Leave(CommandContext ctx, DiscordChannel channel) { var lava = ctx.Client.GetLavalink(); if (!lava.ConnectedNodes.Any()) { await ctx.RespondAsync("The Lavalink connection is not established"); return; } var node = lava.ConnectedNodes.Values.First(); if (channel.Type != ChannelType.Voice) { await ctx.RespondAsync("Not a valid voice channel."); return; } var conn = node.GetGuildConnection(channel.Guild); if (conn == null) { await ctx.RespondAsync("Lavalink is not connected."); return; } await conn.DisconnectAsync(); await ctx.RespondAsync($"Left {channel.Name}!"); } } } ``` ## Adding player commands Now that we can join a voice channel, we can make our bot play music! Let's now create the base for a play command: ```csharp [Command] public async Task Play(CommandContext ctx, [RemainingText] string search) { } ``` One of Lavalink's best features is its ability to search for tracks from a variety of media sources, such as YouTube, SoundCloud, Twitch, and more. This is what makes bots like Rythm, Fredboat, and Groovy popular. The search is used in a REST request to get the track data, which is then sent through the WebSocket connection to play the track in the voice channel. That is what we will be doing in this command. Lavalink can also play tracks directly from a media url, in which case the play command can look like this: ```csharp [Command] public async Task Play(CommandContext ctx, Uri url) { } ``` Like before, we will need to get our node and guild connection and have the appropriate checks. Since it wouldn't make sense to have the channel as a parameter, we will instead get it from the member's voice state: ```csharp //Important to check the voice state itself first, //as it may throw a NullReferenceException if they don't have a voice state. if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) { await ctx.RespondAsync("You are not in a voice channel."); return; } var lava = ctx.Client.GetLavalink(); var node = lava.ConnectedNodes.Values.First(); var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); if (conn == null) { await ctx.RespondAsync("Lavalink is not connected."); return; } ``` Next, we will get the track details by calling `node.Rest.GetTracksAsync()`. There are a variety of overloads for this: 1. `GetTracksAsync(LavalinkSearchType.Youtube, search)` will search YouTube for your search string. 2. `GetTracksAsync(LavalinkSearchType.SoundCloud, search)` will search SoundCloud for your search string. 3. `GetTracksAsync(Uri)` will use the direct url to obtain the track. This is mainly used for the other media sources. For this guide we will be searching YouTube. Let's pass in our search string and store the result in a variable: ```csharp //We don't need to specify the search type here //since it is YouTube by default. var loadResult = await node.Rest.GetTracksAsync(search); ``` The load result will contain an enum called `LoadResultType`, which will inform us if Lavalink was able to retrieve the track data. We can use this as a check: ```csharp //If something went wrong on Lavalink's end if (loadResult.LoadResultType == LavalinkLoadResultType.LoadFailed //or it just couldn't find anything. || loadResult.LoadResultType == LavalinkLoadResultType.NoMatches) { await ctx.RespondAsync($"Track search failed for {search}."); return; } ``` Lavalink will return the track data from your search in a collection called `loadResult.Tracks`, similar to using the search bar in YouTube or SoundCloud directly. The first track is typically the most accurate one, so that is what we will use: ```csharp var track = loadResult.Tracks.First(); ``` And finally, we can play the track: ```csharp await conn.PlayAsync(track); await ctx.RespondAsync($"Now playing {track.Title}!"); ``` Your play command should look like this: ```csharp [Command] public async Task Play(CommandContext ctx, [RemainingText] string search) { if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) { await ctx.RespondAsync("You are not in a voice channel."); return; } var lava = ctx.Client.GetLavalink(); var node = lava.ConnectedNodes.Values.First(); var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); if (conn == null) { await ctx.RespondAsync("Lavalink is not connected."); return; } var loadResult = await node.Rest.GetTracksAsync(search); if (loadResult.LoadResultType == LavalinkLoadResultType.LoadFailed || loadResult.LoadResultType == LavalinkLoadResultType.NoMatches) { await ctx.RespondAsync($"Track search failed for {search}."); return; } var track = loadResult.Tracks.First(); await conn.PlayAsync(track); await ctx.RespondAsync($"Now playing {track.Title}!"); } ``` Being able to pause the player is also useful. For this we can use most of the base from the play command: ```csharp [Command] public async Task Pause(CommandContext ctx) { if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) { await ctx.RespondAsync("You are not in a voice channel."); return; } var lava = ctx.Client.GetLavalink(); var node = lava.ConnectedNodes.Values.First(); var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); if (conn == null) { await ctx.RespondAsync("Lavalink is not connected."); return; } } ``` For this command we will also want to check the player state to determine if we should send a pause command. We can do so by checking `conn.CurrentState.CurrentTrack`: ```csharp if (conn.CurrentState.CurrentTrack == null) { await ctx.RespondAsync("There are no tracks loaded."); return; } ``` And finally, we can call pause: ```csharp await conn.PauseAsync(); ``` The finished command should look like so: ```csharp [Command] public async Task Pause(CommandContext ctx) { if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) { await ctx.RespondAsync("You are not in a voice channel."); return; } var lava = ctx.Client.GetLavalink(); var node = lava.ConnectedNodes.Values.First(); var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); if (conn == null) { await ctx.RespondAsync("Lavalink is not connected."); return; } if (conn.CurrentState.CurrentTrack == null) { await ctx.RespondAsync("There are no tracks loaded."); return; } await conn.PauseAsync(); } ``` Of course, there are other commands Lavalink has to offer. Check out [the docs](https://docs.dcs.aitsys.dev/api/DisCatSharp.Lavalink.LavalinkGuildConnection.html#methods) to view the commands you can use while playing tracks. diff --git a/DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md b/DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md index 36b4630ad..24b72042c 100644 --- a/DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md +++ b/DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md @@ -1,41 +1,41 @@ --- -uid: modules_voicenext_prerequisites +uid: modules_audio_voicenext_prerequisites title: VoiceNext Prerequisites --- # VoiceNext Prerequisites > Note: We highly suggest using the [DisCatSharp.Lavalink](xref:modules_audio_lavalink_configuration) package for audio playback. It is much easier to use and has a lot of features that VoiceNext does not have. ## Required Libraries VoiceNext depends on the [libsodium](https://github.com/jedisct1/libsodium) and [Opus](https://opus-codec.org/) libraries to decrypt and process audio packets.
Both *must* be available on your development and host machines otherwise VoiceNext will *not* work. ### Windows When installing VoiceNext though NuGet, an additional package containing the native Windows binaries will automatically be included with **no additional steps required**. However, if you are using DisCatSharp from source or without a NuGet package manager, you must manually [download](xref:natives) the binaries and place them at the root of your working directory where your application is located. ### MacOS Native libraries for Apple's macOS can be installed using the [Homebrew](https://brew.sh) package manager: ```console $ brew install opus libsodium ``` ### Linux #### Debian and Derivatives Opus package naming is consistent across Debian, Ubuntu, and Linux Mint. ```bash sudo apt-get install libopus0 libopus-dev ``` Package naming for *libsodium* will vary depending on your distro and version: Distributions|Terminal Command :---:|:---: Ubuntu 18.04+, Debian 10+|`sudo apt-get install libsodium23 libsodium-dev` Linux Mint, Ubuntu 16.04, Debian 9 |`sudo apt-get install libsodium18 libsodium-dev` Debian 8|`sudo apt-get install libsodium13 libsodium-dev` diff --git a/DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md b/DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md index f4861fdef..b07b5e118 100644 --- a/DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md +++ b/DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md @@ -1,101 +1,101 @@ --- -uid: modules_voicenext_receive +uid: modules_audio_voicenext_receive title: Receiving --- ## Receiving with VoiceNext ### Enable Receiver Receiving incoming audio is disabled by default to save on bandwidth, as most users will never make use of incoming data. This can be changed by providing a configuration object to `DiscordClient#UseVoiceNext()`. ```cs var discord = new DiscordClient(); discord.UseVoiceNext(new VoiceNextConfiguration() { EnableIncoming = true }); ``` ### Establish Connection The voice channel join process is the exact same as when transmitting. ```cs DiscordChannel channel; VoiceNextConnection connection = await channel.ConnectAsync(); ``` ### Write Event Handler We'll be able to receive incoming audio from the `VoiceReceived` event fired by `VoiceNextConnection`. ```cs connection.VoiceReceived += ReceiveHandler; ``` Writing the logic for this event handler will depend on your overall goal. The event arguments will contain a PCM audio packet for you to make use of. You can convert each packet to another format, concatenate them all together, feed them into an external program, or process the packets any way that'll suit your needs. When a user is speaking, `VoiceReceived` should fire once every twenty milliseconds and its packet will contain around twenty milliseconds worth of audio; this can vary due to differences in client settings. To help keep track of the torrent of packets for each user, you can use user IDs in combination the synchronization value (SSRC) sent by Discord to determine the source of each packet. This short-and-simple example will use [ffmpeg](https://ffmpeg.org/about.html) to convert each packet to a *wav* file. ```cs private async Task ReceiveHandler(VoiceNextConnection _, VoiceReceiveEventArgs args) { var name = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var ffmpeg = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $@"-ac 2 -f s16le -ar 48000 -i pipe:0 -ac 2 -ar 44100 {name}.wav", RedirectStandardInput = true }); await ffmpeg.StandardInput.BaseStream.WriteAsync(args.PcmData); } ```
That's really all there is to it. Connect to a voice channel, hook an event, process the data as you see fit. ![Wav Files](/images/voicenext_receive_01.png) ## Example Commands ```cs [Command("start")] public async Task StartCommand(CommandContext ctx, DiscordChannel channel = null) { channel ??= ctx.Member.VoiceState?.Channel; var connection = await channel.ConnectAsync(); Directory.CreateDirectory("Output"); connection.VoiceReceived += VoiceReceiveHandler; } [Command("stop")] public Task StopCommand(CommandContext ctx) { var vnext = ctx.Client.GetVoiceNext(); var connection = vnext.GetConnection(ctx.Guild); connection.VoiceReceived -= VoiceReceiveHandler; connection.Dispose(); return Task.CompletedTask; } private async Task VoiceReceiveHandler(VoiceNextConnection connection, VoiceReceiveEventArgs args) { var fileName = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var ffmpeg = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $@"-ac 2 -f s16le -ar 48000 -i pipe:0 -ac 2 -ar 44100 Output/{fileName}.wav", RedirectStandardInput = true }); await ffmpeg.StandardInput.BaseStream.WriteAsync(args.PcmData); ffmpeg.Dispose(); } ``` diff --git a/DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md b/DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md index aa8fb0ef2..4216f5c73 100644 --- a/DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md +++ b/DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md @@ -1,118 +1,118 @@ --- -uid: modules_voicenext_transmit +uid: modules_audio_voicenext_transmit title: Transmitting --- ## Transmitting with VoiceNext ### Enable VoiceNext Install the `DisCatSharp.VoiceNext` package from NuGet. ![NuGet Package Manager](/images/voicenext_transmit_01.png) Then use the `UseVoiceNext` extension method on your instance of `DiscordClient`. ```cs var discord = new DiscordClient(); discord.UseVoiceNext(); ``` ### Connect Joining a voice channel is *very* easy; simply use the `ConnectAsync` extension method on `DiscordChannel`. ```cs DiscordChannel channel; VoiceNextConnection connection = await channel.ConnectAsync(); ``` ### Transmit Discord requires that we send Opus encoded stereo PCM audio data at a sample rate of 48,000 Hz. You'll need to convert your audio source to PCM S16LE using your preferred program for media conversion, then read that data into a `Stream` object or an array of `byte` to be used with VoiceNext. Opus encoding of the PCM data will be done automatically by VoiceNext before sending it to Discord. This example will use [ffmpeg](https://ffmpeg.org/about.html) to convert an MP3 file to a PCM stream. ```cs var filePath = "funiculi_funicula.mp3"; var ffmpeg = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1", RedirectStandardOutput = true, UseShellExecute = false }); Stream pcm = ffmpeg.StandardOutput.BaseStream; ``` Now that our audio is the correct format, we'll need to get a *transmit sink* for the channel we're connected to. You can think of the transmit stream as our direct interface with a voice channel; any data written to one will be processed by VoiceNext, queued, and sent to Discord which will then be output to the connected voice channel. ```cs VoiceTransmitSink transmit = connection.GetTransmitSink(); ``` Once we have a transmit sink, we can 'play' our audio by copying our PCM data to the transmit sink buffer. ```cs await pcm.CopyToAsync(transmit); ``` `Stream#CopyToAsync()` will copy PCM data from the input stream to the output sink, up to the sink's configured capacity, at which point it will wait until it can copy more. This means that the call will hold the task's execution, until such time that the entire input stream has been consumed, and enqueued in the sink. This operation cannot be cancelled. If you'd like to have finer control of the playback, you should instead consider using `Stream#ReadAsync()` and `VoiceTransmitSink#WriteAsync()` to manually copy small portions of PCM data to the transmit sink. ### Disconnect Similar to joining, leaving a voice channel is rather straightforward. ```cs var vnext = discord.GetVoiceNext(); var connection = vnext.GetConnection(); connection.Disconnect(); ``` ## Example Commands ```cs [Command("join")] public async Task JoinCommand(CommandContext ctx, DiscordChannel channel = null) { channel ??= ctx.Member.VoiceState?.Channel; await channel.ConnectAsync(); } [Command("play")] public async Task PlayCommand(CommandContext ctx, string path) { var vnext = ctx.Client.GetVoiceNext(); var connection = vnext.GetConnection(ctx.Guild); var transmit = connection.GetTransmitSink(); var pcm = ConvertAudioToPcm(path); await pcm.CopyToAsync(transmit); await pcm.DisposeAsync(); } [Command("leave")] public async Task LeaveCommand(CommandContext ctx) { var vnext = ctx.Client.GetVoiceNext(); var connection = vnext.GetConnection(ctx.Guild); connection.Disconnect(); } private Stream ConvertAudioToPcm(string filePath) { var ffmpeg = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1", RedirectStandardOutput = true, UseShellExecute = false }); return ffmpeg.StandardOutput.BaseStream; } ```