Receiving with Voice
1. Enable Incoming Audio
Incoming receive is opt-in.
using DisCatSharp.Voice;
client.UseVoice(new VoiceConfiguration
{
EnableIncoming = true,
EnableDebugLogging = false
});
2. Connect and Subscribe
using DisCatSharp.Voice;
VoiceConnection connection = await channel.ConnectAsync();
connection.VoiceReceived += OnVoiceReceived;
connection.VoicePacketDropped += OnVoicePacketDropped;
3. Handle Decoded Frames
VoiceReceived gives you decoded PCM plus metadata.
using DisCatSharp.Voice;
using DisCatSharp.Voice.EventArgs;
private static Task OnVoiceReceived(VoiceConnection _, VoiceReceiveEventArgs e)
{
Console.WriteLine(
$"RX user={e.User?.Id} ssrc={e.Ssrc} seq={e.Sequence} " +
$"pcm={e.PcmData.Length} opus={e.OpusData.Length} " +
$"missing={e.MissingFrames} conceal={e.IsConcealmentFrame}");
// e.PcmData is raw PCM bytes for this frame.
return Task.CompletedTask;
}
Useful fields:
PcmData: decoded PCM for the frameOpusData: original Opus payload when availableAudioFormat: format of decoded PCMAudioDuration: frame duration in msMissingFrames: count of gap frames before this frameIsConcealmentFrame: true when frame is packet-loss concealment
4. Monitor Packet Drops
using DisCatSharp.Voice;
using DisCatSharp.Voice.EventArgs;
private static Task OnVoicePacketDropped(VoiceConnection _, VoicePacketDroppedEventArgs e)
{
Console.WriteLine($"DROP reason={e.Reason} user={e.User?.Id} ssrc={e.Ssrc} seq={e.Sequence} detail={e.Detail}");
return Task.CompletedTask;
}
Drop reasons include malformed RTP or extensions, out-of-order packets, DAVE media not ready, missing sender mappings or decryptors, queue overflow, and Opus decode failures.
5. Record to MP3 (ffmpeg)
If you want a listen-test artifact, you can stream decoded PCM directly into ffmpeg:
using System.Diagnostics;
using DisCatSharp.Voice;
using DisCatSharp.Voice.EventArgs;
private static Process? _ffmpeg;
private static Stream? _ffmpegIn;
public static void StartMp3Recording(string outputPath)
{
_ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-y -hide_banner -loglevel warning -f s16le -ar 48000 -ac 2 -i pipe:0 -codec:a libmp3lame -q:a 2 \"{outputPath}\"",
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true
}) ?? throw new InvalidOperationException("Failed to start ffmpeg.");
_ffmpegIn = _ffmpeg.StandardInput.BaseStream;
}
private static async Task OnVoiceReceivedForRecording(VoiceConnection _, VoiceReceiveEventArgs e)
{
if (_ffmpegIn is null || e.PcmData.Length == 0)
return;
// Preserve timeline continuity by filling packet-loss gaps with silence.
if (e.MissingFrames > 0)
{
var bytesPerMs = e.AudioFormat.SampleRate * e.AudioFormat.ChannelCount * sizeof(short) / 1000;
var silenceBytes = bytesPerMs * e.AudioDuration * e.MissingFrames;
if (silenceBytes > 0)
await _ffmpegIn.WriteAsync(new byte[silenceBytes]);
}
await _ffmpegIn.WriteAsync(e.PcmData);
}
public static async Task StopMp3RecordingAsync()
{
if (_ffmpeg is null || _ffmpegIn is null)
return;
await _ffmpegIn.FlushAsync();
_ffmpegIn.Dispose();
_ffmpegIn = null;
_ffmpeg.WaitForExit();
_ffmpeg.Dispose();
_ffmpeg = null;
}
Wire it like this:
connection.VoiceReceived += OnVoiceReceivedForRecording;
StartMp3Recording("logs/recordings/session.mp3");
The same pattern works for WAV by changing ffmpeg output arguments.
6. DAVE Decryption Notes
When DAVE is active for the channel, transport decryption and then per-user DAVE decryption happen before your handler runs.
- You always receive plain PCM in
VoiceReceiveEventArgs.PcmData. IsE2eeUsableForReceivereports whether the executing receive mode can currently process media.ReadyForTransitionandDowngradingdo not make an established receiver unusable. Remote-user decryptors are transitioned in place and retain previous encrypted-epoch ratchets for ten seconds.- A downgrade enables plaintext passthrough before OP23 while retaining old encrypted ratchets for ten seconds.
- An upgrade allows plaintext for a ten-second overlap while the new encrypted mode begins.
- A newly welcomed member can receive with the new-epoch ratchets prepared by OP30, but its local sender remains inactive until matching OP22.
DaveStateChangedandDaveOpcodeObservedhelp diagnose handshake timing.
Do not infer receive usability from DaveState == Active. For example, an established member can receive normally while the control plane is ReadyForTransition, and protocol-0 passthrough is usable while the control plane is Inactive.
7. Unsubscribe / Disconnect
connection.VoiceReceived -= OnVoiceReceived;
connection.VoicePacketDropped -= OnVoicePacketDropped;
connection.Disconnect();