Configuration
Complete configuration reference for Tommy's Radio — config.lua, admin panel, zones, channels, alerts, and tones.
Configuration Overview
Two sources:
| Source | Contains | Edited via |
|---|---|---|
config.lua | Network, keybinds, zones/channels, alerts, callbacks | Text editor (requires restart) |
data.json | Audio, FX, bonking, callsigns, 3D, GPS rate, default layouts | In-game admin panel (live) |
data.json changes broadcast to clients immediately — no restart.
config.lua Reference
Network & Authentication
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 = {
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.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.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.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
endClient Callbacks
talkCheck
Runs before each PTT transmission. Return false to block.
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
endbgSirenCheck
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:
| Order | Provider | Detection | Tone info? |
|---|---|---|---|
| 1 | tELS | exports['tELS']:IsSirenActive(vehicle) — authoritative. nil return = not a tELS vehicle, fall through | Yes (via tels:sirenTone / tels:auxSirenTone state bags) |
| 2 | LVC | Cached state_lxsiren[vehicle] tone ID from lvc:UpdateThirdParty | Yes (tone ID) |
| 3 | JLS | exports['Jace3483_JLS']:IsSirenActive(vehicle) — authoritative. nil = not JLS, fall through | No (bool only) |
| 4 | Native GTA | IsVehicleSirenOn(vehicle) | No |
Return values:
| Return | Behavior |
|---|---|
false / nil | No siren audio |
true | Default 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.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
-- 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)
endFor 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:
# 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 allowIf 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.815 → 154815).
| ACE Permission | Grants |
|---|---|
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) |
# 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 allowAdmin 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.
add_ace group.admin tlib.admin allowCombining 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
- Radio access —
Config.radioAccessCheck(playerId)is the master gate.false= no radio at all. - NAC ID —
Config.getUserNacId(serverId)first. If it returnsnil/empty, fall back totRadio.nac.*ACE. Neither = no NAC ID. - Zone visibility — NAC ID in
Zone.nacIdsortRadio.zone.{N}ACE. - Connect + transmit — NAC ID in
Channel.allowedNacsortRadio.connect.{freq}ACE. - Scan — connect access, or NAC ID in
Channel.scanAllowedNacs, ortRadio.scan.{freq}ACE. - GPS visibility — NAC ID in
gps.visibleToNacsortRadio.gps.{freq}ACE. - Admin controls — SGN + trunked CT/TK buttons require
tlib.adminACE.
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.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.
{
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.
{
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
| Field | Type | Description |
|---|---|---|
name | string | Display name on the radio |
type | string | "conventional" or "trunked" |
frequency | number | Frequency in MHz (must be unique across all zones) |
frequencyRange | table | Trunked only — { min, max } range |
coverage | number | Trunked only — radius in meters |
allowedNacs | table | NAC IDs that can connect and scan |
scanAllowedNacs | table | NAC IDs that can scan only (not connect) |
gps.color | number | Blip color ID |
gps.visibleToNacs | table | NAC 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:
- Activate — fires once on trigger. Banner shows on every radio connected or scanning the channel;
tones.activateplays. - Repeat —
isPersistent = trueonly.tones.repeatfires everyrepeatIntervalms (default: random 5–10 s). Banner flashes again unlessrepeatShowBanner = false. Each channel has its own timer. - Deactivate — fires once when cleared.
tones.deactivateplays; greenRESUMEbanner shows (overridable viadeactivateLabel/deactivateColor).
One-shot alerts (isPersistent = false) run the activate phase only.
Tone Styles
Simple — one tone for all phases:
{
name = "SIGNAL 3",
color = "#0049d1",
isPersistent = true,
tone = "ALERT_A",
}Per-phase — independent tones for activate, repeat, and deactivate:
{
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
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Display text on the radio |
color | string | — | Hex colour of the alert banner |
isPersistent | bool | false | Stays active until manually cleared |
tone | string | — | Fallback tone for all phases |
tones.activate | string | — | Tone played once on activation |
tones["repeat"] | string | — | Tone played on each repeat loop |
tones.deactivate | string | — | Tone played once on clearance |
repeatInterval | number (ms) | random 5-10s | Gap between repeat fires |
repeatShowBanner | bool | true | Flash the banner on each repeat |
deactivateLabel | string | "RESUME" | Banner text shown on clearance |
deactivateColor | string | "#126300" | Colour of the clearance banner |
toneOnly | bool | false | Play tone only, no visual banner |
Examples
A mix of persistent and one-shot alerts covering common dispatch needs:
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 matchesdispatchNacIdor who havetlib.adminACE. - Dispatch panel — picks any alert, fires on one or all channels.
- Exports / HTTP —
exports['radio']:setAlertOnChannel(freq, true, "Signal 100")orPOST /radio/dispatch/alert/trigger. Takes the alert'sname(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:
add_ace identifier.steam:STEAM_ID tlib.admin allowOr grant to a group:
add_ace group.admin tlib.admin allow
add_principal identifier.steam:STEAM_ID group.adminOpen via the /tradio command > Admin Settings.
Audio Settings
| Setting | Default | Description |
|---|---|---|
voiceVolume | 65 | Default voice volume (0-100) |
sfxVolume | 35 | Default SFX/tone volume (0-100) |
volumeStep | 5 | Volume adjustment increment |
pttReleaseDelay | 350 | Milliseconds to keep transmitting after releasing PTT |
pttTriggersProximity | true | PTT 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 → outputP25 IMBE vocoder (p25Enabled) — real P25 codec. See P25 / IMBE Vocoder below.
| Setting | Default | What it does |
|---|---|---|
radioFx.fxEnabled | true | Master switch for the analog chain |
radioFx.p25Enabled | false | Master switch for the IMBE vocoder |
radioFx.inputGain | 1.65 | Pre-chain gain. Higher = louder into the chain, more likely to saturate |
radioFx.highpassFrequency | 250 | Rolls off bass below this cutoff (Hz). Raise to thin the voice |
radioFx.lowpassFrequency | 3500 | Rolls off treble above this cutoff (Hz). Lower = more muffled / telephone-like |
radioFx.compression | 60 | Dynamic range compression. High values flatten peaks, add "punch" but can pump |
radioFx.distortion | 30 | Tube-saturation drive. Adds harmonic grit; too high = crackle |
radioFx.distortionMode | "classic" | Saturation curve. "classic" = old-radio hard-clip; "tube" = smooth tube saturation |
radioFx.midBoost | 2 | Mid-frequency EQ boost (dB) around speech presence range |
Tuning guidelines
| Problem | Fix |
|---|---|
| Too muffled / telephone-like | Raise lowpassFrequency (4000–5000) or lower compression |
| Too tinny / harsh | Lower highpassFrequency (150–200) or reduce midBoost |
| Voice pumping | Lower compression |
| Crackling / distorted | Lower inputGain (0.8–1.2) or distortion |
| Clean radio sound | distortion = 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
pttReleaseDelayif severe - Works identically in the dispatch panel
Transmission Effects
Background sounds near the talker (sirens, helis, gunshots) broadcast over the channel.
| Setting | Default | Description |
|---|---|---|
playTransmissionEffects | true | Enable background SFX during PTT |
analogTransmissionEffects | true | Route background SFX through the analog FX chain |
- Sirens — driven by
Config.bgSirenCheck. Returntruefor the default loop, or a WAV filename for per-tone playback. PopulateConfig.sirenToneFilesto 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.
| Setting | Default | Description |
|---|---|---|
bonking.blockTransmission | false | Suppress the second PTT. If false, both talkers transmit simultaneously |
bonking.playBonkTone | true | Play a bonk tone to the blocked user |
bonking.bonkToneGlobal | true | Also play the bonk tone to everyone on the channel |
bonking.doubleTapOverride | true | Second PTT double-tapped within doubleTapWindow bypasses the block |
bonking.doubleTapWindow | 1500 | Double-tap window (ms) |
blockTransmission | First PTT | Second PTT | Second PTT (double-tap) |
|---|---|---|---|
| false | Transmits | Transmits + bonk | Transmits |
| true | Transmits | Blocked + bonk | Transmits (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.
| Setting | Default | Description |
|---|---|---|
enable3DAudio | false | Master switch for the entire 3D system. Off → no player hears any 3D radio audio |
default3DAudio | false | Initial per-player default. false = new players join with earbuds on (no 3D output), true = new players join with earbuds off (3D output active) |
default3DVolume | 50 | Default 3D audio volume for new players (0-100) |
vehicleRadio3DAudioEnabled | false | When on, a player's radio also plays from their vehicle once they walk away from it |
vehicle3DActivationDistance | 3 | How 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
| Setting | Default | Description |
|---|---|---|
useCallsignSystem | true | Enable 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:
| Field | Type | Description |
|---|---|---|
slotId | number | GTA V component/prop slot ID |
slotType | string | "prop" for accessories/hats, "component" for clothing |
drawableId | number | The 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
| Setting | Default | Description |
|---|---|---|
gpsBlipUpdateRate | 50 | GPS blip update interval (ms). Lower = smoother, higher CPU |
blipsEnabled | true | Master switch for all GPS blips |
signalDegradationEnabled | false | Audio quality degrades with distance from signal towers |
signalDegradationIntensity | 50 | Maximum severity of the degradation at lowest signal (0-100) |
signalFalloffRate | 24 | Slope 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
| Setting | Default | Description |
|---|---|---|
panicTimeout | 60000 | Milliseconds before panic auto-clears |
Misc
| Setting | Default | Description |
|---|---|---|
animationsEnabled | true | Allow PTT/focus animations to play |
focusLayoutMode | false | Default 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.
| Context | Default Model |
|---|---|
| Handheld | ATX-8000 |
| HandheldUnfocused | ATX-8000H |
| Vehicle | AFX-1500 |
| Boat | AFX-1500G |
| Air | TXDF-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:
{
"BEEP": [{ "freq": 910, "duration": 250 }],
"ALERT_A": [
{ "freq": 600, "duration": 150, "delay": 200 },
{ "freq": 600, "duration": 150 }
]
}| Property | Description |
|---|---|
freq | Frequency in Hz (20-20000) |
duration | Length in milliseconds |
delay | Optional 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.
{
"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):
| Tone | When it plays |
|---|---|
PTT | Key up on a conventional channel |
PTT_TRUNKED | Key up on a trunked channel |
PTT_PATCHED | Key up on a channel that's part of an active frequency patch |
PTT_END | Key release on a conventional channel |
PTT_END_TRUNKED | Key release on a trunked channel |
PTT_END_PATCHED | Key release on a patched channel |
TX tones (heard by receivers when someone else keys up):
| Tone | When it plays |
|---|---|
TX_START | Another user starts transmitting on a conventional channel |
TX_START_TRUNKED | Another user starts transmitting on a trunked channel |
TX_START_PATCHED | Another user starts transmitting on a patched channel |
TX_END | Another user stops transmitting (conventional) |
TX_END_TRUNKED | Another user stops transmitting (trunked) |
TX_END_PATCHED | Another 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:
| Tone | Typical use |
|---|---|
BEEP | Generic alert beep |
BONK | Transmission collision (bonking.playBonkTone) |
CHIRP | Short acknowledgment blip |
ECHO | Custom "message received" tone |
PANIC | Panic button activation |
ALERT_A, ALERT_B, ALERT_C | Default alert-tone slots referenced by Config.alerts |
ALERT_SIGNAL3 | Dedicated Signal 3 alert tone |
PRIORITY | Default activate tone for priority alerts |
PRIORITY_REPEAT | Repeat 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:
| File | Purpose |
|---|---|
transStart.wav | "Click-in" sound heard at the start of a received transmission |
transMid.wav | Looping 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.wav | Default siren loop when Config.bgSirenCheck returns true |
bgHeli.wav | Helicopter rotor loop during transmissions from a heli |
bgShot.wav | Gunshot 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.
This lets you give each radio model its own "personality" — a handheld can have a different PTT chirp than a mobile unit, for example.
