---
title: "Add voice chat"
description: "Connect players to voice and give them control over their audio."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Select a voice-capable provider. For lobby voice, enable **Enable Voice** when [creating the lobby](/crossplay/lobbies), then join its voice channel after joining the lobby. Providers without lobby voice ignore that creation option.

The first available provider in **Voice → Providers** takes every channel. With `Betide.Discord` first, a lobby voice channel becomes the Discord call of the lobby's Discord mirror, so the lobby must be mirrored to Discord (see [lobbies](/crossplay/lobbies)); session and named channels are not available on Discord. Discord player volume maps 1.0 to Discord's default of 100, master volumes stop at 1.0, and transmit targeting per channel is not supported. Audio device lists arrive asynchronously from Discord, so the first device query after sign-in can be empty.

## Join lobby voice

**Blueprint**

<BlueprintViewer title="Join lobby voice" src="/blueprints/crossplay-join-voice.txt" />

**C++**

Game module dependencies: `UCIKCore`, `BetideCore`.

```cpp
#include "Engine/GameInstance.h"
#include "Types/UCIKCrossplayTypes.h"
#include "UCIKCore.h"
#include "Voice/UCIKVoiceSubsystem.h"

void JoinPartyVoice(UGameInstance &GameInstance, const FUCIK_Lobby &Lobby)
{
    auto *Service = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>();
    if (!Service)
        return;

    FUCIK_VoiceChannel Channel;
    Channel.Kind = EUCIK_VoiceChannelKind::Lobby;
    Channel.Lobby = Lobby.Primary;
    Service->JoinChannel(
        Channel,
        FBetideCallback<bool>::CreateWeakLambda(&GameInstance, [](const TBetideResult<bool> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            UE_LOG(LogUCIK, Log, TEXT("Voice joined: %d"), Result.GetValue());
        }));
}
```

## Microphone and devices

**Blueprint**

<Accordion>
  <AccordionItem title="Push-to-talk">
<BlueprintViewer title="Push-to-talk" src="/blueprints/crossplay-push-to-talk.txt" />
  </AccordionItem>
  <AccordionItem title="Mute your microphone">
<BlueprintViewer title="Mute your microphone" src="/blueprints/crossplay-local-voice-mute.txt" />
  </AccordionItem>
  <AccordionItem title="Microphone volume">
<BlueprintViewer title="Microphone volume" src="/blueprints/crossplay-voice-input-volume.txt" />
  </AccordionItem>
  <AccordionItem title="Playback volume">
<BlueprintViewer title="Playback volume" src="/blueprints/crossplay-voice-output-volume.txt" />
  </AccordionItem>
  <AccordionItem title="Choose a microphone">
<BlueprintViewer title="Choose a microphone" src="/blueprints/crossplay-voice-input-device.txt" />
  </AccordionItem>
  <AccordionItem title="Choose an output device">
<BlueprintViewer title="Choose an output device" src="/blueprints/crossplay-voice-output-device.txt" />
  </AccordionItem>
</Accordion>

**C++**

Uses the includes above.

```cpp
void ReportVoiceControl(const FBetideError &Error)
{
    if (Error.IsError())
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Error.GetLogString());
}

void SetPushToTalk(UGameInstance &GameInstance, bool bPressed)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        ReportVoiceControl(Voice->SetTransmitMode(
            bPressed ? EUCIK_VoiceTransmitMode::All : EUCIK_VoiceTransmitMode::None, {}));
}

void SetMicrophoneMuted(UGameInstance &GameInstance, bool bMuted)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        ReportVoiceControl(Voice->SetLocalMuted(bMuted));
}

void SetMicrophoneVolume(UGameInstance &GameInstance, float Volume)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        ReportVoiceControl(Voice->SetInputVolume(Volume));
}

void SetPlaybackVolume(UGameInstance &GameInstance, float Volume)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        ReportVoiceControl(Voice->SetOutputVolume(Volume));
}

void ListVoiceDevices(UGameInstance &GameInstance)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
    {
        for (const auto &Device : Voice->GetInputDevices())
            UE_LOG(LogUCIK, Log, TEXT("Microphone: %s (%s)"), *Device.DisplayName, *Device.Id);
        for (const auto &Device : Voice->GetOutputDevices())
            UE_LOG(LogUCIK, Log, TEXT("Output: %s (%s)"), *Device.DisplayName, *Device.Id);
    }
}

void SelectMicrophone(UGameInstance &GameInstance, const FUCIK_VoiceDevice &Device)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        ReportVoiceControl(Voice->SetInputDevice(Device.Id));
}

void SelectVoiceOutput(UGameInstance &GameInstance, const FUCIK_VoiceDevice &Device)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        ReportVoiceControl(Voice->SetOutputDevice(Device.Id));
}
```

