Configuration

Complete configuration reference for Tommy's Radio — config.lua, admin panel, zones, channels, alerts, and tones.

config.luaAdmin Panel

Configuration Overview

Two sources:

SourceContainsEdited via
config.luaNetwork, keybinds, zones/channels, alerts, callbacksText editor (requires restart)
data.jsonAudio, FX, bonking, callsigns, 3D, GPS rate, default layoutsIn-game admin panel (live)

data.json changes broadcast to clients immediately — no restart.


config.lua Reference

Network & Authentication

config.lua
Config = {
    serverAddress = "",           -- Connection URL sent to clients. Leave empty for auto-detect.
                                  -- Set to "http://IP:PORT/" if using a proxy or specific IP.
    serverPort    = 7777,         -- Port for voice server and dispatch panel
    authToken     = "changeme",   -- Secret key for API authentication. Change this.
    dispatchNacId = "141",        -- Dispatch panel login password
}

General

config.lua
Config = {
    checkForUpdates = true,       -- Check for updates on resource start
    healthCheck     = true,       -- Run port reachability check on start
    logLevel        = 3,          -- 0=Error, 1=Warn, 2=Minimal, 3=Normal, 4=Debug, 5=Verbose
    useDiscordAuth  = false,      -- Use Discord OAuth for dispatch panel (see Dispatch Panel page)
}

Keybinds

config.lua
Config.controls = {
    talkRadioKey       = "B",     -- Push-to-talk
    toggleRadioKey     = "F6",    -- Open/close radio
    closeRadioKey      = "",      -- Close radio only (separate from toggle)
    powerBtnKey        = "",      -- Power on/off
    channelUpKey       = "",      -- Next channel
    channelDownKey     = "",      -- Previous channel
    zoneUpKey          = "",      -- Next zone
    zoneDownKey        = "",      -- Previous zone
    menuUpKey          = "",      -- Navigate menu up
    menuDownKey        = "",      -- Navigate menu down
    menuRightKey       = "",      -- Navigate menu right
    menuLeftKey        = "",      -- Navigate menu left
    menuHomeKey        = "",      -- Return to menu home
    menuBtn1Key        = "",      -- Menu button 1 (context-dependent)
    menuBtn2Key        = "",      -- Menu button 2 (context-dependent)
    menuBtn3Key        = "",      -- Menu button 3 (context-dependent)
    emergencyBtnKey    = "",      -- Panic / emergency button
    styleUpKey         = "",      -- Next radio model
    styleDownKey       = "",      -- Previous radio model
    voiceVolumeUpKey   = "EQUALS",   -- Voice volume up
    voiceVolumeDownKey = "MINUS",    -- Voice volume down
    sfxVolumeUpKey     = "RBRACKET", -- SFX volume up
    sfxVolumeDownKey   = "LBRACKET", -- SFX volume down
    volume3DUpKey      = "",         -- 3D audio volume up
    volume3DDownKey    = "",         -- 3D audio volume down
    -- Leave empty ("") to disable a keybind.
    -- Players can also rebind any of these in FiveM → Settings → Key Bindings → FiveM.
}

Signal Towers

Tower positions for the signal-strength icon and optional audio degradation.

config.lua
Config.signalTowerCoordinates = {
    { x = 1860.0,  y = 3677.0,  z = 33.0 },
    { x = 449.0,   y = -992.0,  z = 30.0 },
    -- Add, remove, or move coordinates to match your map
}

The radio shows more signal bars when closer to a tower. When signal degradation is enabled (via admin panel), distance also affects audio quality — further from towers means more garbled audio with random dropouts.

Battery

The batteryTick callback controls battery drain and charge rates.

config.lua
Config.batteryTick = function(currentBattery, deltaTime)
    local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
    if vehicle ~= 0 then
        return math.min(100.0, currentBattery + (0.5 * deltaTime))  -- Charge in vehicle
    else
        return math.max(0.0, currentBattery - (0.1 * deltaTime))    -- Drain on foot
    end
end

Client Callbacks

talkCheck

Runs before each PTT transmission. Return false to block.

config.lua
Config.talkCheck = function()
    if IsPlayerDead(PlayerId()) then return false end
    if IsPedSwimming(PlayerPedId()) then return false end
    -- Add custom conditions: handcuffs, tackle, etc.
    return true
end

bgSirenCheck

