Skip to content

Implement voice chat support (proximity + channel-based) #485

@adrielgro

Description

@adrielgro

Summary

Add voice chat support to BeamMP. This includes server-side routing, client-side capture/playback, and Lua mod integration. The implementation supports proximity-based spatial audio and named channel groups (for e.g. team radio), as well as music stream injection controlled by the car's in-game stereo slider.

Motivation

Voice communication is a frequently requested feature in the BeamMP community. The current workaround is to use external tools like Discord. Native voice chat allows server plugins to control who hears whom (proximity, channels, mute), and enables creative use cases like synchronized in-car music.

Components

This feature spans three repositories:

BeamMP-Server

  • New TVoiceChat class: named channel management, per-player server-side mute, proximity distance control, and a Lua audio injection API (SendAudio, SetStreamGain).
  • TServer::GlobalParser: handles 'F' voice packets — extracts sender position from vehicle data, builds a v2 broadcast packet, and forwards to channel members or players within proximity radius.
  • TNetwork::UDPSend: signature updated to const std::vector<uint8_t>& to avoid unnecessary copies.
  • Lua bindings: CreateChannel, DeleteChannel, AddPlayerToChannel, RemovePlayerFromChannel, MutePlayer, SetProximityDistance, SendAudio, SetStreamGain.
  • onPlayerVoice Lua event, throttled to once per 300 ms per player.

BeamMP-Launcher

  • New VoiceChat class: PortAudio microphone capture (mono 48 kHz, 20 ms Opus frames), Opus encoding/decoding, stereo playback with equal-power panning, jitter buffer, distance attenuation, music stream injection, mic gain (0–800 %), master volume, and device hot-swap.
  • Core.cpp: handles 'F' subcommands from the game (PTT start/stop, listener position/orientation, volume, device selection, mic gain, music volume).
  • GlobalHandler.cpp: forwards incoming 'F' UDP packets to VoiceChat::ProcessIncomingVoice.

BeamMP (mod)

  • New MPVoiceChat.lua extension: PTT with configurable key binding, mic sensitivity slider, music volume slider (sends broadcast gain to server), per-frame listener position/orientation updates, speaking indicators, and mic level meter.
  • MPCoreNetwork.lua: routes launcher voice data to MPVoiceChat.onLauncherData.

Wire Protocol (v2)

25-byte little-endian header followed by Opus payload:

Offset  Size  Field        Description
0       1     type         Always 'F' (0x46)
1       1     version      Protocol version (currently 2)
2       1     flags        0x01 = proximity, 0x02 = injected
3       2     sourceId     Sender client ID (uint16)
5       12    pos          Source world position XYZ (3x float)
17      4     maxDistance  Spatialization cutoff in meters (0 = unlimited)
21      4     gain         Broadcast gain [0.0, 1.0]
25+     N     opusData     Opus-encoded audio payload

PROTOCOL_VERSION must be bumped before any layout change is released.

Testing

Tested locally with 2 players:

  • Proximity audio attenuates correctly with distance and cuts off beyond maxDistance.
  • Channel-based audio bypasses the proximity check and reaches all channel members.
  • Server-side mute silences the target player for all peers.
  • Car stereo slider adjusts music volume for all listeners in real time.

Dependencies

The server has no new dependencies.

The launcher adds two industry-standard libraries via vcpkg (already used for other dependencies):

  • Opus — the de facto standard codec for low-latency voice (used by Discord, WebRTC, Zoom, and countless games). IETF RFC 6716. MIT-compatible license. Available on all target platforms (Windows, Linux, macOS) via vcpkg without system-level requirements.
  • PortAudio — a thin, cross-platform audio I/O abstraction (MIT license). Used by Audacity, among others. It wraps WASAPI/DirectSound on Windows, ALSA/PulseAudio/PipeWire on Linux, and CoreAudio on macOS. No system dependencies beyond what BeamNG.drive already requires to run.

Both are available as prebuilt binaries in the vcpkg binary cache and do not meaningfully increase CI build time.

UDP Transport and Packet Loss Handling

Voice packets are sent over the existing BeamMP UDP path (same socket, no new ports). The design deliberately accepts UDP's unreliability because:

  • Opus PLC (Packet Loss Concealment)opus_decode with a null data pointer generates a comfort-noise/interpolation frame, making single-packet loss nearly imperceptible to the listener.
  • Jitter buffer — the client buffers 3x20 ms = 60 ms before starting playback, absorbing typical internet jitter without added latency for LAN play.
  • Frame size: 20 ms — the Opus-recommended sweet spot: low latency, good compression, and the smallest unit for which PLC works well.
  • Bitrate: 24 kbps — sufficient for clear speech with Opus; well within the bandwidth budget of a BeamMP session.
  • Reordering — out-of-order UDP packets are simply decoded in arrival order. Opus is stateful but tolerant of single-frame discontinuities; the jitter buffer absorbs minor reordering.
  • No retransmission — retransmitting stale audio would increase latency with no perceptible quality benefit. This matches the approach taken by every major real-time voice implementation.

Security and Abuse Mitigation

  • Rate limiting — the server already processes one UDP packet per client per network loop tick. A client sending voice frames faster than the capture rate (50/sec at 20 ms) produces no extra broadcast load because each packet is processed and forwarded exactly once.
  • Flood protection — the onPlayerVoice Lua event is throttled server-side to once per 300 ms per player, preventing event-queue flooding even if a malicious client sends crafted 'F' packets at high rate.
  • Bandwidth cap — at 24 kbps Opus + 25-byte header overhead per frame, one active speaker adds ~27 kbps to the server's outbound bandwidth per recipient. This is negligible compared to vehicle position packet load.
  • PTT-only capture — the client never sends audio unless the player holds the PTT key. There is no always-on microphone mode.
  • Server-side muteMutePlayer(id, true) drops all incoming voice packets from that player before any processing or broadcast, giving server admins and plugins full control.

Audio Injection (SendAudio / SetStreamGain)

These Lua APIs are intentionally restricted to server-side plugin scripts — they are not callable by clients. Intended use cases are server-hosted music streams and admin announcements.

  • Gain is clamped to [0.0, 1.0] server-side before the packet is built, preventing over-amplification.
  • Volume is further scaled client-side by the listener's local music volume setting, so the listener always retains control.
  • Injected audio is flagged (FLAG_INJECTED = 0x02) and rendered separately from voice, allowing the client to apply different volume and panning treatment.
  • Abuse by malicious plugins (e.g. loud noise injection) is a server operator responsibility, same as any other Lua API (SendChatMessage, DropPlayer, etc.).

Launcher Scope

Adding audio I/O to the launcher was a deliberate architectural choice:

  • The launcher already owns the network connection to the server and the IPC channel to the game. It is the only process with access to both, making it the natural host for the encode/decode pipeline.
  • The game engine (Lua sandbox) cannot access native audio hardware directly. Routing through the launcher avoids needing a native BeamNG mod DLL.
  • The audio code is fully encapsulated in the VoiceChat class and does not touch any existing launcher subsystems. It can be disabled at compile time by removing the two vcpkg dependencies and the VoiceChat translation unit.

Notes

  • The 'F' packet code was previously unused in the protocol.
  • The feature is opt-in: voice chat is inactive unless the player holds the PTT key.
  • Protocol version is explicit in every packet header (PROTOCOL_VERSION = 2). Future wire format changes require a version bump, making mixed-version deployments detectable.
Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions