> For the complete documentation index, see [llms.txt](https://docs.0resmon.org/0resmon/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.0resmon.org/0resmon/0resmon-1/wais-resoucres/wais-speaker-boombox/overview.md).

# Overview

***

## 📻 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

<mark style="color:$success;">**Muffled Sound**</mark>**:** Songs sound muffled and distant when outside the vehicle, creating a realistic outdoor feel.

<mark style="color:$success;">**Realistic Echo**</mark>**:** Dynamic reverb is applied to corridors, rooms, and enclosed spaces to create an echo effect appropriate for the space.

<mark style="color:$success;">**Interior Muffle**</mark>**:** Speakers or boomboxes inside buildings and structures sound muffled and distant from outside, creating a realistic outdoor feeling.

***

## 🔊 Boomboxes

**Enjoy yourself with&#x20;**<mark style="color:$success;">**6 different Boombox**</mark> models! (4 Portable, 2 Stationary)

**Place** the **Boombox** **wherever** 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 Download Speed

**Play any song you want on YouTube!** (Using the video ID)

\
All YouTube operations are performed entirely by another server, *<mark style="color:$success;">**so it does not cause issues like NUI (CEF) crashes!**</mark>*

\
**The loading speed for a song** that has not been played before varies **between 1 and 3 seconds**. <sub>*(This time was tested with a song with a maximum length of 5 minutes.)*</sub>

\
**Clients load songs in 1ms to 7ms**, providing very fast performance.

***

## ⚙️ Performance

<mark style="color:$success;">**NUI (CEF)**</mark>: 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.

\ <mark style="color:$success;">**Client Side**</mark>: **0.00 when idle**, but varies between **0.00 and 0.01ms when 1 speaker/boombox** is **rendered.**

\ <mark style="color:$success;">**Server Side**</mark>: **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 <mark style="color:$success;">**same song at the same time!**</mark>

\
**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

<details>

<summary>Config</summary>

```lua
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"] = "J", -- 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.
    ["keep_input"] = false, --[[
        🔆🔆🔆
        Whether you can control the car while the CarPlay menu is open.
        If it is `true`, you can continue driving when CarPlay is activated. You will not be able to control the vehicle when you click on Input. 
        If it is `false`, vehicle controls will not return until the menu closes when CarPlay is activated.
    ]]
    ["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
```

</details>

<details>

<summary>Server Config</summary>

```lua
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)
```

</details>

***

## 🚩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.

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.0resmon.org/0resmon/0resmon-1/wais-resoucres/wais-speaker-boombox/overview.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