Determines if siren background SFX should play when a player transmits. Other players on the channel hear the siren looping behind the voice audio.

The default bgSirenCheck is provider-agnostic — it walks through four siren providers in order and uses the first one that claims ownership of the vehicle:

OrderProviderDetectionTone info?
1tELSexports['tELS']:IsSirenActive(vehicle) — authoritative. nil return = not a tELS vehicle, fall throughYes (via tels:sirenTone / tels:auxSirenTone state bags)
2LVCCached state_lxsiren[vehicle] tone ID from lvc:UpdateThirdPartyYes (tone ID)
3JLSexports['Jace3483_JLS']:IsSirenActive(vehicle) — authoritative. nil = not JLS, fall throughNo (bool only)
4Native GTAIsVehicleSirenOn(vehicle)No

Return values:

ReturnBehavior
false / nilNo siren audio
trueDefault siren (bgSiren.wav)
"filename.wav"Custom audio file from client/radios/default/sounds/

Default behavior works out of the box — no changes needed if you just want the generic siren loop whenever any supported provider reports a siren active.

Per-Provider Tone Mapping

To play a specific WAV matching the active siren tone, fill in Config.sirenToneFiles. The default bgSirenCheck consults this table automatically; any unmapped tone (or an omitted provider) falls back to bgSiren.wav.

config.lua
Config.sirenToneFiles = {
    -- tELS main/aux tone index (1-based, matches SIREN_TONES):
    --   1 = Wail, 2 = Yelp, 3 = Priority, 4 = Powercall, 5 = Custom
    tels = {
        -- [1] = "wail.wav",
        -- [2] = "yelp.wav",
    },
    -- LVC state_lxsiren tone index:
    --   1 = Airhorn, 2 = Wail, 3 = Yelp, 4 = Priority,
    --   5-10 = Custom A-F, 11 = Powercall, 12-14 = Fire tones
    lvc = {
        -- [2] = "wail.wav",
        -- [3] = "yelp.wav",
    },
    -- JLS exposes only a bool (no tone info) → always uses default bgSiren.wav.
    -- Native GTA sirens also fall through to default bgSiren.wav.
}

Drop the referenced WAVs into client/radios/default/sounds/. With [2] = "wail.wav" under lvc, a player running LVC wail hears sounds/wail.wav over the radio instead of the generic loop.

NotifySirenChanged Export

The siren state is polled every ~500ms to pick up changes. Custom ELS integrators that flip siren state outside the built-in providers' event paths can force an immediate re-poll (within one frame) by calling:

exports['tRadio']:NotifySirenChanged()

Call this right after flipping your backing flag. Useful when your ELS uses its own events rather than LVC's lvc:UpdateThirdParty or tELS' state bags.

The siren audio updates between transmissions. If a player switches siren tones mid-transmission, the new tone takes effect the next time they key up.

Server Callbacks

These three functions run server-side and control who can use the radio and what they can access.

The string returned by getUserNacId is the player's NAC ID — the radio compares it against:

  • Zone nacIds — player can see the zone only if their NAC ID is in the list
  • Channel allowedNacs — player can connect and transmit only if their NAC ID is in the list
  • Channel scanAllowedNacs — player can scan (listen only) if their NAC ID is in the list
config.lua
-- Gate: who can use the radio at all (return false to block)
Config.radioAccessCheck = function(playerId)
    return true
end

-- Identity: NAC ID assigned to this player (matched against zones/channels)
Config.getUserNacId = function(serverId)
    return "141"
end

-- Display name shown on the radio and in the dispatch panel
Config.getPlayerName = function(serverId)
    return GetPlayerName(serverId)
end

For framework-specific examples (QBCore, ESX, Qbox job-based NAC IDs), see Framework Integration. When a player's job changes, call exports['tRadio']:refreshNacId(serverId) to update their access immediately.

ACE Permissions

As an alternative to the getUserNacId callback, you can assign permissions directly through FiveM ACE entries in server.cfg. ACE permissions are additive — they work alongside NAC IDs, so existing configs don't need to change.

NAC ID via ACE

Assign a NAC ID to a player or group without writing Lua code. If getUserNacId returns nil, the server checks for a matching tRadio.nac.* ACE:

server.cfg
# Give police group NAC ID "141"
add_ace group.police tRadio.nac.141 allow
add_principal identifier.steam:STEAM_ID group.police

# Give EMS NAC ID "110"
add_ace group.ems tRadio.nac.110 allow