For push-to-talk, send **Pressed = false** after joining and on input release; send **true** while held. Volume ranges from **0 to 2**, with **1** as normal. Pick devices from the returned list, not a hard-coded device ID.

## Mute a player and leave

**Blueprint**

<Accordion>
  <AccordionItem title="Mute a player">
<BlueprintViewer title="Mute a player" src="/blueprints/crossplay-voice-player-mute.txt" />
  </AccordionItem>
  <AccordionItem title="Adjust player volume">
<BlueprintViewer title="Adjust player volume" src="/blueprints/crossplay-voice-player-volume.txt" />
  </AccordionItem>
  <AccordionItem title="Show who is speaking">
<BlueprintViewer title="Show who is speaking" src="/blueprints/crossplay-player-talking.txt" />
  </AccordionItem>
  <AccordionItem title="Leave lobby voice">
<BlueprintViewer title="Leave lobby voice" src="/blueprints/crossplay-leave-voice.txt" />
  </AccordionItem>
</Accordion>

**C++**

Uses the includes above.

```cpp
FBetideCallback<bool> ReportVoiceResult(UGameInstance &GameInstance)
{
    return FBetideCallback<bool>::CreateWeakLambda(
        &GameInstance, [](const TBetideResult<bool> &Result) {
            if (!Result.bOk)
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
            else
                UE_LOG(LogUCIK, Log, TEXT("Voice updated"));
        });
}

void MuteVoicePlayer(UGameInstance &GameInstance, const FUCIK_PlayerHandle &Player, bool bMuted)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        Voice->SetPlayerMuted(Player, bMuted, ReportVoiceResult(GameInstance));
}

void AdjustVoicePlayer(UGameInstance &GameInstance, const FUCIK_PlayerHandle &Player, float Volume)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
        Voice->SetPlayerVolume(Player, Volume, ReportVoiceResult(GameInstance));
}

bool IsVoicePlayerTalking(UGameInstance &GameInstance, const FUCIK_PlayerHandle &Player)
{
    const auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>();
    return Voice && Voice->IsPlayerTalking(Player);
}

void LeavePartyVoice(UGameInstance &GameInstance, const FUCIK_Lobby &Lobby)
{
    if (auto *Voice = GameInstance.GetSubsystem<UUCIKVoiceSubsystem>())
    {
        FUCIK_VoiceChannel Channel;
        Channel.Kind = EUCIK_VoiceChannelKind::Lobby;
        Channel.Lobby = Lobby.Primary;
        Voice->LeaveChannel(Channel, ReportVoiceResult(GameInstance));
    }
}
```

For a mute that survives sessions, use `UUCIKSocialSubsystem::SetPlayerMuted` from `Social/UCIKSocialSubsystem.h`.

## Positional voice

Add **Crossplay Positional Voice** to the pawn class, give it an attenuation asset with spatialization on, and every remote player's voice plays from their own pawn. It follows whichever voice the crossplay lobby uses (EOS lobby voice or Discord), so there is nothing to change when a lobby switches voice service, and there are no room names to set.

The component matches its pawn's player state to a lobby member, so it works once the player has joined the crossplay lobby and travelled with it. Players whose pawn has no component keep ordinary voice. Mute, player volume and deafen from the crossplay voice nodes still apply. **Is Crossplay Voice Positional** reports whether the speaker currently plays through the component.

Positional speech is played by Unreal, not by the voice service, so the service's echo cancellation does not hear it. Players on speakers without a headset may send echo back; recommend headsets. The kits have their own components for games that use one service alone: **EIK Voice Chat Synth Component** for [EOS](/integrations/eos/multiplayer-voice) and **Discord Positional Voice** for [Discord](/integrations/discord/lobbies-voice).

<Accordion>
  <AccordionItem title="Travel, credentials, and testing">

The subsystem restores channels, mutes, and transmit state after non-seamless travel. **Reset Crossplay Voice After Travel** is available for an explicit reset; avoid adding a second join on every map load.

Named rooms can require an access token and endpoint from your server. Credentials depend on the voice provider; a lobby handle is not a general-purpose room token.

An [EOS token room](/integrations/eos/multiplayer-voice) joined with **EIK Token Voice Chat Join Channel** is controlled by these nodes while no crossplay lobby voice is active, and **Get Joined Crossplay Voice Channels** lists it. EIK owns the room: it is not rejoined after travel, and an active lobby voice keeps the controls.

Test with two signed-in accounts and separate audio devices. Check OS microphone permissions and the selected input device before debugging network routing. The Steam-only preset does not supply a voice provider; [EOS voice](/integrations/eos/multiplayer-voice) and [Discord voice](/integrations/discord/lobbies-voice) have their own setup.

  </AccordionItem>
</Accordion>
