- TypeScript 64.5%
- JavaScript 19.8%
- Vue 13.9%
- Lua 1.1%
- HTML 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The check spawned `node node_modules/esbuild/bin/esbuild`. That path is a JavaScript shim on Windows but the native binary itself on Linux and macOS, so the check passed locally and failed on the CI runner with "SyntaxError: Invalid or unexpected token" on an ELF header. Replaced with esbuild.buildSync() from the Node API, which resolves the platform binary itself. This is the pattern build/build-bundle.js already used; the check should have followed it rather than shelling out. Also runs the typecheck before the sync check. Both bundle the same modules, and tsc gives a clear message where esbuild would only report a bundling failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
| .github | ||
| assets/sounds | ||
| build | ||
| docs | ||
| src | ||
| web | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| config.json | ||
| CONTRIBUTING.md | ||
| fxmanifest.lua | ||
| LICENSE | ||
| mkdocs.yml | ||
| NOTICE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| requirements-docs.txt | ||
| SECURITY.md | ||
| tsconfig.json | ||
3D Audio Subsystem
Play arbitrary audio files at world coordinates in FiveM, with real positional audio.
GTA V can only play sounds the game already ships. 3d-audio adds the missing
primitive: play this file, at these coordinates, audible for this many metres.
Any resource can call it, and every nearby player hears the sound from the right
direction, at the right volume, falling to silence at the range you choose.
The game scripts track positions; the actual mixing happens in an invisible NUI page driven by Howler.js over the Web Audio API. That gives real HRTF spatialization — turn your head in game and the sound moves around you — rather than simple left/right panning.
- No framework dependency. No ESX, no QBCore, no ox_lib. There is exactly one optional integration point, and only if you use routing buckets.
- Entity-attached sounds that follow a vehicle or ped and pause when it leaves streaming range.
- Routing bucket support with re-sync, so arriving late still means hearing the alarm that is already ringing.
- Configurable attenuation curve, tick rate, defaults and limits.
Installation
Important
The resource folder must be named
3d-audio. In FiveM the resource name is the folder name, so a differently named folder makes everyexports['3d-audio']call fail to resolve.
- Download the latest
3d-audio-vX.Y.Z.zipfrom Releases. - Extract it into your server's
resources/directory — the archive already contains a correctly named3d-audio/folder. - Add to
server.cfg:ensure 3d-audio
Note
A plain
git cloneis not runnable:fxmanifest.luapoints atdist/, which is generated and not committed. Use a release, or build from source.
Full guide: Installation
FiveM exports
Five exports — two on the server, three on the client.
Server
-- play3d(target, soundFile, x, y, z, range, volume, loop,
-- loopDurationSeconds, dimension, entityNetId) -> soundId
--
-- target = player server-id, or -1 for all clients
-- returns a POSITIVE sound id; keep it if you want to stop the sound
-- a one-shot everyone nearby hears
exports['3d-audio']:play3d(-1, 'vehicle/car_locking.wav',
x, y, z, 15.0, 0.8, false)
-- a looping alarm that stops itself after 3 minutes
local soundId = exports['3d-audio']:play3d(-1, 'alarm/vehiclealarm.mp3',
x, y, z, 150.0, 0.6, true, 180)
-- an alarm attached to a vehicle: follows it, pauses out of streaming range
local netId = NetworkGetNetworkIdFromEntity(vehicle)
exports['3d-audio']:play3d(-1, 'alarm/vehiclealarm.mp3',
0.0, 0.0, 0.0, 150.0, 0.6, true, 180, -1, netId)
-- stop(target, soundId)
exports['3d-audio']:stop(-1, soundId)
Client
-- play3d(soundId, soundFile, x, y, z, range, volume, loop,
-- dimension, entityNetId) -> void
--
-- YOU allocate soundId, and it must be NEGATIVE (positive ids are the server's)
local soundId = -1001
exports['3d-audio']:play3d(soundId, 'vehicle/car_unlocking.wav',
x, y, z, 20.0, 1.0, false)
-- stop(soundId) -- note: one argument, unlike the server's two
exports['3d-audio']:stop(soundId)
-- tell the resource when the local routing bucket changes
exports['3d-audio']:setDimension(bucket)
-- fired when a client-local one-shot finishes
AddEventHandler('3d-audio:ended', function(id) end)
JavaScript / TypeScript
const soundId: number = exports['3d-audio'].play3d(-1, 'alarm/vehiclealarm.mp3',
x, y, z, 150.0, 0.6, true, 180)
exports['3d-audio'].stop(-1, soundId)
Parameters
| Parameter | Type | Default | Meaning |
|---|---|---|---|
target (server only) |
number | — | Player server-id, or -1 for all clients |
soundId (client only) |
number | — | Caller-allocated, must be negative |
soundFile |
string | — | Path relative to assets/sounds/, e.g. alarm/vehiclealarm.mp3 |
x, y, z |
number | — | World position. Ignored when entityNetId > 0 |
range |
number | 50 |
Audible radius in metres; volume hits zero here |
volume |
number | 1.0 |
0.0–1.0 |
loop |
boolean | false |
Whether to loop |
loopDurationSeconds (server only) |
number | 0 |
Auto-stop after N seconds. 0 = forever — you must call stop |
dimension |
number | -1 |
Routing bucket filter; -1 = all buckets |
entityNetId |
number | 0 |
If > 0, the sound follows this entity |
Warning
The server and client
play3dsignatures are not interchangeable: the server's first argument istarget, the client's issoundId, andstoptakes two arguments on the server but one on the client. This is the most common source of integration bugs.
Complete reference with per-export detail: API Reference
Configuration
All settings live in config.json. Every default reproduces the previously
hardcoded value, so deleting the file changes nothing.
{
"debug": false,
"tickIntervalMs": 50,
"soundPath": "assets/sounds",
"defaults": { "range": 50, "volume": 1.0, "loop": false, "dimension": -1 },
"limits": { "maxVolume": 1.0, "maxRange": 1000, "maxConcurrentSounds": 64 },
"panner": {
"panningModel": "HRTF",
"distanceModel": "linear",
"refDistance": 1,
"rolloffFactor": 1
},
"security": { "verifyRoutingBucket": false }
}
Set "debug": true and restart the resource when diagnosing anything — it is the
first step in every troubleshooting path.
Every key explained: Configuration
Adding your own sounds
Drop files into assets/sounds/ and reference them by relative path. Anything
Chromium can decode works (MP3, OGG, WAV, …). Prefer mono for positional sounds —
a stereo file carries its own left/right image that fights the spatialization.
Build from source
Requires Node.js 22. Two npm projects: the game scripts (esbuild) and the NUI (Vite).
npm run install:all # npm ci in both projects
npm run build # typecheck, config check, esbuild, vite
npm run package # stage a runnable resource into package/3d-audio/
Known limitations
This resource has documented rough edges — routing buckets are not a privacy boundary, endless loops must be stopped explicitly or they leak registry entries, and there is no routing bucket handshake on join. All of them are written up honestly, with mitigations, before you hit them in production:
License
Caution
The example sound files under
assets/sounds/are not covered by the Apache-2.0 license and their provenance is unverified. Treat them as local test fixtures, not as something to ship or redistribute. See Sound Files.
Bundles Howler.js (MIT) into the NUI build — see NOTICE.
Build by Northdegree, Julscha