If getUserNacId returns a value, it takes priority over ACE NAC IDs. To use ACEs exclusively, have getUserNacId return nil.

Per-Frequency ACE Permissions

Grant access to specific frequencies without a NAC ID. The frequency is encoded by removing the decimal point (e.g., 154.815154815).

ACE PermissionGrants
tRadio.connect.{freq}Transmit on a frequency
tRadio.scan.{freq}Scan (listen only) on a frequency
tRadio.gps.{freq}See GPS blips for a frequency
tRadio.zone.{N}Access zone N (1-based index from Config.zones)
server.cfg
# EMS can transmit on 154.755 and 154.815
add_ace group.ems tRadio.connect.154755 allow
add_ace group.ems tRadio.connect.154815 allow

# EMS can scan 856.1125
add_ace group.ems tRadio.scan.8561125 allow

# EMS can see GPS blips on 154.755
add_ace group.ems tRadio.gps.154755 allow

# EMS can access zone 1 (Statewide)
add_ace group.ems tRadio.zone.1 allow

Admin Radio Features via ACE

Players with the tlib.admin ACE permission (same permission used for the in-game admin panel) gain access to additional radio controls — the SGN alert button and trunked CT/TK toggle.

server.cfg
add_ace group.admin tlib.admin allow

Combining NAC IDs and ACE Permissions

A player's effective access is the union of their NAC ID permissions and ACE permissions. For example, a player with NAC ID "110" and tRadio.connect.154815 can access everything NAC "110" grants plus transmit on frequency 154.815.

When permissions change (e.g., a job update triggers refreshNacId), both NAC ID and ACE permissions are re-resolved and sent to the client. If a player loses access to their current channel, they are automatically disconnected.

How Access Resolves

  1. Radio accessConfig.radioAccessCheck(playerId) is the master gate. false = no radio at all.
  2. NAC IDConfig.getUserNacId(serverId) first. If it returns nil/empty, fall back to tRadio.nac.* ACE. Neither = no NAC ID.
  3. Zone visibility — NAC ID in Zone.nacIds or tRadio.zone.{N} ACE.
  4. Connect + transmit — NAC ID in Channel.allowedNacs or tRadio.connect.{freq} ACE.
  5. Scan — connect access, or NAC ID in Channel.scanAllowedNacs, or tRadio.scan.{freq} ACE.
  6. GPS visibility — NAC ID in gps.visibleToNacs or tRadio.gps.{freq} ACE.
  7. Admin controls — SGN + trunked CT/TK buttons require tlib.admin ACE.

refreshNacId(serverId) / refreshPlayerInfo() re-run this live. Players on an inaccessible channel are auto-disconnected.


Zones & Channels

Zones group channels together. Players see zones based on their NAC ID matching the zone's nacIds list.

Zone Structure

config.lua
Config.zones = {
    [1] = {
        name = "Statewide",
        nacIds = { "141", "200" },  -- NAC IDs that can see this zone
        Channels = {
            [1] = {
                name = "DISPATCH",
                type = "conventional",
                frequency = 154.755,
                allowedNacs = { "141" },
                scanAllowedNacs = { "200" },
                gps = {
                    color = 54,
                    visibleToNacs = { "141" },
                },
            },
        },
    },
}

Channel Types

Conventional

Single shared frequency — all connected players hear each other regardless of location.

Conventional Channel
{
    name = "DISPATCH",
    type = "conventional",
    frequency = 154.755,
    allowedNacs = { "141" },
    scanAllowedNacs = { "200" },
    gps = { color = 54, visibleToNacs = { "141" } },
}

Trunked

Location-based frequency assignment. Units at different locations get separate sub-frequencies automatically. Dispatchers can still broadcast to all units on the control frequency.

Trunked Channel
{
    name = "CAR-TO-CAR",
    type = "trunked",
    frequency = 856.1125,                  -- Control frequency
    frequencyRange = { 856.000, 859.000 }, -- Available range
    coverage = 500,                        -- Radius in meters
    allowedNacs = { "141" },
    gps = { color = 25, visibleToNacs = { "141" } },
}

Channel Field Reference

