Transmitting with Voice
1. Enable Voice
using DisCatSharp.Voice;
using DisCatSharp.Voice.Entities;
using DisCatSharp.Voice.Enums;
client.UseVoice(new VoiceConfiguration
{
EnableDebugLogging = false,
MaxDaveProtocolVersion = 1,
DavePendingAudioBehavior = DavePendingAudioBehavior.PassThrough
});
2. Connect to a Voice Channel
using DisCatSharp.Voice;
using DisCatSharp.Voice.Entities;
VoiceConnection connection = await channel.ConnectAsync();
VoiceTransmitSink transmit = connection.GetTransmitSink();
3. Send PCM Audio
Discord voice expects PCM S16LE, 48kHz, stereo before Opus encoding.
Example using ffmpeg to convert an input file and stream PCM to the transmit sink:
using System.Diagnostics;
using DisCatSharp.Voice;
public static async Task PlayFileAsync(VoiceConnection connection, string filePath, CancellationToken ct = default)
{
var ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-hide_banner -loglevel warning -i \"{filePath}\" -ac 2 -ar 48000 -f s16le pipe:1",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
}) ?? throw new InvalidOperationException("Failed to start ffmpeg.");
await using var pcm = ffmpeg.StandardOutput.BaseStream;
var transmit = connection.GetTransmitSink();
await pcm.CopyToAsync(transmit, ct);
await transmit.FlushAsync(ct);
await connection.WaitForPlaybackFinishAsync();
}
4. DAVE Media Readiness
DaveState is a control-plane state. Outbound audio is gated by whether a current sender mode remains usable:
- During initial positive-version negotiation, media is not ready until the local sender ratchet is installed.
- During
ReadyForTransitionorDowngrading, an established sender continues using the old epoch until matching OP22 executes the prepared transition. - After a transition to protocol
0, media remains usable through plaintext passthrough even thoughIsDaveActiveisfalseandDaveStateisInactive.
Only when no current media mode is usable does VoiceConfiguration.DavePendingAudioBehavior apply:
PassThrough(default): send without the DAVE layerDrop: drop the outbound frameThrow: throwInvalidOperationException
Check IsE2eeUsableForSend before starting policy-sensitive playback:
if (!connection.IsE2eeUsableForSend)
{
_ = await connection.WaitForDaveActiveAsync(TimeSpan.FromSeconds(5));
if (!connection.IsE2eeUsableForSend)
throw new TimeoutException("Voice media did not become ready.");
}
If the application requires DAVE encryption rather than merely usable voice media, require the explicit activation result:
bool encrypted = await connection.WaitForDaveActiveAsync(TimeSpan.FromSeconds(5));
if (!encrypted)
throw new InvalidOperationException("DAVE encryption is required for this playback.");
Protocol-0 passthrough is media-ready but never makes WaitForDaveActiveAsync return true.
5. Send Filters
using DisCatSharp.Voice;
using DisCatSharp.Voice.Entities;
using DisCatSharp.Voice.Interfaces;
public sealed class SoftClipFilter : IVoiceFilter
{
public void Transform(Span<short> pcmData, AudioFormat pcmFormat, int duration)
{
for (var i = 0; i < pcmData.Length; i++)
{
var sample = pcmData[i];
// Light soft-clip to reduce peaks before encode.
if (sample > 28000) sample = (short)(28000 + (sample - 28000) / 4);
if (sample < -28000) sample = (short)(-28000 + (sample + 28000) / 4);
pcmData[i] = sample;
}
}
}
var transmit = connection.GetTransmitSink();
var filter = new SoftClipFilter();
transmit.InstallFilter(filter); // appended at end of filter chain
transmit.VolumeModifier = 0.9; // optional global gain
// ... write PCM as usual ...
transmit.UninstallFilter(filter);
Use InstallFilter(filter, order) if you need deterministic ordering.
6. Playback Controls
transmit.Pause();
await transmit.ResumeAsync();
await transmit.FlushAsync();
await connection.WaitForPlaybackFinishAsync();
7. Disconnect
connection.Disconnect();
CommandsNext Example
[Command("join")]
public static async Task JoinAsync(CommandContext ctx)
{
var channel = ctx.Member?.VoiceState?.Channel
?? throw new InvalidOperationException("Join a voice channel first.");
await channel.ConnectAsync();
}
[Command("play")]
public static async Task PlayAsync(CommandContext ctx, string path)
{
var voice = ctx.Client.GetVoice()
?? throw new InvalidOperationException("Voice is not enabled.");
var connection = voice.GetConnection(ctx.Guild)
?? throw new InvalidOperationException("Bot is not connected.");
await PlayFileAsync(connection, path, CancellationToken.None);
}
[Command("leave")]
public static Task LeaveAsync(CommandContext ctx)
{
var connection = ctx.Client.GetVoice()?.GetConnection(ctx.Guild);
connection?.Disconnect();
return Task.CompletedTask;
}