Choose a supported seam
| Goal | Supported seam | Context | Avoid |
|---|---|---|---|
| Play or control one positional sound | Sound native client exports | Client Lua | Internal Sound NUI callbacks. |
| Ask a specific player client to play audio | Sound target-first server exports | Server Lua | Unchecked or nil target values, which become broadcast -1. |
| Observe synchronized vehicle Radio state | GetVehicleRadioState | Server Lua | Radio internal mutation network events. |
| Consume a Dispatch channel | Dispatch getters and local transmission event | Server Lua | Direct access to scheduler internals. |
| Read Station Pack state | Pack getters and local change event | Server Lua | Calculating separate program offsets on clients. |
| Add an audio backend | Radio provider registry inside provider files | Radio source extension | Treating the registry as a cross-resource export. |
| Add PMA-style scanner voice | Radio voice provider registry/config | Radio provider files and server config | Joining scanner listeners to the talker's PMA channel. |
| Generate content ZIPs | Content Builder UI | Local desktop browser | Treating loopback HTTP routes as a stable SDK. |
Use client exports when the caller already runs on the player who should hear the sound. Use server relays only after the server validates recipients and input. A client entity handle has meaning only on that client; synchronize a network ID and resolve it independently.
A successful Sound server export confirms only that a client event was emitted. It cannot prove that the target exists, the URL loads, the codec is supported, or playback became audible. Use a client callback or receiving-client diagnostics when confirmation matters.
Supported public surfaces are the documented exports, local Dispatch/Station Pack events, and source-level Radio provider registries. Sound/Radio NUI callbacks, most Radio network events, and Sound transport events are internal implementation details.
Authority and data flow
| Owner | Authoritative for |
|---|---|
| san_andreas_radio | Vehicle eligibility, seat permissions, source/station, power, volume intent, Bluetooth transport, playlist advancement, accessory state, and scanner power. |
| san_andreas_dispatch | Active local clip, exact duration, revision, start time, offset, gaps, and public channel metadata. |
| san_andreas_station_pack | Station definitions, deterministic program order, active segment, exact timing, and local media ownership. |
| san_andreas_sound | Caller-requested rendering, relationship/acoustic processing, transport, diagnostics, and local cleanup. |
Player input and NUI
|
v
san_andreas_radio client
| validated requests
v
san_andreas_radio server <--- snapshots/events --- san_andreas_dispatch
| authoritative vehicle state
v
Radio client audio adapter
|
+--- san_andreas_sound provider (default)
+--- xSound provider or another registered backendThe server synchronizes media descriptors and timestamps rather than audio bytes. Clients load and render media locally. A vehicle network ID crosses the network; a local entity handle does not.
This separation prevents the renderer from deciding station order or permissions, lets Radio switch providers without rewriting lifecycle code, and allows other resources to use Sound independently.
Synchronization model
Radio's server validates the player, actual vehicle, seat, vehicle eligibility, source, station or URL, and rate limit before changing shared state. Only a validated driver publishes native GTA track metadata.
Seekable Station Pack tracks and local Dispatch clips have known durations and start times. A client calculates the current offset and seeks after media readiness. Bluetooth stores a base position, startedAt time, play state, and revision.
Most live streams cannot be meaningfully seeked. With SyncSatelliteStreams enabled, Radio provides a stable syncGroup. Sound can fan one matching live decode into separate vehicle spatial branches; providers may ignore the hint, in which case buffer offsets are possible.
Each client resolves entityNetId to its own handle. entityHandle is only a local optimization and must never cross a server boundary.
Every component cleans up only what it owns. A diagnostic or stop handler must never stop unrelated resources or issue broad native resets that can affect fuel, phone, lighting, or other vehicle systems.
| Source | Seek behavior | Shared identity |
|---|---|---|
| Local GTA Radio | GTA-owned | Synchronized station/power and validated track metadata. |
| Satellite live stream | Normally non-seekable | Matching URL plus syncGroup when supported. |
| Station Pack segment | Seekable by exact offset | Station snapshot revision and deterministic clock. |
| Bluetooth | Seekable when renderer supports it | Vehicle state, startedAt, base position, and revision. |
| Dispatch clip | Seekable by exact offset | One channel descriptor shared by every listener. |
Native Sound integration pattern
fx_version 'cerulean'
game 'gta5'
dependency 'san_andreas_sound'
client_script 'client.lua'local RESOURCE = GetCurrentResourceName()
local ownedSounds = {}
local function playVehicleAlert(vehicle)
local netId = NetworkGetNetworkIdFromEntity(vehicle)
local name = ('%s:alert:%d'):format(RESOURCE, netId)
ownedSounds[name] = true
return exports.san_andreas_sound:PlayPositional({
name = name,
url = 'https://cdn.example.com/authorized-alert.ogg',
volume = 0.35,
entityHandle = vehicle,
entityNetId = netId,
maxDistance = 55.0,
trunkAffectsEnclosure = true,
relationshipOverrides = {
inside = { volumeMultiplier = 0.65, maxDistance = 25.0 },
outside = { volumeMultiplier = 1.0, maxDistance = 55.0 }
},
loop = false,
destroyOnFinish = true,
onPlayStart = function(info)
print(('started %s'):format(info.id))
end
})
end
AddEventHandler('onClientResourceStop', function(resourceName)
if resourceName ~= RESOURCE then return end
for name in pairs(ownedSounds) do
exports.san_andreas_sound:Destroy(name)
end
end)- Use stable resource-prefixed sound names to prevent collisions and make cleanup ownership auditable.
- Supply both the local handle and network ID only when the caller already owns the client entity; the network ID is the shared contract.
- Relationship override profile names normalize camel/Pascal aliases. Each override can set volumeMultiplier from 0.0-2.0 and maxDistance of at least 0.1.
- PlayPositional returning true means local validation queued the request. Use onPlayStart for renderer confirmation.
- Destroy only names owned by the current resource when that same resource stops.
Target one validated client
RegisterNetEvent('my_resource:server:requestNotice', function(x, y, z)
local target = tonumber(source)
if not target or target <= 0 then return end
-- Validate authorization and coordinates for this resource here.
exports.san_andreas_sound:PlayUrlPos(
target,
('my_resource:notice:%d'):format(target),
'https://cdn.example.com/authorized-notice.ogg',
0.30,
{
x = tonumber(x) or 0.0,
y = tonumber(y) or 0.0,
z = tonumber(z) or 0.0
},
false,
{ maxDistance = 30.0, destroyOnFinish = true }
)
end)Server relays can serialize position, entityNetId, relationship options, and the documented playback fields. They cannot send callback functions or a client-local entity handle.
Consume Dispatch, Station Pack, and Radio state
local tx = exports.san_andreas_dispatch:GetChannel('police')
if tx and tx.offset < tx.duration then
print(tx.label, tx.url, tx.offset, tx.duration)
end
AddEventHandler(
'san_andreas_dispatch:server:transmissionChanged',
function(channelId, descriptor)
if channelId ~= 'police' then return end
-- Choose recipients and relay only required fields.
end
)for _, station in ipairs(
exports.san_andreas_station_pack:GetStations()
) do
print(station.id, station.label)
end
local snapshot = exports.san_andreas_station_pack:GetSnapshot()
local source = snapshot['SERVER_VINEWOOD_DEMO']
if source and source.track then
print(source.track.kind, source.track.artist, source.track.title, source.offset)
endlocal state = exports.san_andreas_radio:GetVehicleRadioState(vehicleNetId)
if not state then return end
local summary = {
powered = state.powered == true,
mode = state.mode,
station = state.station,
volume = state.volume,
revision = state.revision
}
if state.bluetooth then
local position = state.bluetooth.position or 0.0
if state.bluetooth.playing then
position = position + math.max(0, os.time() - state.bluetooth.startedAt)
end
summary.bluetoothPosition = position
end- Dispatch's local event marks transmission start only. offset >= duration means the channel is in its synchronized silent gap.
- Station Pack getters return safe copied definitions and current program sources.
- Radio returns a raw internal state table. Copy required fields immediately; do not mutate or retain it and do not call internal player-authenticated events as setters.
- If a server-owned feature needs a Radio mutation, add a validated public server export to Radio and document its authority rules rather than bypassing validation.
Add a Radio audio provider
Radio core calls SanAndreasRadioAudio only. Provider-specific normalization belongs in client/audio/providers and server/audio/providers, loaded before Radio core scripts.
Client provider interface
registerProvider
functionClientRegister the provider table under the configured lowercase provider name.
SanAndreasRadioAudio.registerProvider(name, provider)isAvailable
functionClientReport whether the selected provider can be used.
isAvailable()isProviderResource
functionClientIdentify whether a resource lifecycle event belongs to this provider.
isProviderResource(resourceName)playPositional
functionClientReplace a same-name sound and apply its initial distance atomically.
playPositional(options)- Call options.onPlayEnd only after non-looping natural completion.
destroy
functionClientRemove one provider-owned sound.
destroy(name)setPosition
functionClientMove a provider sound.
setPosition(name, position)setDistance
functionClientUpdate its audible radius.
setDistance(name, distance)setVolume
functionClientUpdate provider volume intent.
setVolume(name, volume)reset
functionClientOptional provider cleanup hook.
reset()setTime
functionClientOptional seek operation. Return exact false when unsupported.
setTime(name, position)setPlaying
functionClientOptional transport operation. Return exact false when unsupported.
setPlaying(name, playing)setSpectrumTarget
functionClientOptional spectrum callback registration. Return exact false when unsupported.
setSpectrumTarget(name, callback, slot)| Field | Type and effect |
|---|---|
| supportsRelationshipOverrides | Boolean. When true, Radio delegates relationship shaping to the provider instead of applying it itself. |
Server provider interface
registerProvider
functionServerRegister the matching server provider table under the same name used on the client.
SanAndreasRadioAudio.registerProvider(name, provider)isAvailable
functionServerReport provider readiness to the Radio server adapter.
isAvailable()isProviderResource
functionServerIdentify provider lifecycle ownership.
isProviderResource(resourceName)- Audit which Radio adapter operations the backend genuinely supports.
- Create matching client and server provider files and register the same provider name on both sides.
- Normalize provider-specific options inside the adapter without changing Radio station or lifecycle code.
- Load provider files before core scripts, map the provider resource in Config.Audio.Resources, and select Config.Audio.Provider.
- Test same-name replacement, initial distance, seek, pause/resume, natural completion, destroy, resource restart, vehicle deletion, and two-client synchronization.
- Document unsupported optional operations and return false rather than pretending success.
Extend the live scanner voice provider
Client voice provider
isAvailable
functionClientReport client provider availability.
isProviderResource
functionClientIdentify provider resource lifecycle events.
addRecipients
functionClientApply the receive-only recipient routing selected by Radio.
setTalkerActive
functionClientUpdate one talker's active voice path.
reset
functionClientOptional provider cleanup.
Server voice provider
isAvailable
functionServerReport provider availability.
isProviderResource
functionServerIdentify lifecycle ownership.
getPlayerRadioChannel
functionServerRead the player's current radio channel for mapped talker and duplicate-path checks.
getPlayerCallChannel
functionServerRead the call channel for duplicate-path avoidance.
isPlayerInRadioChannel
functionServerTest whether a listener already shares the talker channel.
The voice registry is an internal Radio source-extension seam, not a public cross-resource export. Preserve receive-only routing: listeners must not be joined to or transmit on the monitored channel.
Public API index by component
| Component | Public surface | Canonical detail |
|---|---|---|
| Sound client | PlayPositional, PlayUrlPos, PlayUrl, destroy/mutations, transport, spectrum, readiness, diagnostics, and streamer-mode exports plus three integration events. | Sound component page: Native client API and callbacks. |
| Sound server | Target-first play, destroy, position, entity network ID, distance, volume, timestamp, playing, pause, and resume relays. | Sound component page: Server relay API. |
| xSound compatibility | Playback, mutations, information getters, callbacks, fades, state event, and completion event on the documented tested surface. | Sound component page: Legacy compatibility. |
| InteractSound compatibility | CL/SV PlayOnOne, PlayOnAll, PlayWithinDistance variants, PlayOneShot, and StopOneShot. | Sound component page: Legacy compatibility and security warning. |
| Dispatch | GetSnapshot, GetChannel, GetCatalogCounts, and local transmissionChanged event. | Dispatch component page: Public server API. |
| Station Pack | GetStations, GetSnapshot, GetStation, and local stationPackChanged event. | Station Pack component page: Public server API. |
| Radio | GetVehicleRadioState only. | Radio component page: Public cross-resource contract. |
Security, privacy, and operations
- Keep Bluetooth HTTP and direct URLs disabled unless the server has an explicit trusted requirement. Restrict hosts and length, reject credentials and control characters, and revalidate everything on the server.
- Retain Radio state-change and metadata rate limits and test repeated rejected requests as well as normal use.
- Before replacing Dispatch media, listen to every file and document permission, privacy review, source, review date, license, and a recoverable staging plan.
- Distinguish private server playback, rebroadcasting, inclusion in a downloadable resource, and public redistribution; permission for one does not imply the others.
- Treat successful Builder validation as a packaging result, never proof of ownership, consent, privacy compliance, or runtime playback.
- Use at least two clients and two vehicles on a staging server.
- Exercise all direct, fallback, native, server-hosted, Bluetooth, scanner, and optional voice paths.
- Move between world/inside/outside/other-vehicle profiles, doors/windows/trunk, boats, and occlusion while inspecting diagnostics.
- Stop and start each component independently and confirm no unrelated resource is stopped and no audio remains orphaned.
- Measure client/server frame time, bandwidth, NUI memory, active sound count, renderer mode, and errors in an area with representative nearby vehicles.
A stream works but is not spatial.
Likely causes
- CORS forced HTML fallback
- The source is YouTube
Checks
- Inspect renderer and source permission
- Use an authorized direct Web Audio endpoint
Expected result
Compatible direct sources report webaudio; fallback limitations remain explicit.
Nearby vehicles drift on one Satellite station.
Likely causes
- Provider ignored syncGroup
- URL/group differs
- HTML fallback
Checks
- Confirm SyncSatelliteStreams, identical URL/group, and renderer
Expected result
Compatible providers share one live transport with separate spatial branches.
Dispatch or Bluetooth begins too loudly or advances incorrectly.
Likely causes
- Startup fade removed
- Initial provider gain applied too early
- onPlayEnd called on replacement
Checks
- Restore ScannerStartupFadeMs
- Review provider atomic start and natural-completion semantics
Expected result
Dispatch fades after seek; Bluetooth advances exactly once after natural completion.
A diagnostic experiment causes skipping or duplicates.
Likely causes
- Temporary owned sounds remain
- The stack restarted mid-experiment
Checks
- Destroy only experiment names
- Remove the test resource
- Restart only affected Suite components
Expected result
Normal playback resumes without broad resets or unrelated resource impact.
Terminology and source paths
| Term | Meaning in the Suite |
|---|---|
| CEF | FiveM's embedded Chromium browser used by NUI and media. |
| CORS | Browser permission that determines whether a media source exposes samples to Web Audio. |
| Descriptor | Synchronized URL, start time, duration, revision, offset, and public metadata. |
| Entity handle | Client-local GTA entity reference; never relay between clients. |
| Entity network ID | Network-safe identifier resolved independently on each client. |
| HRTF | Headphone-oriented directional 3D panning. |
| Occlusion | Additional gain/filter shaping when geometry blocks a source. |
| Revision | Changing value used to reject stale or duplicate state. |
| syncGroup | Provider hint that matching non-seekable live media can share one client connection. |
| Task | Repository path |
|---|---|
| Sound configuration | san_andreas_sound/config.lua |
| Sound native client API | san_andreas_sound/client/main.lua |
| Sound legacy bridges | san_andreas_sound/client/compat and server/compat |
| Dispatch metadata | san_andreas_dispatch/config.lua |
| Dispatch catalog/media | san_andreas_dispatch/shared/catalog.lua and audio/<channel> |
| Radio configuration | san_andreas_radio/config.lua |
| Scanner contract | san_andreas_radio/shared/scanners.lua |
| Radio provider adapters | san_andreas_radio/client/audio and server/audio |
| Radio artwork | san_andreas_radio/web/assets/stations |
| Station Pack media/catalog | san_andreas_station_pack/audio, artwork, and config.lua |