FieldTypeDescription
namestringDisplay name on the radio
typestring"conventional" or "trunked"
frequencynumberFrequency in MHz (must be unique across all zones)
frequencyRangetableTrunked only — { min, max } range
coveragenumberTrunked only — radius in meters
allowedNacstableNAC IDs that can connect and scan
scanAllowedNacstableNAC IDs that can scan only (not connect)
gps.colornumberBlip color ID
gps.visibleToNacstableNAC IDs that can see this channel's GPS blips

Alerts

Alerts are defined in Config.alerts. The first entry ([1]) is the default triggered by the in-game SGN button. Players whose NAC ID matches dispatchNacId can trigger it from the radio; all alerts can also be triggered from the dispatch panel or API.

Alert Lifecycle

Three phases, each with its own optional tone:

  1. Activate — fires once on trigger. Banner shows on every radio connected or scanning the channel; tones.activate plays.
  2. RepeatisPersistent = true only. tones.repeat fires every repeatInterval ms (default: random 5–10 s). Banner flashes again unless repeatShowBanner = false. Each channel has its own timer.
  3. Deactivate — fires once when cleared. tones.deactivate plays; green RESUME banner shows (overridable via deactivateLabel / deactivateColor).

One-shot alerts (isPersistent = false) run the activate phase only.

Tone Styles

Simple — one tone for all phases:

config.lua
{
    name = "SIGNAL 3",
    color = "#0049d1",
    isPersistent = true,
    tone = "ALERT_A",
}

Per-phase — independent tones for activate, repeat, and deactivate:

config.lua
{
    name = "SIGNAL 100",
    color = "#d19d00",
    isPersistent = true,
    tones = {
        activate   = "PRIORITY",
        ["repeat"] = "PRIORITY_REPEAT",
        deactivate = "ALERT_C",
    },
}

Any phase can be omitted — it falls back to the tone field, or plays nothing if absent.

Alert Field Reference

FieldTypeDefaultDescription
namestringDisplay text on the radio
colorstringHex colour of the alert banner
isPersistentboolfalseStays active until manually cleared
tonestringFallback tone for all phases
tones.activatestringTone played once on activation
tones["repeat"]stringTone played on each repeat loop
tones.deactivatestringTone played once on clearance
repeatIntervalnumber (ms)random 5-10sGap between repeat fires
repeatShowBannerbooltrueFlash the banner on each repeat
deactivateLabelstring"RESUME"Banner text shown on clearance
deactivateColorstring"#126300"Colour of the clearance banner
toneOnlyboolfalsePlay tone only, no visual banner

Examples

A mix of persistent and one-shot alerts covering common dispatch needs:

config.lua
Config.alerts = {
    -- [1] is what the SGN button on the radio triggers by default
    [1] = {
        name = "SIGNAL 100",
        color = "#a38718",
        isPersistent = true,
        tones = {
            activate   = "PRIORITY",
            ["repeat"] = "PRIORITY_REPEAT",
            deactivate = "ALERT_C",
        },
        repeatInterval   = 15000,
        repeatShowBanner = false,
        deactivateLabel  = "RESUME",
        deactivateColor  = "#1a8a38",
    },

    -- Persistent with a single tone for every phase
    [2] = {
        name = "SIGNAL 3",
        color = "#1852a3",
        isPersistent = true,
        tone = "ALERT_A",
    },

    -- One-shot with banner + tone
    [3] = {
        name = "Ping",
        color = "#1852a3",
        tone = "ALERT_B",
    },

    -- Tone-only — plays sound, no banner
    [4] = {
        name = "Boop",
        color = "#1c4ba3",
        toneOnly = true,
        tone = "BONK",
    },
}

Triggering alerts

  • In-game SGN button — fires Config.alerts[1] on the connected channel, press again to clear. Available to players whose NAC ID matches dispatchNacId or who have tlib.admin ACE.
  • Dispatch panel — picks any alert, fires on one or all channels.
  • Exports / HTTPexports['radio']:setAlertOnChannel(freq, true, "Signal 100") or POST /radio/dispatch/alert/trigger. Takes the alert's name (case-insensitive) or 1-based index.

To restrict SGN to dispatchers, set dispatchNacId to a NAC ID only they return.


Admin Panel (data.json)

Settings managed through the in-game admin panel. Access requires the tlib.admin ACE permission.

Grant it in server.cfg:

server.cfg
add_ace identifier.steam:STEAM_ID tlib.admin allow

Or grant to a group:

server.cfg
add_ace group.admin tlib.admin allow
add_principal identifier.steam:STEAM_ID group.admin

