Client Exports¶
Call these from any client script.
play3d¶
Plays a sound that only the local player hears. No network traffic, no server state.
exports['3d-audio']:play3d(
soundId, soundFile, x, y, z, range, volume, loop,
dimension, entityNetId
)
You allocate the sound ID, and it must be negative
Unlike the server export, this one does not return an ID — you pass one in. It must be negative: positive IDs belong to the server's namespace and reusing one makes the two sounds collide on the same slot.
Keep a counter:
Parameters¶
| # | Name | Type | Default | Description |
|---|---|---|---|---|
| 1 | soundId |
number | required | Negative ID you allocate yourself. |
| 2 | soundFile |
string | required | Path relative to soundPath (assets/sounds by default). |
| 3 | x |
number | required | World X. |
| 4 | y |
number | required | World Y. |
| 5 | z |
number | required | World Z. |
| 6 | range |
number | defaults.range |
Audible radius in metres. Clamped to limits.maxRange. |
| 7 | volume |
number | defaults.volume |
0.0–1.0. Clamped to limits.maxVolume. |
| 8 | loop |
boolean | defaults.loop |
Whether to loop. There is no loopDurationSeconds here — use your own timer. |
| 9 | dimension |
number | defaults.dimension (-1) |
Only play if the local routing bucket matches. -1 = always. |
| 10 | entityNetId |
number | 0 |
If > 0, follow this entity's position with pause/resume on streaming range. |
Returns¶
Nothing.
Examples¶
Knowing when a sound finished¶
When a client-local (negative ID) one-shot finishes, the resource fires a local event with that ID:
AddEventHandler('3d-audio:ended', function(soundId)
print(('sound %d finished'):format(soundId))
end)
This only fires for non-looping sounds with negative IDs. Looping sounds never fire it, and server-initiated sounds report to the server instead.
stop¶
Stops a client-local sound.
Parameters¶
| # | Name | Type | Description |
|---|---|---|---|
| 1 | soundId |
number | The negative ID you passed to play3d. |
Returns¶
Nothing.
One argument, not two
The server's stop takes (target, soundId). This one takes just
(soundId). Passing two arguments here means the second is ignored and the
first is treated as the sound ID — which, if you copied a server call, is
your target value and almost certainly the wrong sound.
setDimension¶
Tells 3d-audio that the local player's routing bucket changed.
Parameters¶
| # | Name | Type | Description |
|---|---|---|---|
| 1 | bucket |
number | The routing bucket the local player is now in. |
Returns¶
Nothing.
Why this exists¶
This is the only thing 3d-audio needs from your framework. FiveM exposes no
client-side native that reads the local player's routing bucket — the server knows
it, the client does not. So the resource cannot detect bucket changes on its own
and has to be told.
You only need this if you use routing buckets. If every sound you play uses
dimension = -1 (the default), you can ignore this export entirely.
What it does¶
- Stops and forgets every tracked sound whose
dimensionis not-1, and cancels their entity-tracking ticks. Sounds withdimension = -1keep playing. - Records the new bucket, so subsequent dimension-scoped sounds are filtered against it.
- Notifies the server, which re-sends every registered sound belonging to the new bucket — so you hear an alarm that was already ringing when you arrived.
Where to call it¶
Wherever your code changes a player's bucket. Server-side you call
SetPlayerRoutingBucket; mirror that to the client:
-- server
SetPlayerRoutingBucket(src, bucket)
TriggerClientEvent('myframework:bucketChanged', src, bucket)
-- client
RegisterNetEvent('myframework:bucketChanged', function(bucket)
exports['3d-audio']:setDimension(bucket)
end)
The client assumes bucket 0 until you say otherwise
There is no handshake on join: the client starts with currentDimension = 0.
A player who spawns directly into a non-zero bucket silently drops
dimension-scoped sounds until something calls setDimension. Call it once on
spawn as well as on every change. See
Known Limitations.
Calling it with the current value does nothing
setDimension returns immediately if the bucket has not actually changed, so
calling it defensively on a loop is harmless — but it also means you cannot
use it to force a re-sync.