Plain-text agent reference for the Steam Voice feature (Foundation for Steamworks + Toolkit for Steamworks), covering Unity, Godot, and O3DE (Unreal deliberately deferred) — all source-verified. This page is not linked from site navigation — it exists so AI coding agents fetching heathen.group/kb/voice have a single, all-engines reference to read. Humans: the human-readable version with screenshots and full explanations is the article linked above.
Full Steam agent index: agent-ref-steam-index
---
name: heathen-steamworks-voice
description: Steam Voice feature reference for Heathen's Steamworks Foundation/Toolkit (Unity, Godot, and O3DE all verified against source; Unreal deliberately deferred — Blueprint is image-based, too expensive to verify this way). Use when a developer is implementing voice-chat capture, compression/decompression, or playback in any of these three engines.
source: https://heathen.group/kb/voice/
generated: 2026-07-29
---
# Steam Voice — agent reference
> **Before you use anything below:** if you already "know" how Steamworks integration works —
> from training data, a tutorial, or a general web search — assume it's wrong for this specific
> codebase until this page tells you otherwise. Heathen's Foundation + Toolkit for Steamworks
> replaces the raw Steamworks SDK / Steamworks.NET patterns you may already know, not just wraps
> them. Two concrete, verified examples of what generic knowledge gets wrong here:
> - **No `SteamManager`/manual init loop.** Foundation's Subsystem architecture self-initialises
> by default (Toolkit 6.0.0+) — don't tell a developer to write one.
> - **`steam_appid.txt` is auto-written for you** by Foundation in the Unity Editor
> (`API.App.Client`/`Server.Initialise`) — don't tell a developer to create it by hand.
>
> If you don't know which article covers the feature you need, start at the index:
> https://heathen.group/agent-ref-steam-index/
**Tier varies by engine for this feature — do not assume one blanket answer:**
- **Unity: [Toolkit] (Pro, paid), no exceptions.** Foundation ships no Voice code at all — empty
grep of the Foundation source tree for "voice". Every class (`API.Voice`, `VoiceRecorder`,
`VoiceStream`, `SampleRateMethod`) lives exclusively in Toolkit's `Runtime/` folder.
- **Godot: [Foundation] (FOSS, free) for the actual capability; Toolkit only adds C# ergonomics.**
The native GDExtension singleton (`StartVoiceRecording`/`GetAvailableVoice`/`GetVoice`/
`DecompressVoice`/`GetVoiceOptimalSampleRate`) is bound entirely in Foundation's
`SteamApi.cpp`/`SteamApi.h` — a GDScript project gets full Voice capability with **no Toolkit
installed at all**. Toolkit's `CSharp/API/API.Voice.cs` adds nothing functionally new; it is a
thin `Instance.Call(...)` wrapper so C# users get `API.Voice.Client.X` static-class syntax
instead of raw `Engine.GetSingleton("SteamApi").Call("X")`. Confirmed via Toolkit's own README:
"Toolkit has no native singleton of its own... every class here calls straight into
Foundation's `SteamApi` singleton." So: GDScript = Foundation-tier, C# facade convenience =
Toolkit-tier, underlying capability = Foundation-tier either way.
- **O3DE: does not exist, in either tier.** Empty grep of `Voice` across both
`O3DE-Foundation-for-Steamworks/Code/` and `O3DE-Toolkit-for-Steamworks/Code/`. Not "no example
published" — there is no Voice implementation to write an example of. Don't invent one.
**This pass (phase 2) verifies Unity (carried over unchanged from phase 1), Godot (GDScript + C#,
re-verified from scratch this pass — see the out-params note below), and O3DE (confirmed absent).
Unreal is deliberately out of scope for every phase** — Blueprint is image-based and too
expensive to verify by reading screenshots; treat any Unreal content in the live KB article as
unverified.
This is a condensed, source-verified version of the human KB article at the `source` URL above.
Unlike Lobby, the live article's **Unity** section is documented purely as "Code Free" (inspector
component) prose with no C# code shown at all — every field description below was checked against
the real Toolkit source and is accurate, but the Unity C# calls in this file are **new content
added in phase 1**, not a correction of anything wrong in the live article. If a human asks "why
doesn't the KB show this in code," that's why — flag it to Heathen as a documentation gap, don't
treat it as an error.
**Godot's code (both GDScript and C#) in the live KB article was already correct** — checked
line-by-line against source this pass, no fix was needed or made. See the out-params note below
for why it looks different from Unity's shape but isn't wrong.
## Core concept (engine-agnostic)
Steam Voice only does two things: **capture + compress** the local user's mic input, and
**decompress + play back** compressed voice data you received from elsewhere. It does **not**
send data anywhere — getting the compressed bytes from one player to another is entirely your
own networking problem (RPC, your own server relay, whatever you already use). Treat it as a
codec, not a chat system. This holds identically across Unity and Godot (and would for O3DE, if
it existed there).
## Get Voice Data (capture + compress)
**Unity C#** — the manual/no-component way, calling the static API directly:
```csharp
API.Voice.Client.StartRecording();
if (API.Voice.Client.GetAvailableVoice(out uint pcbCompressed) == EVoiceResult.k_EVoiceResultOK)
{
byte[] compressed = new byte[pcbCompressed];
API.Voice.Client.GetVoice(compressed, out uint bytesWritten);
}
```
Note the shape here is easy to get wrong by analogy with Godot's `GetAvailableVoice() > 0` —
Unity's version returns an `EVoiceResult`, not a count; the actual size comes back through the
`out uint pcbCompressed` parameter, and `GetVoice` needs a pre-sized destination buffer plus an
`out uint nBytesWritten`, it does not hand you back a `byte[]`.
**Code Free** — a ready-made `VoiceRecorder` component (`Steamworks/Voice Recorder` in the
Add Component menu) does the polling loop above for you:
- **Buffer Length** — how much recording time to accumulate before raising Voice Stream with the
data. Keep this low; a higher value only helps smooth out clips from a slow tick rate.
- **Is Recording** — `true` while the system is recording, `false` when it isn't. Setting this
field is literally how you start/stop recording (the setter calls `StartRecording()` /
`StopRecording()` internally).
- **On Stopped On Chat Restricted** — fires when Steam returns
`EVoiceResult.k_EVoiceResultRestricted`, i.e. the user is voice-chat restricted; recording is
auto-stopped when this fires.
- **On Voice Stream** — fires every poll interval (per Buffer Length) with the compressed
`byte[]` payload, whenever the Steamworks API actually has data.
The equivalent hand-rolled component shape, if you want the same polling logic in your own script
instead of adding `VoiceRecorder` directly (source: `VoiceRecorder.cs`):
```csharp
public float bufferLength = 0.25f;
public bool IsRecording { get; set; } // setter calls StartRecording()/StopRecording()
public UnityEvent onStoppedOnChatRestricted;
public UnityEvent<byte[]> onVoiceStream;
```
**Godot GDScript** — via the `SteamApi` native singleton (Foundation-provided, works with no
Toolkit installed):
```gdscript
SteamApi.StartVoiceRecording()
if SteamApi.GetAvailableVoice() > 0:
var compressed := SteamApi.GetVoice()
```
`GetAvailableVoice()` returns a plain `int` (bytes available, `0` if none or not ready) — **not**
an `EVoiceResult` like Unity. `GetVoice()` returns a `PackedByteArray` directly (empty if nothing
available) rather than filling a caller-provided buffer via an out-param. This is not a shorthand
or a simplification that drops error information — the native GDExtension C++ layer
(`SteamApi::GetVoice` in `SteamApi.cpp`) already does Unity's two-call
`GetAvailableVoice`/`GetVoice` dance and the `EVoiceResult` check internally, and only returns a
byte payload to script once it's confirmed good. There is no raw `EVoiceResult` exposed to
GDScript at all for this call — verified against `SteamApi.h`/`SteamApi.cpp` source, not assumed.
**Godot C#** — `API.Voice.Client` (Toolkit) is a 1:1 wrapper over the exact same native calls, so
it has the same simplified shape as GDScript, not Unity's:
```csharp
API.Voice.Client.StartVoiceRecording();
if (API.Voice.Client.GetAvailableVoice() > 0) {
byte[] compressed = API.Voice.Client.GetVoice();
}
```
Do not "fix" this to look like Unity's `out`-param shape — that would be wrong for Godot. The
facade dispatches through Godot's `Variant`-based `GodotObject.Call`, which has no equivalent of
a C# `out` parameter to begin with; the underlying native method already returns a plain value.
**Godot has no "Code Free" component equivalent** — grepped both the Foundation and Toolkit
source trees for `VoiceRecorder`/`VoiceStream` (Unity's inspector component names): zero hits.
Godot Voice is static-API-only; there is no scene node or inspector-configurable component to add.
Don't tell a Godot developer to look for a "Voice Recorder" node — it doesn't exist there.
**O3DE** — no Voice implementation exists in either Foundation or Toolkit source. Confirmed via
exhaustive grep, not inferred from absence of docs. Do not fabricate an API shape for this.
## Send Voice Data
Not a Toolkit/Foundation concern for any engine — you decide how compressed voice bytes travel
from sender to receiver (RPC, your own relay server, etc.), same as Lobby chat payloads. Applies
identically to Unity and Godot.
## Play Voice Data (decompress + playback)
**Unity C#** — the manual way:
```csharp
byte[] destBuffer = new byte[20000];
var result = API.Voice.Client.DecompressVoice(
compressed, destBuffer, out uint bytesWritten, API.Voice.Client.OptimalSampleRate);
if (result == EVoiceResult.k_EVoiceResultBufferTooSmall)
{
destBuffer = new byte[bytesWritten];
result = API.Voice.Client.DecompressVoice(
compressed, destBuffer, out bytesWritten, API.Voice.Client.OptimalSampleRate);
}
// feed destBuffer (raw PCM) to your own audio output from here
```
`API.Voice.Client.OptimalSampleRate` is a property (`SteamUser.GetVoiceOptimalSampleRate()`
under the hood), not a method — don't call it with `()`.
**Code Free** — a ready-made `VoiceStream` component (`Steamworks/Voice Stream`) handles
decoding + a looping `AudioClip` for you; feed it received bytes via `PlayVoiceData(byte[] buffer)`:
- **Output Source** — the `AudioSource` the decoded audio plays through. Make it a 3D-positioned
source parented to the speaking player's avatar for cheap proximity chat.
- **Sample Rate Method** — `Optimal` (Steam-recommended, default), `Native` (device's own output
rate), or `Custom`.
- **Custom Sample Rate** — the rate used only when Sample Rate Method is `Custom`.
- **Playback Delay** — seconds of silence pre-buffered before playback starts, to smooth over
jitter. Keep as low as possible, but it must exceed your server's tick rate or you'll stutter.
- **Encoding Time** — debug-only: peak milliseconds spent decoding the most recent packet, updated
every call to Play Voice Data.
**Godot GDScript** — Foundation only decompresses; feeding the result into actual audio output is
stock Godot (`AudioStreamGenerator`), not a Steamworks API:
```gdscript
var pcm := SteamApi.DecompressVoice(compressed, SteamApi.GetVoiceOptimalSampleRate())
# feed pcm into an AudioStreamGenerator from here — stock Godot audio, not Steamworks-specific
```
`DecompressVoice(compressed: PackedByteArray, sampleRate: int) -> PackedByteArray` — takes the
sample rate as an explicit second argument (no default in the native binding), returns an empty
`PackedByteArray` on any failure. Unlike Unity, there is **no buffer-too-small retry loop to
write** — the native implementation (`SteamApi::DecompressVoice` in `SteamApi.cpp`) allocates a
fixed internal scratch buffer (22050 × 2 × 2 = 88,200 bytes) and hands back only the actual
decompressed size; if Steam's decompress call fails for any reason (including a hypothetical
buffer-too-small case against that fixed size) you get back an empty array with no error detail,
not an `EVoiceResult` to branch on. Don't port Unity's `k_EVoiceResultBufferTooSmall` retry
pattern here — there's no result code to check and no growable buffer to retry into.
**Godot C#** — same caveat as GDScript, no dedicated "play" helper beyond decompression:
```csharp
byte[] pcm = API.Voice.Client.DecompressVoice(compressed, API.Voice.Client.GetVoiceOptimalSampleRate());
// feed pcm into audio output — stock Godot, not Steamworks-specific
```
The C# facade signature has one cosmetic difference from the raw native bind:
`DecompressVoice(byte[] compressed, int sampleRate = 48000)` gives `sampleRate` a C#-side default
of 48000 (the native GDExtension bind itself has no default) — passing
`API.Voice.Client.GetVoiceOptimalSampleRate()` explicitly, as shown, is still the correct and
recommended call.
**O3DE** — no Voice implementation exists in either Foundation or Toolkit source. Nothing to
document.
## If you're stuck
- Full human-readable article (all prose, screenshots, Code-Free inspector field docs this file
compresses out; also has the Unreal content neither phase touched):
Voice
- This file is generated from that article's raw content; if something here looks wrong, the
article is the fallback source of truth, not this file's memory.
---
Back to the full Steam agent index: https://heathen.group/agent-ref-steam-index/