Skip to content

Voice Chat

BlackICE has built-in proximity voice chat. Without any bundle code, players hear each other when they are near each other in the world. Voices come from the speaker's character, pan with it and fade with distance.

The server-side component can change how voice is routed and how it sounds:

  • Range - How far a player can be heard
  • Direct connections - Voice between two players regardless of distance (radios, phone calls, team channels)
  • Speakers and mics - Pick up voices and game audio at one place and play them from an entity somewhere else (PA systems, radios, intercoms)
  • Effects - An effect chain on a voice or a speaker: filters, distortion, compression, pitch, reverb and more
  • Effect presets - Named sets of effect parameters you define once and apply by name or handle

All of this is server API, under re.Voip. Clients apply whatever the server decides, players can't change another player's voice. The examples on this page assume your source file has using re;.

How voice reaches a listener

A listener hears a speaker when one of these is true:

  • The speaker's character is in the listener's network range (the area the listener's game streams other players in), and closer than the speaker's voice range if one is set. This is spatial voice, it comes from the speaker's character and is affected by distance, direction, occlusion and doppler
  • A script created a direct connection between them. This is direct voice, it plays without position, like a phone call
  • The speaker is near a mic that a script connected to a speaker entity the listener can hear. The voice then plays from that entity, see Speakers and mics

Spatial voice always applies doppler to moving speakers, at 75 % of the physical shift. Nothing needs to be configured for it.

Voice range

Voip.SetVoiceRange(player, 30f); // metres, 0 restores the default
  • Listeners further away than the range stop receiving the speaker. The voice fades out towards the range and is silent at it
  • The range only affects spatial voice, direct connections ignore it
  • The range is per speaker. Shouting and whispering are a range change:
void SetVoiceMode(ClientId player, string mode)
{
    switch (mode)
    {
        case "whisper": Voip.SetVoiceRange(player, 5f); break;
        case "normal": Voip.SetVoiceRange(player, 20f); break;
        case "shout": Voip.SetVoiceRange(player, 60f); break;
    }
}

Info

A short range also saves bandwidth. The server stops sending voice to listeners outside it

Direct connections

// Two way radio between two players
Voip.CreateDirectConnection(alice, bob, false);
Voip.CreateDirectConnection(bob, alice, false);

// A whole channel at once
Voip.CreateDirectConnection(dispatcher, members, false);

Voip.RemoveDirectConnection(alice, bob);
Voip.RemoveAllDirectConnections(alice);
  • A connection is "one way". CreateDirectConnection(speaker, listener, ...) lets listener hear speaker. Create both directions for a conversation
  • With exclusive set to true, a listener that would also hear the speaker spatially hears them only through the connection. Use it so a radio partner standing next to you doesn't sound doubled

Speakers and mics

A mic picks up voices and game audio around a point in the world. A speaker plays what its connected mics pick up, from an entity such as a radio prop or a car.

Note

Currently all radio emitters are filtered, even the ones in world space. This might change in the future

// A mic fixed in the world, picking up within 15 m
var mic = Voip.CreateMic(position, 15f);

// Or a mic that moves with an entity
var mic = Voip.CreateEntityMic(entity, 15f);

// The speaker plays from an entity
var speaker = Voip.CreateSpeaker(speakerEntity);

Voip.ConnectMic(mic, speaker);
Voip.DisconnectMic(mic, speaker);

Voip.DestroyMic(mic);
Voip.DestroySpeaker(speaker);
  • A mic only plays through the speakers you connect it to. Connections are many-to-many. One mic can feed several speakers and one speaker can play several mics. To have one player heard on several speakers, use one mic and connect it to each speaker
  • A mic picks up the voices of players within its range and the game audio around it. Front-end sounds such as menus and radio music are left out
  • SetMicPosition moves a fixed mic, SetMicRange changes how far it picks up. The range has to be above 0, a mic created with an invalid range has the id 0 and does nothing
  • SetMicAttenuation scales how far game sounds carry to the mic. Above 1 they reach further, below 1 they fall off sooner. It doesn't change which players' voices are picked up, that is the mic's range
  • Listeners hear a speaker like any other sound on its entity. Positioned, fading with distance, and at their own broadcast volume setting
  • Mics and speakers together are limited to 511 per server

Mic hosts

Every mic is captured by a host, which defaults to the closest player within the mic's range. The host's game records what it hears at the mic and sends it on. A mic picks its host as follows:

  • Each client hosts at most one mic at a time. A client already hosting another mic is skipped
  • The host keeps the mic until they move further than 1.1x the mic's range from it, so a mic doesn't flip between two players standing close together
  • A mic only gets a host while someone other than that host can hear one of its connected speakers. A mic without listeners stays idle

A mic without a host is silent. You can create several mics on or around the same player, but with nobody else nearby only one of them captures. The others start once another player comes within their range to host them.

Note

The host's own volume settings don't affect what their mic captures

Effects

Effects are set on a target. A target is one of:

Target What it changes
FxTarget.Voice(player) The player's spatial voice
FxTarget.Direct(player) The player's voice over direct connections
FxTarget.Emitter(entity) Everything a speaker entity plays
var voice = FxTarget.Voice(player);

Voip.SetFxParameter(voice, "reverb_mix", 40f);
Voip.SetFxParameter(voice, "reverb_decay", 3f);

// Only for one listener, on top of the target's own parameters
Voip.SetFxParameterForListener(FxTarget.Direct(speaker), listener, "lowpass", 3000f);

// Remove one parameter, or all of them
Voip.SetFxParameter(voice, "reverb_mix", float.NaN);
Voip.ResetFx(voice);
  • Spatial and direct voice are separate targets, so a player can sound plain to the people next to them and filtered on the radio at the same time
  • Per-listener parameters override the target's parameters for that one listener
  • Values are clamped to the ranges in the table below. Unknown names are ignored and logged on the server
  • ResetFx also clears the target's presets. It keeps the voice range
  • Everything a player had is cleared when they disconnect, and an emitter's effects are dropped when its entity is deleted

The first parameter set on a voice switches it to the effect chain. The switch takes effect from the next time they start transmitting. Value changes on a voice that is already playing apply right away. Speakers always play through the effect chain, with every parameter at its neutral default until you set one.

Parameter reference

Name Range Default What it does
volume -96 to 12 dB 0 Output level
highpass 20 to 5000 Hz 20 Cuts everything below, 24 dB/oct
lowpass 100 to 20000 Hz 20000 Cuts everything above, 24 dB/oct
eq_freq 20 to 20000 Hz 1000 Centre of the peak EQ band
eq_gain -24 to 24 dB 0 Boost or cut of the peak EQ band
eq_q 0.5 to 20 1 Width of the peak EQ band, higher is narrower
drive 0 to 100 0 Distortion amount
distortion_type 0 to 4 1 0 none, 1 overdrive, 2 heavy, 3 fuzz, 4 clip
distortion_tone 0 to 100 50 Distortion brightness
distortion_mix 0 to 100 % 0 How much distorted signal is mixed in
compressor_threshold -60 to 0 dB 0 Level above which the compressor works
compressor_ratio 1 to 50 1 Compression strength, 1 is off
compressor_attack 0 to 2 s 0.01 How fast the compressor reacts
compressor_release 0 to 2 s 0.1 How fast the compressor lets go
compressor_gain -24 to 24 dB 0 Makeup gain after the compressor
tremolo_depth 0 to 100 % 0 Amplitude modulation depth
tremolo_rate 0.02 to 2000 Hz 5 Modulation rate. Above ~30 Hz it turns metallic and robotic
flanger_mix 0 to 100 % 0 Flanger amount
flanger_rate 0.002 to 20 Hz 0.5 Flanger sweep speed
flanger_depth 0 to 100 % 50 Flanger sweep depth
flanger_feedback -1 to 1 0 Flanger resonance
pitch -2400 to 2400 cents 0 Pitch shift, 1200 cents is one octave
delay_mix 0 to 100 % 0 Echo amount, 0.3 s echo
delay_feedback 0 to 95 % 0 How long the echo repeats
reverb_mix 0 to 100 % 0 Room reverb amount
reverb_decay 0.2 to 10 s 1.5 Reverb length
reverb_predelay 0 to 500 ms 20 Gap before the reverb starts, bigger feels like a bigger room
reverb_damping 0.5 to 10 2.25 Damps the reverb's highs, higher sounds softer

Effect presets

A preset is a named set of parameters. Define it once, then apply it to as many targets as you like. Editing a preset updates everything that uses it.

var radio = Voip.CreateFxPreset("radio");
Voip.SetFxPresetParameter(radio, "highpass", 500f);
Voip.SetFxPresetParameter(radio, "lowpass", 2800f);
Voip.SetFxPresetParameter(radio, "distortion_mix", 30f);

Voip.SetFxPreset(FxTarget.Direct(player), radio);
Voip.SetFxPreset(FxTarget.Direct(player), "radio"); // the same, by name

Voip.ClearFxPreset(FxTarget.Direct(player));
  • CreateFxPreset returns the existing preset when the name is already taken, so a restarted bundle gets its presets back instead of new ones
  • Presets don't contain a voice range, set it with SetVoiceRange
  • A target's own parameters are layered on top of its preset: SetFxPreset(voice, "radio") and then SetFxParameter(voice, "drive", 60f) is "radio, but dirtier"
  • SetFxPresetForListener applies a preset for one listener only. An empty or unknown name clears it, the listener then hears the target's own preset again
  • Presets exist until the server stops. A server holds up to 1024

Radio channel example

Everyone on the channel hears each other through a radio, the people standing next to a speaker still hear their plain voice:

var radio = Voip.CreateFxPreset("radio");
// ... parameters, see below

void JoinChannel(ClientId player, List<ClientId> channel)
{
    foreach (var member in channel)
    {
        Voip.CreateDirectConnection(player, member, true);
        Voip.CreateDirectConnection(member, player, true);
    }

    Voip.SetFxPreset(FxTarget.Direct(player), radio);
    channel.Add(player);
}

PA system example

A mic on a stage, speakers around a plaza, all of them sounding like a megaphone:

var megaphone = Voip.CreateFxPreset("megaphone");
// ... parameters, see below

var mic = Voip.CreateMic(stagePosition, 5f);
foreach (var entity in speakerProps)
{
    var speaker = Voip.CreateSpeaker(entity);
    Voip.ConnectMic(mic, speaker);
    Voip.SetFxPreset(FxTarget.Emitter(entity), megaphone);
}

Preset templates

Starting points for common voices. Tune them by ear until you are happy with the result:

var p = Voip.CreateFxPreset("radio");
Voip.SetFxPresetParameter(p, "highpass", 500f);
Voip.SetFxPresetParameter(p, "lowpass", 2800f);
Voip.SetFxPresetParameter(p, "eq_freq", 1500f);
Voip.SetFxPresetParameter(p, "eq_gain", 5f);
Voip.SetFxPresetParameter(p, "eq_q", 1.2f);
Voip.SetFxPresetParameter(p, "distortion_type", 1f);
Voip.SetFxPresetParameter(p, "drive", 30f);
Voip.SetFxPresetParameter(p, "distortion_tone", 60f);
Voip.SetFxPresetParameter(p, "distortion_mix", 30f);
Voip.SetFxPresetParameter(p, "compressor_threshold", -24f);
Voip.SetFxPresetParameter(p, "compressor_ratio", 8f);
Voip.SetFxPresetParameter(p, "compressor_attack", 0.003f);
Voip.SetFxPresetParameter(p, "compressor_release", 0.08f);
Voip.SetFxPresetParameter(p, "compressor_gain", 4f);
var p = Voip.CreateFxPreset("walkie_talkie");
Voip.SetFxPresetParameter(p, "highpass", 800f);
Voip.SetFxPresetParameter(p, "lowpass", 2400f);
Voip.SetFxPresetParameter(p, "eq_freq", 1800f);
Voip.SetFxPresetParameter(p, "eq_gain", 6f);
Voip.SetFxPresetParameter(p, "eq_q", 1.5f);
Voip.SetFxPresetParameter(p, "distortion_type", 3f);
Voip.SetFxPresetParameter(p, "drive", 45f);
Voip.SetFxPresetParameter(p, "distortion_mix", 45f);
Voip.SetFxPresetParameter(p, "compressor_threshold", -28f);
Voip.SetFxPresetParameter(p, "compressor_ratio", 8f);
Voip.SetFxPresetParameter(p, "compressor_attack", 0.005f);
Voip.SetFxPresetParameter(p, "compressor_release", 0.12f);
Voip.SetFxPresetParameter(p, "compressor_gain", 5f);
var p = Voip.CreateFxPreset("phone");
Voip.SetFxPresetParameter(p, "highpass", 300f);
Voip.SetFxPresetParameter(p, "lowpass", 3400f);
Voip.SetFxPresetParameter(p, "eq_freq", 1200f);
Voip.SetFxPresetParameter(p, "eq_gain", 4f);
Voip.SetFxPresetParameter(p, "distortion_type", 1f);
Voip.SetFxPresetParameter(p, "drive", 15f);
Voip.SetFxPresetParameter(p, "distortion_mix", 10f);
Voip.SetFxPresetParameter(p, "compressor_threshold", -20f);
Voip.SetFxPresetParameter(p, "compressor_ratio", 4f);
Voip.SetFxPresetParameter(p, "compressor_attack", 0.005f);
Voip.SetFxPresetParameter(p, "compressor_release", 0.1f);
Voip.SetFxPresetParameter(p, "compressor_gain", 3f);
var p = Voip.CreateFxPreset("megaphone");
Voip.SetFxPresetParameter(p, "highpass", 600f);
Voip.SetFxPresetParameter(p, "lowpass", 4500f);
Voip.SetFxPresetParameter(p, "eq_freq", 2500f);
Voip.SetFxPresetParameter(p, "eq_gain", 6f);
Voip.SetFxPresetParameter(p, "distortion_type", 2f);
Voip.SetFxPresetParameter(p, "drive", 50f);
Voip.SetFxPresetParameter(p, "distortion_mix", 50f);
Voip.SetFxPresetParameter(p, "compressor_threshold", -18f);
Voip.SetFxPresetParameter(p, "compressor_ratio", 8f);
Voip.SetFxPresetParameter(p, "compressor_attack", 0.003f);
Voip.SetFxPresetParameter(p, "compressor_release", 0.08f);
Voip.SetFxPresetParameter(p, "compressor_gain", 3f);
Voip.SetFxPresetParameter(p, "reverb_mix", 15f);
var p = Voip.CreateFxPreset("robot");
Voip.SetFxPresetParameter(p, "tremolo_depth", 100f);
Voip.SetFxPresetParameter(p, "tremolo_rate", 50f);
Voip.SetFxPresetParameter(p, "pitch", -300f);
Voip.SetFxPresetParameter(p, "flanger_mix", 25f);
Voip.SetFxPresetParameter(p, "flanger_rate", 0.2f);
Voip.SetFxPresetParameter(p, "flanger_feedback", 0.6f);
var p = Voip.CreateFxPreset("cave");
Voip.SetFxPresetParameter(p, "reverb_mix", 55f);
Voip.SetFxPresetParameter(p, "reverb_decay", 4f);
Voip.SetFxPresetParameter(p, "reverb_predelay", 45f);
Voip.SetFxPresetParameter(p, "reverb_damping", 4f);
Voip.SetFxPresetParameter(p, "delay_mix", 15f);
Voip.SetFxPresetParameter(p, "delay_feedback", 30f);

What players control

Players set their own volumes in the game's audio settings, and bundles can't change them:

Setting Affects
Output Volume Spatial Other players' spatial voice
Output Volume Direct Voice over direct connections
Output Volume Broadcast Everything played from speakers
Output Volume Media Spatial Web content and video on in-game screens
Output Volume Media Direct Web content and video in the player's own interface

Network cost

Voice audio itself is what costs bandwidth. Each player's voice is sent to every listener who can hear them, at the server's voice bitrate, while they transmit. Nothing is sent while nobody talks. A mic host also uploads one stream for the mic it captures.

The API itself is cheap, only changes are networked. This means that you should aim to reduce voice ranges and target lists to the smallest amount of targets needed at any given time.

The server is optimized for throughput, but very large crodws of people are yet to be tested.