VoiceChat v2 — proximity + radio/phone voice chat for open.mp and
SA-MP 0.3.7 R5, with a Windows .asi client. Server-authoritative positioning,
cryptographic-token identity, end-to-end AEAD-encrypted audio, neural noise suppression,
and an in-game ImGui control panel.
- What it does
- Architecture
- Repository layout
- Build
- Install / deploy
- Networking & ports
- Configuration
- Pawn API
- In-game controls (client)
- Test filterscript
- Status
- Proximity voice — hear nearby players in 3D: logarithmic distance falloff, stereo panning, interior/virtual-world gating, and occlusion (muffled through walls and closed vehicles). Whisper / Normal / Shout ranges, or a custom range.
- Radio & phone channels — filtered "buses" (bandpass + squelch + half-duplex garble for radio, narrowband colouring for phone), mixed per-listener on the server.
- Generic channel primitive — the plugin exposes channels; gameplay (faction radio, taxi dispatch, phone calls) is built in Pawn on top of channels + filters + members.
- Mix policy —
MIX/DUCK/EXCLUSIVEacross a listener's active buses; proximity is never ducked below a floor. - Per-pair mute — a listener can locally silence one target without affecting anyone else.
- Speakerphone (J6) — a player's received call audio is also played into their proximity so bystanders hear the call "out of the phone".
- Client audio quality — Opus codec, RNNoise neural noise suppression, an adaptive jitter buffer, exclusive Push-to-Talk or Voice-Activation modes.
- In-game UI — a Dear ImGui control panel (INSERT) for volumes, devices, mode and PTT-key rebind, plus a "who's talking" HUD and optional floating labels above heads.
- Security — identity is a per-connect cryptographic token (not the source IP); every relay↔client datagram is sealed with XChaCha20-Poly1305 AEAD; the server is authoritative for position/flags (anti-spoof).
Hybrid mixing model:
- Proximity voice is relayed per speaker as opaque Opus with a server-computed volume/pan/occlusion; the client spatialises and mixes it (lowest latency).
- Filtered channels (radio/phone) are decoded, filtered and mixed per listener on the server, re-encoded, and sent as one bus stream the client just plays.
┌─────────────────────────── game server host ───────────────────────────┐
GTA SA client │ SA-MP plugin (.dll) / open.mp component ── loopback IPC ──► relay │
┌────────────┐ │ (mints tokens, pushes authoritative pos/flags/channels) (.exe) │
│ VoiceChat │ │ │ core │
│ .asi │◄┼──── AEAD audio (UDP) ──────────────────────────────────────────┘ engine │
│ WASAPI+Opus│ │ proximity (raw Opus) + buses (server-mixed) │
└────────────┘ └──────────────────────────────────────────────────────────────────────┘
Six modules, one tree:
| Module | Role | Bitness |
|---|---|---|
shared/ |
wire protocol, crypto (libsodium), INI, UDP — platform-free | any |
core/ |
routing engine: proximity, bus router, channels, mix policy | any |
relay/ |
transport + AEAD shell around the core (standalone process) | x64 |
samp/ |
SA-MP 0.3.7 R5 plugin bridge (Pawn natives → core) | x86 |
omp/ |
open.mp component bridge (same core) | x64 |
client/ |
Windows .asi: WASAPI capture/playback, Opus, RNNoise, ImGui panel |
x86 |
See docs/ARCHITECTURE.md and docs/SECURITY.md
for the locked design decisions.
shared/ protocol.h, crypto, ini.h, udp.h, relay_launcher.h
core/ engine, bus_router, proximity, dsp, mixer, registry, control_link, opus_codec
relay/ relay.cpp + main.cpp (voice_relay) config.h reads voice.ini
samp/ plugin.cpp (+ include/voicechat.inc, voicechat_ui.inc; vendored sdk/)
omp/ component.cpp (open.mp; gated by VC_WITH_OMP_SDK)
client/ dllmain, audio_capture, audio_playback, voice_client, net_session,
d3d9_overlay (ImGui), voice_panel, overlay (HUD), client_config.h
examples/ voicechat_test.pwn — a command-per-native test filterscript
tests/ crypto, dsp, e2e handshake/status/config, scale benchmark
docs/ ARCHITECTURE.md, BUILD.md, SECURITY.md
build.ps1 one-shot local build + deploy
voice.ini server config (annotated)
Third-party deps are fetched + built by CMake (FetchContent): libsodium, opus,
Dear ImGui, MinHook, RNNoise (cpuimage/rnnoise). The SA-MP SDK is vendored
in samp/sdk/. Nothing else needs to be installed.
powershell -ExecutionPolicy Bypass -File .\build.ps1Builds the relay, the SA-MP plugin and the .asi client (Release, x86), compiles the
test filterscript with pawncc, and deploys everything to the local server + each GTA
install (with .bak-<stamp> backups). Flags: -NoDeploy, -NoPawn, -Configure.
Locked files (server/GTA running) are skipped with a warning instead of aborting.
# production config (real crypto + opus + full client), 32-bit:
cmake -S . -B build -A Win32 -DVC_NO_SODIUM=OFF -DVC_WITH_OPUS=ON -DVC_BUILD_CLIENT=ON
cmake --build build --config Release
ctest --test-dir build -C ReleaseKey CMake options (defaults in brackets):
| Option | Meaning |
|---|---|
VC_BUILD_RELAY [ON] |
build the standalone relay |
VC_BUILD_SAMP [ON] |
build the SA-MP plugin (x86) |
VC_BUILD_OMP [OFF] |
build the open.mp component (needs omp/sdk, x64) |
VC_BUILD_CLIENT [OFF] |
build the .asi client (Windows x86) |
VC_WITH_OPUS [OFF] |
link libopus (required for buses + client) |
VC_NO_SODIUM [ON] |
pass-through crypto for testing — set OFF for production |
VC_CLIENT_GUI [ON] |
ImGui D3D9 control panel in the client |
VC_WITH_RNNOISE [ON] |
RNNoise noise suppression in the client |
VC_BUILD_TESTS [ON] |
build the verification tests |
The client and SA-MP plugin must be x86 (
-A Win32); the relay and open.mp component are x64. Build the pieces you need per configure — seedocs/BUILD.md.
GitHub Actions (.github/workflows/build.yml) builds the
Windows x86 production stack and a Linux x64 relay+core, runs the tests, and
uploads the binaries as artifacts on every push/PR to main.
Pushing a v* tag runs .github/workflows/release.yml,
which publishes a GitHub Release with ready-to-use bundles — voicechat-client-win32.zip,
voicechat-server-win.zip, and voicechat-relay-linux-x64.tar.gz (each with an
INSTALL.txt):
git tag v0.1.0 && git push origin v0.1.0Building on Linux (relay, 32-bit SA-MP .so, open.mp component): see
docs/LINUX.md.
Server (SA-MP or open.mp):
plugins/VoiceChat.dll(SA-MP) orcomponents/(open.mp) +components/voice_relay.exe.qawno/include/voicechat.inc(+voicechat_ui.inc) for compiling your gamemode.voice.ininext to the server executable.- Add the plugin to
config.json(legacy_plugins: ["VoiceChat"]) /server.cfg. - Call the lifecycle natives from your gamemode (see below). The plugin launches + supervises the relay automatically.
Client (each player's GTA SA folder):
VoiceChat.asi(needs an ASI loader, e.g. SilentPatch/CLEO).voicechat.ini— setrelay_ipto the server's IP (127.0.0.1if you host locally).
UDP only. Full details + firewall commands in docs/NETWORKING.md.
| Port (default) | Direction | Expose? |
|---|---|---|
| 7779 audio | client ⇄ relay (voice + handshake) | YES — open inbound on the server |
| 7778 control | plugin → relay (loopback IPC) | No — keep local |
| 7780 cmd | server → client (token delivery) | Client side (LAN: automatic; internet: may need forward) |
Open UDP 7779 on the server:
# Windows (admin)
netsh advfirewall firewall add rule name="VoiceChat 7779" dir=in action=allow protocol=UDP localport=7779# Linux
sudo ufw allow 7779/udpAll ports are overridable: server in voice.ini [network], client in
voicechat.ini [network] (only audio_port must match on both sides).
Annotated; the relay reads it at boot and the plugin re-pushes it over IPC. Sections:
[network] (ports), [security] (IPC secret), [proximity] (whisper/normal/shout
ranges, falloff exponent, occlusion, nearest_n, max_streams_per_client), [audio]
(vad_allowed, default_mode, bitrate), [radio] (bandpass/phone bands, drive, squelch,
static, garble), [behavior] (suppress_double_play, speakerphone server-wide
enable, dead_can_talk, proximity_floor, limiter ceiling). Edit live and call
Voice_ReloadConfig().
[network]
relay_ip = 127.0.0.1 ; server IP (LAN IP if the server is on another PC)
audio_port = 7779
cmd_port = 7780
[keys]
ptt_key = B ; proximity push-to-talk
ptt_key2 = N ; radio push-to-talk (use with your /radio command)
mute_key = F9
panel_key = INSERT ; open the settings panel
[audio]
master_volume = 1.0
mic_volume = 1.0
vad_enabled = false ; false = PTT only, true = Voice-Activation only (exclusive)
vad_threshold = 0.02
deafen = false
noise_suppress = true ; RNNoise
[ui]
show_overlay = true
overlay_key = F8
overlay_anchor = radar ; above the minimap; or "topleft" + overlay_x/overlay_y#include <voicechat> (and optionally #include <voicechat_ui>).
Voice_CreateChannel(filter, bool:positional, priority = 100); // -> channelid
Voice_DestroyChannel(channelid);
Voice_AddToChannel(playerid, channelid);
Voice_RemoveFromChannel(playerid, channelid);
Voice_IsInChannel(playerid, channelid);
Voice_SetChannelFilter(channelid, filter); // VOICE_FILTER_NONE/RADIO/PHONE
Voice_SetChannelPriority(channelid, priority);
// queries (answered from the bridge mirror, no round-trip):
Voice_IsValidChannel(channelid);
Voice_GetChannelCount();
Voice_GetChannelMemberCount(channelid);
Voice_GetChannelPlayers(channelid, players[], maxplayers = sizeof players); // -> countVoice_UpdatePosition(playerid, Float:x, Float:y, Float:z, interior, vworld, vehState);
Voice_SetPlayerRange(playerid, preset, Float:override_units = 0.0); // VOICE_RANGE_*
Voice_SetPlayerFlags(playerid, bool:muted, bool:deafen, bool:alive, bool:spectator);
Voice_SetTransmitting(playerid, bool:on, channelMask = 0); // 1 << (channelid & 31)
Voice_SetMixPolicy(playerid, policy, Float:duckLevel = 0.5, Float:proximityFloor = 0.25);
Voice_MutePlayer(playerid, targetid, bool:mute = true); // per-pair (J11)
Voice_SetSpeakerphone(playerid, bool:on = true); // J6Voice_OnPlayerConnect(playerid, const ip[]); // GetPlayerIp — mints+delivers the token
Voice_OnPlayerDisconnect(playerid);
Voice_ReloadConfig(); // re-read voice.ini at runtime
stock Voice_SetupProximity(); // convenience: a proximity channelforward OnPlayerStartTalking(playerid);
forward OnPlayerStopTalking(playerid);
forward OnPlayerRadioKey(playerid, channelid, bool:down);voicechat_ui.inc turns those into floating "(( talking ))" / "(( radio ))" labels above
heads — call VoiceUI_OnStartTalking/OnStopTalking/OnRadioKey/OnDisconnect from your
callback bodies.
Minimal wiring:
#include <voicechat>
#include <voicechat_ui>
public OnPlayerConnect(playerid) {
new ip[16]; GetPlayerIp(playerid, ip, sizeof ip);
Voice_OnPlayerConnect(playerid, ip);
Voice_AddToChannel(playerid, gProximity);
Voice_SetPlayerRange(playerid, VOICE_RANGE_NORMAL);
return 1;
}
public OnPlayerUpdate(playerid) {
new Float:x,Float:y,Float:z; GetPlayerPos(playerid,x,y,z);
new veh = GetPlayerVehicleID(playerid) ? VOICE_VEH_CLOSED : VOICE_ONFOOT;
Voice_UpdatePosition(playerid,x,y,z,GetPlayerInterior(playerid),GetPlayerVirtualWorld(playerid),veh);
return 1;
}
public OnPlayerStartTalking(playerid) { VoiceUI_OnStartTalking(playerid); return 1; }
public OnPlayerStopTalking(playerid) { VoiceUI_OnStopTalking(playerid); return 1; }- B — talk on proximity (PTT). N — talk on radio/phone (PTT).
- F9 — self-mute. F8 — toggle the "who's talking" HUD.
- INSERT — open the settings panel (volumes, devices, PTT/VAD mode, key rebind, noise suppression, per-player local mute/volume).
examples/voicechat_test.pwn exposes one chat command per
native — drop it in as a side-script and type /vhelp:
| Command | Tests |
|---|---|
/vrange whisper|normal|shout, /vrangeu <u> |
proximity range |
/vradio, /vphone, /vinchan |
bus membership + queries |
/vspeaker |
J6 speakerphone toggle |
/vfilter, /vprio, /vchans |
live channel tweaks + queries |
/vmute, /vdeafen, /valive, /vspec |
player flags |
/vmix mix|duck|exclusive |
mix policy |
/vpmute <id>, /vpunmute <id> |
per-pair mute |
/vreload, /vstatus |
config reload + state dump |
Core + all major features are implemented and validated in-game (2-player proximity with
occlusion + falloff, radio/phone, speakerphone, RNNoise, the ImGui panel). Remaining:
AEC (acoustic echo cancellation — deferred; headphones avoid echo) and a stale
loopback test (e2e_handshake, excluded from CI).