Open via the /tradio command > Admin Settings.

Audio Settings

SettingDefaultDescription
voiceVolume65Default voice volume (0-100)
sfxVolume35Default SFX/tone volume (0-100)
volumeStep5Volume adjustment increment
pttReleaseDelay350Milliseconds to keep transmitting after releasing PTT
pttTriggersProximitytruePTT also triggers proximity voice

Radio FX

Two independent audio processors. Both toggle on/off separately; enabling both stacks them (analog applied after the vocoder).

Analog FX chain (fxEnabled) — classic radio sound. Signal path:

mic → inputGain → highpass → lowpass → softener EQ → compressor
    → limiter → tube saturation → mid EQ → voice volume → output

P25 IMBE vocoder (p25Enabled) — real P25 codec. See P25 / IMBE Vocoder below.

SettingDefaultWhat it does
radioFx.fxEnabledtrueMaster switch for the analog chain
radioFx.p25EnabledfalseMaster switch for the IMBE vocoder
radioFx.inputGain1.65Pre-chain gain. Higher = louder into the chain, more likely to saturate
radioFx.highpassFrequency250Rolls off bass below this cutoff (Hz). Raise to thin the voice
radioFx.lowpassFrequency3500Rolls off treble above this cutoff (Hz). Lower = more muffled / telephone-like
radioFx.compression60Dynamic range compression. High values flatten peaks, add "punch" but can pump
radioFx.distortion30Tube-saturation drive. Adds harmonic grit; too high = crackle
radioFx.distortionMode"classic"Saturation curve. "classic" = old-radio hard-clip; "tube" = smooth tube saturation
radioFx.midBoost2Mid-frequency EQ boost (dB) around speech presence range

Tuning guidelines

ProblemFix
Too muffled / telephone-likeRaise lowpassFrequency (4000–5000) or lower compression
Too tinny / harshLower highpassFrequency (150–200) or reduce midBoost
Voice pumpingLower compression
Crackling / distortedLower inputGain (0.8–1.2) or distortion
Clean radio sounddistortion = 0, compression ≈ 20

P25 and analog FX can be used independently or together. Using both stacks the analog chain on top of the vocoder output.

P25 / IMBE Vocoder

WebAssembly build of op25's imbe_vocoder — the codec used by real P25 Phase 1 radios.

The sender encodes mic audio to IMBE frames once; every listener (in-game, dispatch panel, 3D bystander) decodes them locally. Same codec artefacts for everyone, no re-encoding per listener, ~3.7× smaller on the wire than Opus.

Decoded audio passes through a small EQ (100 Hz HP → +3 dB @ 1.5 kHz → 4 kHz LP) before the analog FX chain (if enabled).

Notes:

  • First PTT of a session has a brief cold-start warm-up (self-resolves)
  • First ~10 ms of a transmission may clip — adjust pttReleaseDelay if severe
  • Works identically in the dispatch panel

Transmission Effects

Background sounds near the talker (sirens, helis, gunshots) broadcast over the channel.

SettingDefaultDescription
playTransmissionEffectstrueEnable background SFX during PTT
analogTransmissionEffectstrueRoute background SFX through the analog FX chain
  • Sirens — driven by Config.bgSirenCheck. Return true for the default loop, or a WAV filename for per-tone playback. Populate Config.sirenToneFiles to map tELS / LVC tone indices to custom WAVs (see above)
  • Helicopters — auto-triggered when the talker is in a heli with rotors spinning
  • Gunshots — picked up during a transmission, broadcast to listeners and 3D bystanders

Bonking

Behaviour when a second player keys up while someone is already transmitting.

SettingDefaultDescription
bonking.blockTransmissionfalseSuppress the second PTT. If false, both talkers transmit simultaneously
bonking.playBonkTonetruePlay a bonk tone to the blocked user
bonking.bonkToneGlobaltrueAlso play the bonk tone to everyone on the channel
bonking.doubleTapOverridetrueSecond PTT double-tapped within doubleTapWindow bypasses the block
bonking.doubleTapWindow1500Double-tap window (ms)
blockTransmissionFirst PTTSecond PTTSecond PTT (double-tap)
falseTransmitsTransmits + bonkTransmits
trueTransmitsBlocked + bonkTransmits (override)

As of v5.7, blockTransmission is enforced server-side in addition to the client check. Previously the denial was client-local only, which meant a modified client could bypass the block.

3D Audio

