VoiceOutputController
The VoiceOutputController is the recommended way to manage audio output in DisCatSharp. It implements IExternalOpusSource and plugs directly into BindExternalOpusSourceAsync.
Common use cases:
- Stream music from the Lavalink bridge with zero-decode Opus passthrough
- Queue TTS/system audio as PCM overlays that pause or duck music automatically
- Rebind music sources cleanly during playback
How It Works
flowchart LR
Music["Lavalink bridge: Opus"] -->|"SetMusicSourceAsync"| Controller["VoiceOutputController"]
Overlay["TTS/system audio: PCM"] -->|"QueuePcmOverlayAsync"| Controller
Controller -->|"Opus frames"| Connection["VoiceConnection"]
Connection --> Ready["DAVE media-readiness gate"]
Ready --> Packet["RTP + DAVE mode + transport encryption"]
Packet --> Discord
The controller operates in three states:
- Idle — no music source bound, no overlays queued; the output channel is quiet.
- Opus passthrough — music source frames are forwarded directly without decode/encode overhead.
- Overlay playback — PCM overlays are encoded and emitted one-at-a-time; music is paused (default) or ducked.
Quick Start
using DisCatSharp.Voice;
using DisCatSharp.Voice.Entities;
client.UseVoice(new VoiceConfiguration
{
// Required by BindExternalOpusSourceAsync.
EnableExternalOpus = true
});
// Create the controller (defaults to 48kHz stereo)
await using var controller = new VoiceOutputController();
// Bind to the voice connection
await connection.BindExternalOpusSourceAsync(controller);
// Set the Lavalink bridge as the music source (Opus passthrough)
var bridgeSource = lavalinkSession.GetBridgeOpusSource(guildId);
await controller.SetMusicSourceAsync(bridgeSource);
// Queue a TTS overlay (pauses music while playing)
await controller.QueuePcmOverlayAsync(ttsStream, "tts-greeting");
// Queue an overlay and let the controller dispose the stream when done
await controller.QueueOwnedPcmOverlayAsync(systemSoundStream, "join-chime");
Important
BindExternalOpusSourceAsync throws InvalidOperationException unless VoiceConfiguration.EnableExternalOpus was enabled before the voice connection was created.
Music Source
Bind or swap the music source at any time with SetMusicSourceAsync:
// Bind music
await controller.SetMusicSourceAsync(bridgeSource);
// Swap to a different source (old pump is cleanly stopped)
await controller.SetMusicSourceAsync(anotherSource);
// Stop music entirely
await controller.SetMusicSourceAsync(null);
The music pump reads IExternalOpusSource.ReadFramesAsync on a background task. Frames flow at the source's cadence — no PeriodicTimer master clock is needed.
The controller does not bypass connection security. Pre-encoded frames skip only the internal Opus encoder; VoiceConnection still applies media-readiness gating, RTP framing, the currently executing DAVE mode, and Discord transport encryption.
Overlay Queue
Overlays (TTS, system sounds, etc.) are queued and played serially:
// Queue PCM overlays (16-bit LE, matching the controller's AudioFormat)
await controller.QueuePcmOverlayAsync(ttsStream, "tts");
await controller.QueuePcmOverlayAsync(chimePcmStream, "chime");
// Auto-dispose the stream when playback finishes
await controller.QueueOwnedPcmOverlayAsync(ownedStream, "alert");
While an overlay is active:
- By default, music is paused (no decode/re-encode needed — lowest latency)
- Alternatively, set
PauseMusicForOverlays = falseand useSetDucking(true, 0.2f)for concurrent playback at reduced volume (requires decode/mix/re-encode)
Ducking vs. Pausing
// Default: music pauses for overlays (recommended)
controller.PauseMusicForOverlays = true;
// Alternative: duck music to 20% volume during overlays
controller.PauseMusicForOverlays = false;
controller.SetDucking(true, 0.20f);
// Restore full volume
controller.SetDucking(false);
// Or set gain directly
controller.MusicGain = 0.5f; // 50% volume (triggers slow decode/encode path)
Note
Music gain values below 1.0 trigger a decode → scale → re-encode path.
Keep MusicGain at 1.0 (the default) for zero-overhead passthrough.
Lifecycle
// 1. Create controller
await using var controller = new VoiceOutputController();
// 2. Bind to voice connection
await connection.BindExternalOpusSourceAsync(controller);
// 3. Use as needed
await controller.SetMusicSourceAsync(source);
await controller.QueuePcmOverlayAsync(stream);
// 4. Dispose (cancels all tasks, releases Opus encoder)
await controller.DisposeAsync();
Important
Only one consumer may call ReadFramesAsync at a time (enforced at runtime).
In practice this means one BindExternalOpusSourceAsync call per controller.
With Lavalink Bridge Mode
// Get the raw Opus source from the bridge
var bridgeSource = lavalinkSession.GetBridgeOpusSource(guildId);
// Create controller and set the bridge as music
await using var controller = new VoiceOutputController();
await controller.SetMusicSourceAsync(bridgeSource);
// Rebind the voice connection to use the controller
if (!lavalinkSession.RebindBridgeOpusSource(guildId, controller))
throw new InvalidOperationException("No bridge voice connection is active for this guild.");
// Now you can overlay TTS while music plays
await controller.QueuePcmOverlayAsync(ttsStream, "tts");
DAVE Transitions
The controller does not need to be rebound during a DAVE epoch change. Both direct Opus music frames and encoded PCM overlays use the same connection-level behavior:
- An established sender continues using its old epoch while the next transition is
ReadyForTransitionorDowngrading. - A matching OP22 switches the local sender to the prepared ratchet or protocol-0 passthrough.
DavePendingAudioBehaviorapplies only when no executing media mode is usable.
Use VoiceConnection.IsE2eeUsableForSend when deciding whether to start or continue output. Use WaitForDaveActiveAsync only when the application specifically requires an encrypting DAVE sender; protocol-0 passthrough is usable but never DAVE-active.
Configuration Reference
VoiceOutputController
| Property | Type | Default | Description |
|---|---|---|---|
MusicGain |
float |
1.0 |
Music volume multiplier (0.0–1.0). Values < 1.0 enable the slow decode/encode path. |
PauseMusicForOverlays |
bool |
true |
When true, music pauses during overlay playback (no mixing overhead). |
HasActiveOverlay |
bool |
— | Read-only. True when one or more overlays are currently playing. |
Constructor
| Parameter | Type | Default | Description |
|---|---|---|---|
format |
AudioFormat |
AudioFormat.Default |
Audio format for PCM encode/decode (48kHz, 2ch, Music preset) |
Note
The previous VoiceOutputMixer and its channel-based API have been removed.
All new integrations should use VoiceOutputController.