DisCatSharp.Voice Overview
DisCatSharp.Voice is the built-in Discord voice client for DisCatSharp.
It supports:
- Voice gateway signaling (join, move, reconnect)
- RTP audio send and receive
- Opus encode/decode
- Discord transport encryption
- DAVE end-to-end encryption (when the channel enables it)
Install
Install DisCatSharp.Voice from NuGet.
<PackageReference Include="DisCatSharp.Voice" Version="10.7.1" />
DisCatSharp.Voice.Natives is pulled automatically as a dependency for normal NuGet installs.
Quick Start
using DisCatSharp;
using DisCatSharp.Voice;
using DisCatSharp.Voice.Entities;
var client = new DiscordClient(new DiscordConfiguration
{
Token = "...",
TokenType = TokenType.Bot,
Intents = DiscordIntents.AllUnprivileged | DiscordIntents.GuildVoiceStates
});
client.UseVoice(new VoiceConfiguration
{
EnableIncoming = true,
EnableDebugLogging = false
});
// Later, once you have a DiscordChannel (voice/stage):
VoiceConnection connection = await channel.ConnectAsync();
VoiceTransmitSink sink = connection.GetTransmitSink();
// Optional: enable extra diagnostics only for this specific connection.
connection.EnableDebugLogging = true;
Complete Example
This example configures voice, records receive diagnostics, waits only when outbound media is genuinely unavailable, streams a file through ffmpeg, and cleans up the connection.
using System.Diagnostics;
using DisCatSharp;
using DisCatSharp.Enums;
using DisCatSharp.Voice;
using DisCatSharp.Voice.Enums;
const string token = "YOUR_BOT_TOKEN";
const ulong voiceChannelId = 123456789012345678;
const string audioPath = "audio/example.mp3";
using var shutdown = new CancellationTokenSource();
await using var client = new DiscordClient(new DiscordConfiguration
{
Token = token,
TokenType = TokenType.Bot,
Intents = DiscordIntents.Guilds | DiscordIntents.GuildVoiceStates
});
client.UseVoice(new VoiceConfiguration
{
EnableIncoming = true,
MaxDaveProtocolVersion = 1,
DavePendingAudioBehavior = DavePendingAudioBehavior.Throw
});
await client.ConnectAsync(cancellationToken: shutdown.Token);
var channel = await client.GetChannelAsync(voiceChannelId, cancellationToken: shutdown.Token);
var connection = await channel.ConnectAsync();
connection.DaveStateChanged += (_, e) =>
{
Console.WriteLine(
$"[DAVE] {e.OldState} -> {e.NewState}; " +
$"protocol={e.ProtocolVersion}; senderEncrypted={e.IsActive}; reason={e.Reason}");
return Task.CompletedTask;
};
connection.VoicePacketDropped += (_, e) =>
{
Console.WriteLine($"[DROP] reason={e.Reason}; user={e.User?.Id}; detail={e.Detail}");
return Task.CompletedTask;
};
connection.VoiceReceived += (_, e) =>
{
Console.WriteLine($"[RX] user={e.User?.Id}; pcmBytes={e.PcmData.Length}");
return Task.CompletedTask;
};
try
{
// Protocol 0 is usable passthrough. A positive DAVE negotiation with no
// executing sender ratchet is not usable under the Throw policy above.
if (!connection.IsE2eeUsableForSend)
{
_ = await connection.WaitForDaveActiveAsync(
TimeSpan.FromSeconds(10),
shutdown.Token);
if (!connection.IsE2eeUsableForSend)
throw new TimeoutException("Voice media did not become ready before playback.");
}
await PlayFileAsync(connection, audioPath, shutdown.Token);
}
finally
{
connection.Disconnect();
await client.DisconnectAsync();
}
static async Task PlayFileAsync(
VoiceConnection connection,
string path,
CancellationToken cancellationToken)
{
var startInfo = new ProcessStartInfo
{
FileName = "ffmpeg",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
startInfo.ArgumentList.Add("-hide_banner");
startInfo.ArgumentList.Add("-loglevel");
startInfo.ArgumentList.Add("warning");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(path);
startInfo.ArgumentList.Add("-ac");
startInfo.ArgumentList.Add("2");
startInfo.ArgumentList.Add("-ar");
startInfo.ArgumentList.Add("48000");
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add("s16le");
startInfo.ArgumentList.Add("pipe:1");
using var ffmpeg = Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start ffmpeg.");
await using var pcm = ffmpeg.StandardOutput.BaseStream;
var sink = connection.GetTransmitSink();
var buffer = new byte[81920];
int bytesRead;
while ((bytesRead = await pcm.ReadAsync(buffer, cancellationToken)) > 0)
await sink.WriteAsync(buffer, 0, bytesRead, cancellationToken);
await sink.FlushAsync(cancellationToken);
await connection.WaitForPlaybackFinishAsync();
await ffmpeg.WaitForExitAsync(cancellationToken);
if (ffmpeg.ExitCode != 0)
throw new InvalidOperationException($"ffmpeg exited with code {ffmpeg.ExitCode}.");
}
IsE2eeUsableForSend answers whether audio can flow under the configured policy. If your application requires DAVE encryption specifically, require WaitForDaveActiveAsync(...) to return true instead of accepting protocol-0 passthrough.
DAVE and Runtime Behavior
When Discord enables DAVE for a channel, DisCatSharp negotiates it automatically.
Important runtime flags on VoiceConnection:
IsDaveNegotiated: a DAVE coordinator exists, including protocol-0 passthroughIsDaveActive: the currently executing sender transform is encryptingIsE2eeUsableForSend: outbound media can flow under the selected pending-audio policyIsE2eeUsableForReceive: inbound media can be processed in the current receive modeDaveProtocolVersion: currently executing sender protocol versionDaveState: control-plane negotiation state
You can also wait for DAVE activation explicitly:
bool active = await connection.WaitForDaveActiveAsync(TimeSpan.FromSeconds(5));
WaitForDaveActiveAsync waits for DAVE encryption. It can return false while protocol-0 passthrough is nevertheless media-ready.
Debug Logging Scope
VoiceConfiguration.EnableDebugLogging sets the default debug/trace behavior for voice connections.
You can override this at runtime per connection using VoiceConnection.EnableDebugLogging.
connection.EnableDebugLogging = true; // enable only this connection
connection.EnableDebugLogging = false; // disable again