Broadcasts a player's radio output (voice, tones, sirens, heli, gunshots) in 3D space so nearby players hear it.

SettingDefaultDescription
enable3DAudiofalseMaster switch for the entire 3D system. Off → no player hears any 3D radio audio
default3DAudiofalseInitial per-player default. false = new players join with earbuds on (no 3D output), true = new players join with earbuds off (3D output active)
default3DVolume50Default 3D audio volume for new players (0-100)
vehicleRadio3DAudioEnabledfalseWhen on, a player's radio also plays from their vehicle once they walk away from it
vehicle3DActivationDistance3How far (meters) the owner must walk from the vehicle before the vehicle takes over as the 3D source

How 3D audio works

A player broadcasts in 3D while all of these are true: enable3DAudio = true, their Earbuds toggle is OFF, and they are transmitting, receiving, or playing a tone. Nearby players hear it spatialised within 100 m (person) or 200 m (vehicle) via Web Audio PannerNodes.

Earbuds only gates your outgoing 3D audio. You still hear others based on their earbud settings.

Vehicle radio (3D)

With vehicleRadio3DAudioEnabled = true, a player who enters a vehicle claims it as a 3D source. While they're inside, the player is the source; once they walk more than vehicle3DActivationDistance m away, the vehicle becomes the source at the longer 200 m range. The source hands back to the player when they return, and releases entirely when they disconnect.

One vehicle, one owner — another player can't claim it until the first releases.

Callsign System

SettingDefaultDescription
useCallsignSystemtrueEnable the built-in callsign system
callsignCommand"callsign"Chat command to set callsign (e.g., /callsign 2L-319)

Storage & Scope

Callsigns persist client-side in tLib KVP, namespaced by tlib_community_id (falling back to sv_projectName if unset). Two servers sharing the same community ID share callsigns across both; different IDs keep them isolated. The server-side cache is session-only — callsigns are re-sent by the client on connect.

Earbud Clothing Detection

Automatically enable earbuds mode when a player wears specific clothing items (hats, earpieces, accessories). Configured through the admin panel — no restart required.

Each entry in the earbudItems list defines a clothing slot and drawable ID to match:

FieldTypeDescription
slotIdnumberGTA V component/prop slot ID
slotTypestring"prop" for accessories/hats, "component" for clothing
drawableIdnumberThe drawable index to match (use the 3D Prop Editor or GTA ped tools to find IDs)

Behaviour:

  • Checked every 7 seconds via a polling thread
  • Only triggers on state change (wearing → not wearing or vice versa)
  • If the player manually toggles earbuds, the auto-detection respects that override until the clothing state changes again

GPS & Signal

SettingDefaultDescription
gpsBlipUpdateRate50GPS blip update interval (ms). Lower = smoother, higher CPU
blipsEnabledtrueMaster switch for all GPS blips
signalDegradationEnabledfalseAudio quality degrades with distance from signal towers
signalDegradationIntensity50Maximum severity of the degradation at lowest signal (0-100)
signalFalloffRate24Slope of signal drop-off with distance. Higher values = signal drops faster

How signal strength works

Client-side: distance to the nearest tower in Config.signalTowerCoordinates converts to a 1–5 bar reading using signalFalloffRate (higher = bars drop faster).

With signalDegradationEnabled, low bars also garble audio — clean at full bars, up to signalDegradationIntensity at zero bars.

Put towers at real comm-tower locations on your map; the demo coords are examples.

Emergency

SettingDefaultDescription
panicTimeout60000Milliseconds before panic auto-clears

Misc

SettingDefaultDescription
animationsEnabledtrueAllow PTT/focus animations to play
focusLayoutModefalseDefault state of the on-foot focus/unfocus split layout toggle

Default Layouts

Default radio model per context. Configurable via admin panel or /tradio > Set as Default Layout for Vehicle.

ContextDefault Model
HandheldATX-8000
HandheldUnfocusedATX-8000H
VehicleAFX-1500
BoatAFX-1500G
AirTXDF-9100

HandheldUnfocused is only used when the player enables Separate On-Foot Focus Layout in /tradio, giving the radio a different model when it is not focused.

Radio models are auto-discovered from client/radios/ subfolders.


Custom Tones

Tones are defined in client/radios/default/tones.json. Each key is the tone name (case-insensitive). Tones can be either oscillator sequences (synthesised in the browser) or WAV files dropped into sounds/.

