Table of Contents

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

┌──────────────┐                                              ┌──────────────────┐
│ Lavalink      │──Opus──▶ SetMusicSourceAsync()               │                  │
│ Bridge        │         (direct passthrough — no decode)     │ VoiceOutput-     │   Opus   ┌──────────────────┐
└──────────────┘                                        ──────▶│ Controller       │────────▶│ VoiceConnection  │──▶ Discord
┌──────────────┐                                               │                  │         └──────────────────┘
│ TTS / System │──PCM──▶ QueuePcmOverlayAsync()                │ (serial overlay  │
│ Audio        │         (serialized, pauses music)            │  + music slot)   │
└──────────────┘                                              └──────────────────┘

The controller operates in three states:

  1. Idle — no music source bound, no overlays queued; the output channel is quiet.
  2. Opus passthrough — music source frames are forwarded directly without decode/encode overhead.
  3. 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;

// 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");

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.

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 = false and use SetDucking(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.

// 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
lavalinkSession.RebindBridgeOpusSource(guildId, controller);

// Now you can overlay TTS while music plays
await controller.QueuePcmOverlayAsync(ttsStream, "tts");

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.

See Also