What features does the speaker script offer? What is it?
📻 Music Everywhere!
Play songs anywhere, in your car or with boomboxes!
🎧 Spatial (3D) Audio
Players experience sounds from their actual locations, with a sense of distance and direction.
🎯 Audio Effects
Muffled Sound: Songs sound muffled and distant when outside the vehicle, creating a realistic outdoor feel.
Realistic Echo: Dynamic reverb is applied to corridors, rooms, and enclosed spaces to create an echo effect appropriate for the space.
Interior Muffle: Speakers or boomboxes inside buildings and structures sound muffled and distant from outside, creating a realistic outdoor feeling.
🔊 Boomboxes
Enjoy yourself with 6 different Boombox models! (4 Portable, 2 Stationary)
Place the Boomboxwherever you like
Carry your boomboxes in your hands or on your back!
🪄 User-Friendly Menu (UI - User Interface)
Manage your music enjoyment with a compact and simple interface!
View and play your recently played songs!Save your favorite songs and play them whenever you want!
🔗 YouTube Integration - Song Upload Speed
Play any song you want on YouTube! (Using the video ID)
All YouTube operations are performed entirely by another server, so it does not cause issues like NUI (CEF) crashes!
The loading speed for a song that has not been played before varies between 1 and 3 seconds. (This time was tested with a song with a maximum length of 5 minutes.)
Clients load songs in 1ms to 7ms, providing very fast performance.
⚙️ Performance
NUI (CEF): The JavaScript Instance allocates an average of 100KB of RAM for a new song. Thanks to the Garbage Collector and optimizations, usage decreases over time.
Client Side: 0.00 when idle, but varies between 0.00 and 0.01ms when 1 speaker/boombox is rendered.
Server Side: 0.00 during idle time, but varies between 0.00 and 0.03ms during a new song loading process. Under load, it performs at an average of 0.00 to 0.05ms.
♻️ Sync & OneSync Compatibility
Music is synchronized among all players, so everyone hears the same song at the same time!
Music synchronization is maintained in cases such as reconnecting to the server, dying, or changing regions!
🚫 Blacklisted locations - Vehicles
Prevent music from playing in specific regions and vehicles! (You can add specific coordinates and vehicle models and claases)
📑Config File
ConfigServer Config
🚩Supporting 7 Languages / You can add more
Currently, ar, cz, ro, it, fr, de, and tr language support is available. You can add more from the locale files.
Config = {}
Config.Framework = {
["Framework"] = "auto", -- [[LEAVE AUTO]] auto, esx, qbcore or qbox
["ResourceName"] = "auto", -- [[LEAVE AUTO]] auto, es_extended or qb-core or your resource name. If you using qbx you should write qb-core
["SharedEvent"] = "" -- Event name for old cores.
}
Config.Language = "en" -- ar, cz, ro, it, fr, de, tr
Config.ShowDebug = false -- Show debug messages
Config.Reverb = true -- Enable reverb/echo effects based on ceiling height. If `true`, it will increase the use of resmon. While there is a more pronounced reverb in low ceilings, there is no reverb in open areas, resulting in a cleaner sound.
Config.RateLimit = 2.5 -- Time (in seconds) a player must wait before playing another sound.
Config.ActionCooldown = 0.05 -- Time (in seconds) a player must wait before performing another action (like change seek, distance, volume etc.)
Config.DisableGTARadio = true -- It prevents the GTA5 radio menu from opening in vehicles. This way, you cannot turn on your car radio while the speaker is working, preventing sound confusion.
Config.SpatialUpdateRate = 200 -- How often (in ms) to update spatial audio positions and volumes (lower values may increase CPU usage but provide smoother audio transitions)
Config.MaxCeilingDistance = 25.0 -- The maximum ceiling height to be checked in the area where the speaker or car is located. This allows echo/reverb sounds to be added.
Config.DeleteBoomboxWhenOwnerQuit = false --[[
⚠️⚠️⚠️
If you set it to `true`, boomboxes placed by a user will be automatically deleted when they leave the server.
If you set it to `false`, boomboxes will remain even if the player who placed them leaves.
]]
Config.StopMusicWhenVehicleIsEmpty = false --[[
⚠️⚠️⚠️
If you set it to `true`, the music will stop when there are no players in the vehicle.
If you set it to `false`, the music will continue to play even if there are no players in the vehicle.
]]
Config.SyncCheck = true --[[
⚠️⚠️⚠️
This variable belongs to the code structure that checks whether the song is progressing as it should, in sync with other people.
The reasons I use this are as follows:
- When the game is minimized, the CPU freezes all game-related processes for a certain number of seconds or minutes.
> This suspends the game's processes. When the player returns to the game, they will hear the songs behind schedule by the duration of the freeze compared to other players.
- Players experiencing occasional freezes or lag will hear the song delayed by the duration of the game's lag.
This way, the remaining users will move back to the current second.
💢💢💢
If you set it to `false`, when these conditions occur, the user's song will lag behind the others (by the amount of time it freezes or stutters).
Therefore, it will not be synchronized, and when the song ends at the expected second, it will suddenly cut off.
]]
Config.Carplay = {
["open"] = "G", -- Key to open car speaker menu.
["enabled"] = true, -- You determine whether music can be played in the car. `true` allows this. `false` prevents the music menu from opening in the vehicle.
["install"] = {
--[[
💢💢💢
Caution if you are going to use this system:
!-> You need to make some changes to your garage script for this system.
!-> If you can't do it or your script doesn't allow it (those parts are rescrow), please don't use it.
🔧🔧🔧
Trigger the following events/exports on the server or client side of your garage script when a new vehicle is spawned:
- Trigger this to send it to the server in a client-side operation:
- vehicleNetId: You can get the network ID of the vehicle using `NetworkGetNetworkIdFromEntity(vehicleEntity)`
> TriggerServerEvent('wais:speaker:server:CheckCarPlay', vehicleNetId)
- Trigger this export in server-side spawn operations:
- vehicleNetId: You can get the network ID of the vehicle using `NetworkGetNetworkIdFromEntity(vehicleEntity)`
> exports["wais-speaker"]:CheckCarPlay(vehicleNetId)
]]
["use"] = false, --[[
To play music in the car, a “car_play” or the specified item must be installed in that vehicle. This item can be purchased and installed by mechanics or other individuals.
The `true` value does not allow playing songs without installing the item in the car.
The `false` value allows you to use it via the menu without needing to install the item in the car.
]]
["item"] = "car_play", -- The item name that needs to be installed in the car to play music. (Only if `use` is `true`)
["remove_key"] = "H", -- The button used to remove the car radio installed in the vehicle. Only the actual owner of the vehicle can remove this item from the vehicle.
["only_owner_can_remove"] = false, -- If set to `true`, only the actual owner of the vehicle can remove the car radio item from the vehicle. If set to `false`, anyone can remove it.
["progresses"] = {
["install"] = {
["anim"] = {
["dict"] = "mini@repair",
["clip"] = "fixing_a_ped",
["flag"] = 16,
},
["duration"] = 5 * 1000, -- Time (in ms default is 10 sec) to install the car radio item in the vehicle
},
["remove"] = {
["anim"] = {
["dict"] = "mini@repair",
["clip"] = "fixing_a_ped",
["flag"] = 16,
},
["duration"] = 10 * 1000, -- Time (in ms default is 10 sec) to remove the car radio item from the vehicle
}
},
}
}
Config.InteractionDistances = {
--[[
🔉🔉🔉🎶🎶
These distances are selectable by users.
Even if a user renders a vehicle or speaker, they will not be able to hear the sound unless they approach within the selected distance.
When it enters within the selected distance, the sound will play at 0.0 volume. The level will increase as it approaches.
]]
["vehicles"] = 15.0, -- Maximum distance to hear speaker from vehicle
["speakers"] = 10.0, -- Maximum distance to hear speaker from ground speaker
}
Config.BoomBoxes = {
--[[
📻📻📻
Add your boombox items and models here.
Example: ["boombox_item_name"] = { ["model"] = "model_name", ["attach"] = { ... } }
]]
["boombox_a"] = {
["model"] = "qua_b_speaker_a",
},
["boombox_b"] = {
["model"] = "qua_b_speaker_b",
["attach"] = {
["bone"] = 4154, -- Bone index to attach to
["offsets"] = vector3(0.0065000279928427, 0.014985923761626, 0.03601949922086), -- Offsets from the bone (x, y, z)
["rotation"] = vector3(0.0, 0.0, 0.0), -- Rotation offsets (x, y, z)
["animation"] = {
["dict"] = "anim@heists@humane_labs@finale@keycards",
["clip"] = "ped_a_enter_loop",
["flag"] = 49,
}
},
},
["boombox_c"] = {
["model"] = "qua_b_speaker_c",
["attach"] = {
["bone"] = 4154, -- Bone index to attach to
["offsets"] = vector3(0.0065000279928427, 0.014985923761626, 0.03601949922086), -- Offsets from the bone (x, y, z)
["rotation"] = vector3(0.0, 0.0, 0.0), -- Rotation offsets (x, y, z)
["animation"] = {
["dict"] = "anim@heists@humane_labs@finale@keycards",
["clip"] = "ped_a_enter_loop",
["flag"] = 49,
}
},
},
["boombox_d"] = {
["model"] = "qua_b_speaker_d",
["attach"] = {
["bone"] = 60309, -- Bone index to attach to
["offsets"] = vector3(0.076437357549594, 0.0067159641248784, 0.074606438615931), -- Offsets from the bone (x, y, z)
["rotation"] = vector3(-97.361141027512, 0.61234434151617, 1.8357067867561), -- Rotation offsets (x, y, z)
["animation"] = {
["dict"] = "impexp_int-0",
["clip"] = "mp_m_waremech_01_dual-0",
["flag"] = 51,
}
},
},
["boombox_e"] = {
["model"] = "qua_b_speaker_e",
["attach"] = {
["bone"] = 57005, -- Bone index to attach to
["offsets"] = vector3(0.21548992346879, 0, -0.031528220090249), -- Offsets from the bone (x, y, z)
["rotation"] = vector3(1.0652214653109, -81.414172039437, -19.730610318178), -- Rotation offsets (x, y, z)
["animation"] = {
["dict"] = "move_weapon@jerrycan@generic",
["clip"] = "idle",
["flag"] = 51,
}
},
},
["boombox_f"] = {
["model"] = "qua_b_speaker_f",
},
}
Config.Actions = {
--[[
You can control features such as boombox placement, removal, and attachment from here.
]]
["place"] = {
["select_speaker_location"] = true, -- Enable/Disable boombox placement feature
["timeout"] = 10 * 1000, -- Time (in ms default is 10 sec) to wait for player to place boombox
["offsets"] = vector4(0.75, 0.0, 0.0, 180.0), -- Offset from player position to spawn boombox (x, y, z, heading)
["distance"] = 8.0, -- Max distance to place boombox from player
["buttons"] = {
["accept"] = "E", -- Key to place boombox
["cancel"] = "G" -- Key to cancel boombox placement
},
["animations"] = {
["dict"] = "amb@medic@standing@tendtodead@base",
["clip"] = "base",
["flag"] = 1,
["duration"] = 2 * 1000,
}
},
["detach"] = {
["key"] = "X" -- Key to detach boombox from your character and drop it on the ground
}
}
Config.BlacklistedVehicles = {
--[[
⚠️⚠️⚠️
From here, you can disable the speaker menu for the vehicle class or specific models of your choice,
so that the song menu will no longer open and songs will not play in those vehicles.
]]
["class"] = {
--[[
You can find all other vehicle classes here:
0: Compacts
1: Sedans
2: SUVs
3: Coupes
4: Muscle
5: Sports Classics
6: Sports
7: Super
8: Motorcycles
9: Off-road
10: Industrial
11: Utility
12: Vans
13: Cycles
14: Boats
15: Helicopters
16: Planes
17: Service
18: Emergency
19: Military
20: Commercial
21: Trains
22: Open Wheel
]]
[8] = true, -- Motorcycles
[13] = true, -- Cycles
[14] = true, -- Boats
[15] = true, -- Helicopters
[16] = true, -- Planes
},
["models"] = {
--[[
Example: Add specific vehicle models to blacklist by their hash. You can add as many as you want.
]]
[`issi2`] = true,
}
}
Config.BlacklistedCoords = {
--[[
🚫🚫🚫
You can add blacklist coordinates so that people cannot place speakers there.
If someone dies in that area, the speaker is returned to the inventory.
Songs cannot be played in vehicles within that area.
⚠️❓❌
If you have a speaker in your hand or enter the area with a song playing in your car,
the song will not stop. Because why should it stop? I think that much force is unnecessary.
Example: { coords = vector3(x, y, z), radius = 10.0 }
]]
{ coords = vector3(455.5057, -994.2595, 26.9876), radius = 45.0 }, -- Mission Row Police Department [MRPD]
{ coords = vector3(320.7686, -590.9871, 45.0281), radius = 40.0 } -- Pillbox Hill Medical Center [PHMC]
}
Config.Debug = function(...)
if Config.ShowDebug then
print(...)
end
end
Config.Notification = function(title, text, type, time)
title = title or "Speaker"
time = time or 5000
if lib ~= nil then
return lib.notify({
title = title,
type = type,
duration = time,
description = text,
iconAnimation = "beatFade",
position = "center-right"
})
end
if GetResourceState("okokNotify"):find("start") then
return exports['okokNotify']:Alert(title, text, time, type, true)
end
end
ConfigSv = {}
ConfigSv.Webhook = "" -- Webhook URL for logging when a song starts playing. Set this to your Discord webhook URL to enable logging.
ConfigSv.Inventorys = {
-- [[ 🟢 Inventory detections, to work automatically compatible with some inventories. 🟢 ]]
["qb_inventory"] = GetResourceState("qb-inventory"):find("start") and true or false,
["qs_inventory"] = GetResourceState("qs-inventory"):find("start") and true or false,
["ox_inventory"] = GetResourceState("ox_inventory"):find("start") and true or false,
["export"] = nil
}
---------------------------------------------------------------------------------------------------------------
---Function that takes care of player items giving
---@enum [parent=#ConfigSv] AddItem
---@param source
---@param item
---@param amount
---@return #boolean
ConfigSv.AddItem = function(src, item, amount)
local p = promise:new()
if Config.Framework.Framework == "esx" then
local xPlayer = wFramework.Framework.GetPlayerFromId(src)
if xPlayer ~= nil then
xPlayer.addInventoryItem(item, amount)
p:resolve(true)
else
Config.Debug(("[^1ERROR - ADDITEM^0] Could not find player with source %s - ConfigSv.AddItem"):format(src))
p:resolve(false)
end
elseif Config.Framework.Framework == "qbcore" or Config.Framework.Framework == "qbx" then
local Player = wFramework.Framework.Functions.GetPlayer(src)
if Player ~= nil then
if (ConfigSv.Inventorys.export == nil or ConfigSv.Inventorys.export:AddItem(src, item, amount) == nil) then
Player.Functions.AddItem(item, amount)
end
p:resolve(true)
else
Config.Debug(("[^1ERROR - ADDITEM^0] Could not find player with source %s - ConfigSv.AddItem"):format(src))
p:resolve(false)
end
else
Config.Debug(("[^1ERROR - ADDITEM^0] No compatible inventory found for adding item to player with source %s - ConfigSv.AddItem"):format(src))
p:resolve(false)
end
return Citizen.Await(p)
end
---------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------
---Function that takes care of player items removing
---@enum [parent=#ConfigSv] RemoveItem
---@param source
---@param item
---@param amount
---@return #boolean
ConfigSv.RemoveItem = function(src, item, amount)
local p = promise:new()
local Player = Config.Framework.Framework == "esx" and wFramework.Framework.GetPlayerFromId(src) or ((Config.Framework.Framework == "qbcore" or Config.Framework.Framework == "qbx") and wFramework.Framework.Functions.GetPlayer(src) or nil)
if not Player then
p:resolve(false)
Config.Debug(("[^1ERROR - REMOVEITEM^0] Could not find player with source %s - ConfigSv.RemoveItem"):format(src))
return Citizen.Await(p)
end
if ConfigSv.Inventorys.export ~= nil then
p:resolve(ConfigSv.Inventorys.export:RemoveItem(src, item, amount))
return Citizen.Await(p)
end
if Config.Framework.Framework == "qbcore" or Config.Framework.Framework == "qbx" then
p:resolve(Player.Functions.RemoveItem(item, amount))
else
if Player.getInventoryItem(item).count >= amount then
Player.removeInventoryItem(item, amount)
p:resolve(true)
else
p:resolve(false)
end
end
return Citizen.Await(p)
end
---------------------------------------------------------------------------------------------------------------
---Function to send a webhook when a song starts playing. It sends the owner's source, video ID, net ID, and metadata (which includes title, length, author, etc.). You can customize the data sent to the webhook as needed.
---@enum [parent=#ConfigSv] SendWebhook
---@param owner number The source of the player who started the song.
---@param videoId string The ID of the YouTube video being played.
---@param netId number The network ID of the speaker entity.
---@param metaData table A table containing metadata about the song, such as title, length, author, thumbnail, etc.
---@return void
ConfigSv.SendWebhook = function(owner, videoId, netId, metaData)
if NetworkGetEntityFromNetworkId(netId) == 0 then return end
if ConfigSv.Webhook == "" then return end
--[[
🟢🟢🟢
You can set up a webhook to log when a song starts playing.
The function receives the owner's source, video ID, net ID, and metadata (which includes title, length, author, etc.).
You can customize the data sent to the webhook as needed.
]]
local ts = os.time()
local time = os.date('!%Y-%m-%dT%H:%M:%S.000Z', ts)
local name = owner > 0 and GetPlayerName(owner) or "Server Console"
local entity = NetworkGetEntityFromNetworkId(netId)
local dynamicData = GetEntityType(entity) == 2 and ('Plate: %s'):format(GetVehicleNumberPlateText(entity)) or ('Coords: %s'):format(GetEntityCoords(entity))
local embed = {
{
["title"] = 'Played a Song',
["description"] = 'The player played a song',
["color"] = 16705372,
["fields"] = {
{["name"] = "📃 Player", ["value"] = ('**%s - [%s]**'):format(name, owner)},
{["name"] = "🎵 Song", ["value"] = ('**[%s](%s)**'):format(metaData.title, ('https://www.youtube.com/watch?v=%s'):format(videoId))},
{["name"] = "📍 Plate/Coords", ["value"] = dynamicData},
},
["author"] = {
["name"] = "Speaker",
["url"] = "https://0resmon.tebex.io/package/fivem-speaker-boombox-carplay-script",
["icon_url"] = "https://thumbs.dreamstime.com/b/minimalist-speaker-volume-icon-black-white-simple-black-white-speaker-icon-volume-waves-ideal-audio-music-378762185.jpg"
},
["timestamp"] = time,
["thumbnail"] = {
["url"] = metaData.thumbnail
}
}
}
PerformHttpRequest(ConfigSv.Webhook, function(err, text, headers) end, 'POST', json.encode({username = "Speaker", avatar_url = "https://cdn.discordapp.com/avatars/225154669817626626/12042ba04def3b74fdb3f6492853ee37.png?size=1024", embeds = embed}), { ['Content-Type'] = 'application/json' })
end
---------------------------------------------------------------------------------------------------------------
CreateThread(function()
ConfigSv.Inventorys.export = ConfigSv.Inventorys.qs_inventory and exports['qs-inventory'] or ConfigSv.Inventorys.ox_inventory and exports.ox_inventory or nil or ConfigSv.Inventorys.qb_inventory and exports['qb-inventory'] or nil
if not ConfigSv.Inventorys.export then
Config.Debug("No compatible inventory found, please set the export method manually in config_sv.lua")
end
end)