Oscillator Tones

Synthesised on the fly by the Web Audio API:

client/radios/default/tones.json
{
  "BEEP": [{ "freq": 910, "duration": 250 }],
  "ALERT_A": [
    { "freq": 600, "duration": 150, "delay": 200 },
    { "freq": 600, "duration": 150 }
  ]
}
PropertyDescription
freqFrequency in Hz (20-20000)
durationLength in milliseconds
delayOptional pause before this step (ms)

Sequences play their steps in order; use delay to space them out.

WAV File Tones

Set a tone's value to an empty array [] to play a WAV file instead. The file name must match the tone key in lowercase, with the extension .wav.

client/radios/default/tones.json
{
  "ECHO": [],
  "PANIC": []
}

"ECHO": [] looks for echo.wav in the model's sounds/ folder (or client/radios/default/sounds/ if the model doesn't override it). Supported formats: .wav, .mp3, .ogg (.wav recommended for lowest latency).

WAV tones work everywhere oscillator tones do: in-game radio, 3D spatial audio, and the dispatch panel.

Standard Tone Catalog

Tommy's Radio plays specific tones at specific events. Overriding these in tones.json changes what users hear — you can customise or replace any of them.

PTT tones (heard by the talker on their own radio):

ToneWhen it plays
PTTKey up on a conventional channel
PTT_TRUNKEDKey up on a trunked channel
PTT_PATCHEDKey up on a channel that's part of an active frequency patch
PTT_ENDKey release on a conventional channel
PTT_END_TRUNKEDKey release on a trunked channel
PTT_END_PATCHEDKey release on a patched channel

TX tones (heard by receivers when someone else keys up):

ToneWhen it plays
TX_STARTAnother user starts transmitting on a conventional channel
TX_START_TRUNKEDAnother user starts transmitting on a trunked channel
TX_START_PATCHEDAnother user starts transmitting on a patched channel
TX_ENDAnother user stops transmitting (conventional)
TX_END_TRUNKEDAnother user stops transmitting (trunked)
TX_END_PATCHEDAnother user stops transmitting (patched)

The engine picks the correct variant based on channel type — so if you set a distinctive PTT_TRUNKED, users will hear it only when they key up on a trunked frequency. Leave a variant undefined (or keep it empty) to silence it for that context.

General-purpose tones used by alerts and dispatch:

ToneTypical use
BEEPGeneric alert beep
BONKTransmission collision (bonking.playBonkTone)
CHIRPShort acknowledgment blip
ECHOCustom "message received" tone
PANICPanic button activation
ALERT_A, ALERT_B, ALERT_CDefault alert-tone slots referenced by Config.alerts
ALERT_SIGNAL3Dedicated Signal 3 alert tone
PRIORITYDefault activate tone for priority alerts
PRIORITY_REPEATRepeat tone for persistent priority alerts

Any tone name can also be passed to playToneOnChannel / playToneOnSource exports or the /radio/dispatch/tone HTTP endpoint.

Background Transmission Sounds

The following WAV files in client/radios/default/sounds/ aren't played via tones.json — they're referenced directly by the transmission-effect system and can't be renamed:

FilePurpose
transStart.wav"Click-in" sound heard at the start of a received transmission
transMid.wavLooping background noise during a received transmission
transEnd.wav, transEnd1.wav, transEnd2.wav"Click-out" sounds at the end of a transmission — one is picked randomly
bgSiren.wavDefault siren loop when Config.bgSirenCheck returns true
bgHeli.wavHelicopter rotor loop during transmissions from a heli
bgShot.wavGunshot fallback sample
Custom tone WAVs (e.g. wail.wav, yelp.wav)Per-tone siren files referenced by Config.sirenToneFiles or a direct filename return from bgSirenCheck

Drop in replacements with the same filenames to customise. These files are served under /client/radios/default/sounds/ and also via the legacy /audio/ redirect.

Per-Model Overrides

Place a tones.json in client/radios/YOUR-MODEL/ to override individual tones for that model. Keys present take precedence; any key not defined falls back to client/radios/default/tones.json. WAV files override the same way — a sounds/echo.wav in the model folder takes priority over the default one.

tones.json
echo.wav
panic.wav
transStart.wav
...

This lets you give each radio model its own "personality" — a handheld can have a different PTT chirp than a mobile unit, for example.

On this page

Need help?

Ask on Discord