--[[ Metrópoles Vehicle Radio 3D + Ambiente em tempo real Baseado no recurso original mtrp_vehicle_radio. Versão TRUNK_LID_DEEP_MUFFLE + RADIO_STATIONS_SCROLL: - Scroll do mouse: navega entre estações de rádio reais. - Teclas [ e ]: controlam o volume. - Dentro do veículo: mantém som mais abafado/encorpado. - Fora do veículo: o som nasce no porta-malas. - Crossfade ao entrar/sair do veículo. - Porta-malas aberto/fechado afeta o som externo. - Refração/difração sonora realista. - Anti-ronco em longa distância. ]] local radioSound = {} local ENV_TICK = 120 local lastEnvTick = 0 -- ========================================================================= -- LISTA ORDENADA DE RÁDIOS -- Espelho da tabela do server. Usada para navegar com o scroll. -- ========================================================================= local RADIOS_LIST = { { nome = "89 FM (89.1)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/RADIO89FMAAC.aac" }, { nome = "Alpha FM (101.7)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/ALPHAFM_SP_AAC.aac" }, { nome = "Antena 1 (94.7)", url = "https://antenaone.crossradio.com.br/stream/1" }, { nome = "Band FM (96.1)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/BANDFM_SPAAC" }, { nome = "BandNews FM (96.9)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/BANDNEWSFM_SPAAC" }, { nome = "CBN SP (90.5)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/CBN_SPAAC.aac" }, { nome = "Kiss FM (92.5)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/RADIO_KISSFMAAC.aac" }, { nome = "Metropolitana (98.5)", url = "https://ice.fabricahost.com.br/metropolitana985sp" }, { nome = "Mix FM (106.3)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/MIXFM_SAOPAULOAAC.aac" }, { nome = "Nova Brasil (89.7)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/NOVABRASIL_SPAAC.aac" }, { nome = "Transamérica (100.1)", url = "https://playerservices.streamtheworld.com/api/livestream-redirect/RT_SPAAC.aac" }, } -- Índice atual da rádio selecionada local currentRadioIndex = 1 -- ========================================================================= -- REVERB BLEND -- ========================================================================= local REVERB_BLEND = { dryStart = 5.5, wetFull = 28.0, -- era 34.0 | começa a dominar mais cedo para cair antes do ruído dryMinFar = 0.18, -- era 0.28 | camada seca cai mais forte no longe wetMax = 0.62, -- era 0.92 | PRINCIPAL FIX: wet máximo muito mais baixo smoothing = 0.12, -- era 0.18 | suavização mais lenta = menos acúmulo de sinal } -- ========================================================================= -- INSIDE BLEND -- ========================================================================= local INSIDE_BLEND = { enterSmoothing = 0.105, exitSmoothing = 0.135, } -- ========================================================================= -- TRUNK LID -- ========================================================================= local TRUNK_LID = { doorIndex = 1, openRatioClean = 0.35, closedRatioMuffled = 0.05, smoothing = 0.16, bassGainMul = 2.85, dryMuffleMul = 0.10, wetMuffleMul = 0.07, totalMuffleMul = 0.34, bassLayerMul = 0.42, closedMaxDistance = 28.0, -- era 38.0 | alcance menor com porta fechado closedMinDistance = 2.4, } -- ========================================================================= -- SOUND REFRACTION -- ========================================================================= local SOUND_REFRACTION = { enabled = true, maxDistance = 60.0, minUsefulDistance = 2.0, sideOffset = 1.65, rearOffset = 1.35, topOffset = 1.15, rayHeight = 0.65, smoothing = 0.14, -- era 0.16 | mais lento para não acumular dryMul = 0.38, wetMul = 0.82, -- era 1.08 | PRINCIPAL FIX: estava amplificando o reverb acima de 1.0 totalMul = 0.72, -- era 0.82 | volume geral cai mais com obstáculo minWet = 0.18, -- era 0.30 | menos reverb mínimo forçado } -- ========================================================================= -- OPEN TRUNK RUMBLE CONTROL -- ========================================================================= local OPEN_TRUNK_RUMBLE_CONTROL = { enabled = true, startDistance = 14.0, -- era 20.0 | começa a cortar ruído mais cedo fullDistance = 70.0, -- era 58.0 | atinge o corte máximo antes dryFarMul = 0.28, -- era 0.48 | corta mais a camada seca no longe pressureFarMul = 0.55, -- era 0.78 | reduz mais a pressão de grave wetFarMul = 0.72, -- era 0.96 | PRINCIPAL FIX: wet também cai no longe } -- ========================================================================= -- RADIO 3D PRESETS -- ========================================================================= local RADIO_3D = { minDistance = 4.0, maxDistance = 70.0, nearBoostDistance = 5.5, farFadeDistance = 60.0, presets = { open = { reverbFx = true, echo = false, parameq = true, volumeMul = 0.72, bassPressure = true, }, open_near = { reverbFx = false, echo = false, parameq = true, volumeMul = 0.86, bassPressure = true, }, open_far = { reverbFx = true, echo = false, parameq = true, volumeMul = 0.38, bassPressure = true, }, street_near = { reverbFx = false, echo = false, parameq = true, volumeMul = 0.90, bassPressure = true, }, street_far = { reverbFx = true, echo = false, parameq = true, volumeMul = 0.58, bassPressure = true, }, alley_near = { reverbFx = false, echo = false, parameq = true, volumeMul = 0.91, bassPressure = true, }, alley_far = { reverbFx = true, echo = true, parameq = true, volumeMul = 0.60, bassPressure = true, }, tunnel_near = { reverbFx = false, echo = false, parameq = true, volumeMul = 0.93, bassPressure = true, }, tunnel_far = { reverbFx = true, echo = true, parameq = true, volumeMul = 0.62, bassPressure = true, }, street = { reverbFx = true, echo = false, parameq = true, volumeMul = 0.86, bassPressure = true, params = { room = -2300, roomHF = -1500, decayTime = 0.75, decayHFRatio = 0.55, reflections = -4200, reflectionsDelay = 0.015, reverb = -1700, reverbDelay = 0.020, diffusion = 35, density = 45, HFReference = 5000, inGain = 0, reverbMix = -28, reverbTime = 650, highFreqRTRatio = 0.50, } }, alley = { reverbFx = true, echo = true, parameq = true, volumeMul = 0.90, bassPressure = true, params = { room = -1400, roomHF = -1100, decayTime = 1.65, decayHFRatio = 0.62, reflections = -2200, reflectionsDelay = 0.030, reverb = -900, reverbDelay = 0.040, diffusion = 65, density = 75, HFReference = 4200, inGain = 0, reverbMix = -18, reverbTime = 1300, highFreqRTRatio = 0.58, echoWet = 14, echoFeedback = 12, echoDelay = 95, } }, tunnel = { reverbFx = true, echo = true, parameq = true, volumeMul = 0.94, bassPressure = true, params = { room = -700, roomHF = -1700, decayTime = 2.85, decayHFRatio = 0.44, reflections = -1200, reflectionsDelay = 0.050, reverb = 250, reverbDelay = 0.065, diffusion = 90, density = 100, HFReference = 3500, inGain = 0, reverbMix = -10, reverbTime = 2300, highFreqRTRatio = 0.42, echoWet = 21, echoFeedback = 18, echoDelay = 150, eqCenter = 3800, eqBandwidth = 18, eqGain = -4.5, } }, interior = { reverbFx = true, echo = false, parameq = true, volumeMul = 0.64, bassPressure = false, params = { room = -1150, roomHF = -2600, decayTime = 1.15, decayHFRatio = 0.38, reflections = -1800, reflectionsDelay = 0.018, reverb = -900, reverbDelay = 0.024, diffusion = 55, density = 60, HFReference = 3200, inGain = 0, reverbMix = -15, reverbTime = 1050, highFreqRTRatio = 0.38, eqCenter = 4200, eqBandwidth = 24, eqGain = -7.5, } }, trunk_closed_bass = { reverbFx = false, echo = false, parameq = true, compressor = true, volumeMul = 0.40, bassPressure = true, }, vehicle_inside = { reverbFx = false, echo = false, parameq = true, compressor = true, volumeMul = 0.52, bassPressure = true, params = { room = -2200, roomHF = -4200, decayTime = 0.45, decayHFRatio = 0.22, reflections = -3600, reflectionsDelay = 0.006, reverb = -2600, reverbDelay = 0.010, diffusion = 25, density = 35, HFReference = 0, inGain = 0, reverbMix = -35, reverbTime = 350, highFreqRTRatio = 0.25, eqCenter = 3000, eqBandwidth = 30, eqGain = -13.5, } }, occluded = { reverbFx = true, echo = false, parameq = true, volumeMul = 0.38, bassPressure = false, params = { room = -2000, roomHF = -5200, decayTime = 0.95, decayHFRatio = 0.25, reflections = -4000, reflectionsDelay = 0.020, reverb = -2100, reverbDelay = 0.030, diffusion = 40, density = 45, HFReference = 0, inGain = 0, reverbMix = -32, reverbTime = 750, highFreqRTRatio = 0.25, eqCenter = 2500, eqBandwidth = 30, eqGain = -11.5, } }, } -- fecha presets } -- fecha RADIO_3D -- ========================================================================= -- FUNÇÕES UTILITÁRIAS -- ========================================================================= local function clamp(v, min, max) if v < min then return min end if v > max then return max end return v end local function smoothstep(edge0, edge1, x) if edge0 == edge1 then return x >= edge1 and 1 or 0 end local t = clamp((x - edge0) / (edge1 - edge0), 0, 1) return t * t * (3 - 2 * t) end local function lerp(a, b, t) return a + ((b - a) * t) end -- ========================================================================= -- SAFE STOP SOUND -- ========================================================================= local function safeStopSound(veh) if radioSound[veh] then if isElement(radioSound[veh].soundElement) then stopSound(radioSound[veh].soundElement) end if isElement(radioSound[veh].wetSoundElement) then stopSound(radioSound[veh].wetSoundElement) end if isElement(radioSound[veh].insideSoundElement) then stopSound(radioSound[veh].insideSoundElement) end if isElement(radioSound[veh].bassSoundElement) then stopSound(radioSound[veh].bassSoundElement) end end radioSound[veh] = nil end -- ========================================================================= -- FX / PRESET -- ========================================================================= local function setFxEnabled(sound, preset) setSoundEffectEnabled(sound, "reverb", preset.reverbFx == true) setSoundEffectEnabled(sound, "echo", preset.echo == true) setSoundEffectEnabled(sound, "parameq", preset.parameq == true) setSoundEffectEnabled(sound, "compressor", preset.compressor == true) end local function applyReverb(sound, p) return true end local function applyEcho(sound, p) return true end local function applyEQ(sound, p) return true end local function applyPreset(sound, presetName) local preset = RADIO_3D.presets[presetName] or RADIO_3D.presets.open setFxEnabled(sound, preset) if preset.reverbFx then applyReverb(sound, preset.params) end if preset.echo then applyEcho(sound, preset.params) end if preset.parameq then applyEQ(sound, preset.params) end end -- ========================================================================= -- HELPERS DE VEÍCULO / POSIÇÃO -- ========================================================================= local function isInsideSameVehicle(veh) local playerVeh = getPedOccupiedVehicle(localPlayer) return playerVeh and playerVeh == veh end local function getTrunkOpenRatio(veh) if not isElement(veh) then return 1 end local ratio = 1 if getVehicleDoorOpenRatio then ratio = getVehicleDoorOpenRatio(veh, TRUNK_LID.doorIndex) or 0 end return clamp(tonumber(ratio) or 0, 0, 1) end local function getTrunkClosedAmount(veh) local openRatio = getTrunkOpenRatio(veh) return 1.0 - smoothstep(TRUNK_LID.closedRatioMuffled, TRUNK_LID.openRatioClean, openRatio) end local function getVehicleTrunkOffset(veh) local minX, minY, minZ, maxX, maxY, maxZ = getElementBoundingBox(veh) local rearY = -2.15 local heightZ = 0.45 if minY and maxY then rearY = minY + 0.25 end if minZ and maxZ then heightZ = math.max(0.25, math.min(0.85, minZ + ((maxZ - minZ) * 0.45))) end return 0, rearY, heightZ end local function getVehicleTrunkPosition(veh) local ox, oy, oz = getVehicleTrunkOffset(veh) local m = getElementMatrix(veh) if not m then local x, y, z = getElementPosition(veh) return x, y, z + 0.45 end local x = ox * m[1][1] + oy * m[2][1] + oz * m[3][1] + m[4][1] local y = ox * m[1][2] + oy * m[2][2] + oz * m[3][2] + m[4][2] local z = ox * m[1][3] + oy * m[2][3] + oz * m[3][3] + m[4][3] return x, y, z end local function isOccluded(vx, vy, vz, px, py, pz) if isLineOfSightClear(px, py, pz + 0.6, vx, vy, vz + 0.7, true, true, false, true, false, false, false, localPlayer) then return false end return true end local function matrixPoint(m, ox, oy, oz) return ox * m[1][1] + oy * m[2][1] + oz * m[3][1] + m[4][1], ox * m[1][2] + oy * m[2][2] + oz * m[3][2] + m[4][2], ox * m[1][3] + oy * m[2][3] + oz * m[3][3] + m[4][3] end -- ========================================================================= -- REFRAÇÃO SONORA -- ========================================================================= local function getSoundRefractionAmount(veh, sx, sy, sz, px, py, pz, dist) if not SOUND_REFRACTION.enabled then return 0 end if dist < SOUND_REFRACTION.minUsefulDistance or dist > SOUND_REFRACTION.maxDistance then return 0 end if not isOccluded(sx, sy, sz, px, py, pz) then return 0 end local m = getElementMatrix(veh) if not m then return 0.15 end local ox, oy, oz = getVehicleTrunkOffset(veh) local points = { { ox + SOUND_REFRACTION.sideOffset, oy, oz + SOUND_REFRACTION.rayHeight }, { ox - SOUND_REFRACTION.sideOffset, oy, oz + SOUND_REFRACTION.rayHeight }, { ox, oy, oz + SOUND_REFRACTION.topOffset }, { ox, oy - SOUND_REFRACTION.rearOffset, oz + SOUND_REFRACTION.rayHeight }, } local clear = 0 for _, point in ipairs(points) do local rx, ry, rz = matrixPoint(m, point[1], point[2], point[3]) if isLineOfSightClear(px, py, pz + 0.75, rx, ry, rz, true, true, false, true, false, false, false, localPlayer) then clear = clear + 1 end end if clear <= 0 then return 0.18 end local byRays = clamp(0.28 + (clear / #points) * 0.58, 0, 0.86) local byDistance = 1.0 - smoothstep(SOUND_REFRACTION.minUsefulDistance, SOUND_REFRACTION.maxDistance, dist) * 0.25 return clamp(byRays * byDistance, 0, 0.86) end -- ========================================================================= -- CONTEXT PRESET -- ========================================================================= local function getContextPreset(veh, dist, ignoreInside) if not ignoreInside and isInsideSameVehicle(veh) then return "vehicle_inside" end local px, py, pz = getElementPosition(localPlayer) local vx, vy, vz = getVehicleTrunkPosition(veh) local playerInterior = getElementInterior(localPlayer) local vehInterior = getElementInterior(veh) if playerInterior ~= vehInterior or playerInterior ~= 0 then return "interior" end local refractionAmount = getSoundRefractionAmount(veh, vx, vy, vz, px, py, pz, dist) if isOccluded(vx, vy, vz, px, py, pz) and refractionAmount < 0.28 then return "occluded" end local hits = 0 local checks = { { 9, 0, 0 }, { -9, 0, 0 }, { 0, 9, 0 }, { 0, -9, 0 }, { 0, 0, 7 }, { 14, 0, 2 }, { -14, 0, 2 }, { 0, 14, 2 }, { 0, -14, 2 }, } for _, c in ipairs(checks) do local clear = isLineOfSightClear( px, py, pz + 1.0, px + c[1], py + c[2], pz + 1.0 + c[3], true, true, false, true, false, false, false, localPlayer ) if not clear then hits = hits + 1 end end local env = "open" if hits >= 5 then env = "tunnel" elseif hits >= 3 then env = "alley" elseif dist < 28 then env = "street" end if dist <= 7.5 then return env .. "_near" elseif dist >= 24 then return env .. "_far" end return env end local function getExternalWetPresetName(basePresetName) if basePresetName == "occluded" then return "occluded" end if basePresetName == "interior" then return "interior" end if basePresetName:find("tunnel") then return "tunnel" end if basePresetName:find("alley") then return "alley" end if basePresetName:find("street") then return "street" end return "open" end -- ========================================================================= -- UPDATE ONE RADIO -- ========================================================================= local function updateOneRadio(veh, data) if not isElement(veh) or not data or not isElement(data.soundElement) then safeStopSound(veh) return end local drySound = data.soundElement local wetSound = data.wetSoundElement local insideSound = data.insideSoundElement local bassSound = data.bassSoundElement local sx, sy, sz = getVehicleTrunkPosition(veh) local px, py, pz = getElementPosition(localPlayer) local dist = getDistanceBetweenPoints3D(sx, sy, sz, px, py, pz) local targetRefraction = getSoundRefractionAmount(veh, sx, sy, sz, px, py, pz, dist) data.refractionBlend = lerp(data.refractionBlend or targetRefraction, targetRefraction, SOUND_REFRACTION.smoothing) local refraction = data.refractionBlend or 0 -- Mantém dimension/interior corretos setElementDimension(drySound, getElementDimension(veh)) setElementInterior(drySound, getElementInterior(veh)) if isElement(wetSound) then setElementDimension(wetSound, getElementDimension(veh)) setElementInterior(wetSound, getElementInterior(veh)) end if isElement(insideSound) then setElementDimension(insideSound, getElementDimension(veh)) setElementInterior(insideSound, getElementInterior(veh)) end if isElement(bassSound) then setElementDimension(bassSound, getElementDimension(veh)) setElementInterior(bassSound, getElementInterior(veh)) end local baseVolume = data.serverVolume or 1.0 local distMul = 1.0 if dist <= RADIO_3D.nearBoostDistance then distMul = isInsideSameVehicle(veh) and 1.00 or 1.18 else distMul = 1.0 - ((dist - RADIO_3D.nearBoostDistance) / (RADIO_3D.farFadeDistance - RADIO_3D.nearBoostDistance)) * 0.25 end distMul = clamp(distMul, 0.35, 1.10) local inside = isInsideSameVehicle(veh) local targetInsideBlend = inside and 1 or 0 local insideSmooth = inside and INSIDE_BLEND.enterSmoothing or INSIDE_BLEND.exitSmoothing data.insideBlend = lerp(data.insideBlend or 0, targetInsideBlend, insideSmooth) local targetTrunkClosed = getTrunkClosedAmount(veh) data.trunkClosedBlend = lerp(data.trunkClosedBlend or targetTrunkClosed, targetTrunkClosed, TRUNK_LID.smoothing) local trunkClosed = data.trunkClosedBlend or 0 local presetName = getContextPreset(veh, dist, true) local preset = RADIO_3D.presets[presetName] or RADIO_3D.presets.open local insidePreset = RADIO_3D.presets.vehicle_inside local dryPresetName = (presetName == "occluded" and "occluded" or "open_near") if data.currentDryPreset ~= dryPresetName then data.currentDryPreset = dryPresetName setTimer(function(s, name) if isElement(s) then applyPreset(s, name) end end, 1, 1, drySound, dryPresetName) end local wetPresetName = getExternalWetPresetName(presetName) if isElement(wetSound) and data.currentWetPreset ~= wetPresetName then data.currentWetPreset = wetPresetName setTimer(function(s, name) if isElement(s) then applyPreset(s, name) end end, 1, 1, wetSound, wetPresetName) end if isElement(insideSound) and data.currentInsidePreset ~= "vehicle_inside" then data.currentInsidePreset = "vehicle_inside" setTimer(function(s) if isElement(s) then applyPreset(s, "vehicle_inside") end end, 1, 1, insideSound) end if isElement(bassSound) and data.currentBassPreset ~= "trunk_closed_bass" then data.currentBassPreset = "trunk_closed_bass" setTimer(function(s) if isElement(s) then applyPreset(s, "trunk_closed_bass") end end, 1, 1, bassSound) end local externalPressureMul = 1.0 if preset.bassPressure then local pressure = 1.0 - clamp(dist / 18.0, 0, 1) externalPressureMul = 0.92 + (pressure * 0.22) end local openRumble = 0 if OPEN_TRUNK_RUMBLE_CONTROL.enabled then openRumble = smoothstep(OPEN_TRUNK_RUMBLE_CONTROL.startDistance, OPEN_TRUNK_RUMBLE_CONTROL.fullDistance, dist) * (1.0 - trunkClosed) externalPressureMul = externalPressureMul * lerp(1.0, OPEN_TRUNK_RUMBLE_CONTROL.pressureFarMul, openRumble) end externalPressureMul = externalPressureMul * lerp(1.0, TRUNK_LID.bassGainMul, trunkClosed) local internalPressureMul = 0.94 local closedRangeMul = lerp(1.0, clamp(1.0 - (dist / TRUNK_LID.closedMaxDistance), 0.18, 0.72), trunkClosed) setSoundMaxDistance(drySound, lerp(RADIO_3D.maxDistance, TRUNK_LID.closedMaxDistance, trunkClosed)) setSoundMinDistance(drySound, lerp(RADIO_3D.minDistance, TRUNK_LID.closedMinDistance, trunkClosed)) if isElement(wetSound) then setSoundMaxDistance(wetSound, lerp(RADIO_3D.maxDistance, TRUNK_LID.closedMaxDistance, trunkClosed)) setSoundMinDistance(wetSound, lerp(RADIO_3D.minDistance, TRUNK_LID.closedMinDistance, trunkClosed)) end if isElement(bassSound) then setSoundMaxDistance(bassSound, lerp(RADIO_3D.maxDistance, TRUNK_LID.closedMaxDistance, trunkClosed)) setSoundMinDistance(bassSound, lerp(RADIO_3D.minDistance, TRUNK_LID.closedMinDistance, trunkClosed)) end local refractionTotalMul = lerp(1.0, SOUND_REFRACTION.totalMul, refraction) local externalTotal = clamp( baseVolume * distMul * (preset.volumeMul or 1.0) * externalPressureMul * lerp(1.0, TRUNK_LID.totalMuffleMul, trunkClosed) * closedRangeMul * refractionTotalMul, 0, 1 ) local internalTotal = clamp(baseVolume * 1.00 * (insidePreset.volumeMul or 1.0) * internalPressureMul, 0, 1) local wetAmount = smoothstep(REVERB_BLEND.dryStart, REVERB_BLEND.wetFull, dist) if presetName == "occluded" then wetAmount = math.max(wetAmount, 0.42) end if presetName:find("alley") then wetAmount = math.max(wetAmount, 0.22) end if presetName:find("tunnel") then wetAmount = math.max(wetAmount, 0.30) end local dryAmount = lerp(1.00, REVERB_BLEND.dryMinFar, wetAmount) local wetAmountVol = lerp(0.00, REVERB_BLEND.wetMax, wetAmount) if refraction > 0.01 then dryAmount = dryAmount * lerp(1.0, SOUND_REFRACTION.dryMul, refraction) wetAmountVol = math.max(wetAmountVol, SOUND_REFRACTION.minWet * refraction) wetAmountVol = wetAmountVol * lerp(1.0, SOUND_REFRACTION.wetMul, refraction) end if openRumble and openRumble > 0.01 then dryAmount = dryAmount * lerp(1.0, OPEN_TRUNK_RUMBLE_CONTROL.dryFarMul, openRumble) wetAmountVol = wetAmountVol * lerp(1.0, OPEN_TRUNK_RUMBLE_CONTROL.wetFarMul, openRumble) end dryAmount = dryAmount * lerp(1.0, TRUNK_LID.dryMuffleMul, trunkClosed) wetAmountVol = wetAmountVol * lerp(1.0, TRUNK_LID.wetMuffleMul, trunkClosed) local externalBlend = 1.0 - (data.insideBlend or 0) local targetDry = externalTotal * dryAmount * externalBlend local targetWet = externalTotal * wetAmountVol * externalBlend local targetInside = internalTotal * (data.insideBlend or 0) local targetBass = 0 if isElement(bassSound) then local nearBass = 1.0 - clamp(dist / 20.0, 0, 1) targetBass = clamp( baseVolume * TRUNK_LID.bassLayerMul * (0.45 + nearBass * 0.55) * trunkClosed * externalBlend * closedRangeMul, 0, 0.55 ) end data.currentDryVolume = lerp(data.currentDryVolume or 0, targetDry, REVERB_BLEND.smoothing) data.currentWetVolume = lerp(data.currentWetVolume or 0, targetWet, REVERB_BLEND.smoothing) data.currentInsideVolume = lerp(data.currentInsideVolume or 0, targetInside, REVERB_BLEND.smoothing) data.currentBassVolume = lerp(data.currentBassVolume or 0, targetBass, REVERB_BLEND.smoothing) setSoundVolume(drySound, clamp(data.currentDryVolume, 0, 1)) if isElement(wetSound) then setSoundVolume(wetSound, clamp(data.currentWetVolume, 0, 1)) end if isElement(insideSound) then setSoundVolume(insideSound, clamp(data.currentInsideVolume, 0, 1)) end if isElement(bassSound) then setSoundVolume(bassSound, clamp(data.currentBassVolume, 0, 0.55)) end end -- ========================================================================= -- ENVIRONMENT ENGINE -- ========================================================================= local function startEnvironmentEngine() if isTimer(_G.mtrpRadioEnvTimer) then return end _G.mtrpRadioEnvTimer = setTimer(function() for veh, data in pairs(radioSound) do updateOneRadio(veh, data) end end, ENV_TICK, 0) end -- ========================================================================= -- CREATE VEHICLE RADIO SOUND -- ========================================================================= local function createVehicleRadioSound(url, veh, volume) if not isElement(veh) then return end safeStopSound(veh) local x, y, z = getVehicleTrunkPosition(veh) local drySound = playSound3D(url, x, y, z, false, false) local wetSound = playSound3D(url, x, y, z, false, false) local insideSound = playSound3D(url, x, y, z, false, false) local bassSound = playSound3D(url, x, y, z, false, false) if not isElement(drySound) then return end local ox, oy, oz = getVehicleTrunkOffset(veh) setSoundMinDistance(drySound, RADIO_3D.minDistance) setSoundMaxDistance(drySound, RADIO_3D.maxDistance) setElementDimension(drySound, getElementDimension(veh)) setElementInterior(drySound, getElementInterior(veh)) setSoundVolume(drySound, 0) attachElements(drySound, veh, ox, oy, oz) if isElement(wetSound) then setSoundMinDistance(wetSound, RADIO_3D.minDistance) setSoundMaxDistance(wetSound, RADIO_3D.maxDistance) setElementDimension(wetSound, getElementDimension(veh)) setElementInterior(wetSound, getElementInterior(veh)) setSoundVolume(wetSound, 0) attachElements(wetSound, veh, ox, oy, oz) end if isElement(insideSound) then setSoundMinDistance(insideSound, RADIO_3D.minDistance) setSoundMaxDistance(insideSound, RADIO_3D.maxDistance) setElementDimension(insideSound, getElementDimension(veh)) setElementInterior(insideSound, getElementInterior(veh)) setSoundVolume(insideSound, 0) attachElements(insideSound, veh, ox, oy, oz) applyPreset(insideSound, "vehicle_inside") end if isElement(bassSound) then setSoundMinDistance(bassSound, RADIO_3D.minDistance) setSoundMaxDistance(bassSound, RADIO_3D.maxDistance) setElementDimension(bassSound, getElementDimension(veh)) setElementInterior(bassSound, getElementInterior(veh)) setSoundVolume(bassSound, 0) attachElements(bassSound, veh, ox, oy, oz) applyPreset(bassSound, "trunk_closed_bass") end radioSound[veh] = { soundElement = drySound, wetSoundElement = wetSound, insideSoundElement = insideSound, bassSoundElement = bassSound, serverVolume = volume or 1.0, currentURL = url, currentDryPreset = nil, currentWetPreset = nil, currentInsidePreset = isElement(insideSound) and "vehicle_inside" or nil, currentBassPreset = isElement(bassSound) and "trunk_closed_bass" or nil, currentDryVolume = 0, currentWetVolume = 0, currentInsideVolume = 0, currentBassVolume = 0, insideBlend = isInsideSameVehicle(veh) and 1 or 0, trunkClosedBlend = getTrunkClosedAmount(veh), refractionBlend = 0, } setTimer(function() if isElement(drySound) then updateOneRadio(veh, radioSound[veh]) end end, 100, 1) startEnvironmentEngine() end -- ========================================================================= -- NAVEGAÇÃO ENTRE ESTAÇÕES POR SCROLL -- ========================================================================= local function findRadioIndexByURL(url) for i, radio in ipairs(RADIOS_LIST) do if radio.url == url then return i end end return 1 end local function changeRadioStation(direction) local veh = getPedOccupiedVehicle(localPlayer) if not veh then return end local seat = getPedOccupiedVehicleSeat(localPlayer) if seat ~= 0 and seat ~= 1 then return end if not radioSound[veh] or not isElement(radioSound[veh].soundElement) then return end -- Sincroniza o índice com a URL que está tocando if radioSound[veh].currentURL then currentRadioIndex = findRadioIndexByURL(radioSound[veh].currentURL) end -- Navega de forma circular currentRadioIndex = currentRadioIndex + direction if currentRadioIndex > #RADIOS_LIST then currentRadioIndex = 1 elseif currentRadioIndex < 1 then currentRadioIndex = #RADIOS_LIST end local novaRadio = RADIOS_LIST[currentRadioIndex] -- Atualiza a URL rastreada localmente radioSound[veh].currentURL = novaRadio.url -- Envia para o servidor processar e propagar para todos triggerServerEvent("onPlayerRadioStationChange", localPlayer, novaRadio.url, novaRadio.nome) end -- ========================================================================= -- VOLUME -- ========================================================================= function volumeUp() local veh = getPedOccupiedVehicle(localPlayer) if veh and radioSound[veh] and isElement(radioSound[veh].soundElement) then triggerServerEvent("onPlayerRadioVolumeChange", localPlayer, radioSound[veh].serverVolume or 1.0, true) end end function volumeDown() local veh = getPedOccupiedVehicle(localPlayer) if veh and radioSound[veh] and isElement(radioSound[veh].soundElement) then triggerServerEvent("onPlayerRadioVolumeChange", localPlayer, radioSound[veh].serverVolume or 1.0, false) end end -- ========================================================================= -- TOGGLE RADIO (tecla R) -- ========================================================================= function clientToggleRadio() triggerServerEvent("onPlayerToggleRadio", localPlayer) end -- ========================================================================= -- BINDS E INICIALIZAÇÃO -- ========================================================================= addEventHandler("onClientResourceStart", resourceRoot, function() -- Liga/desliga rádio bindKey("r", "down", clientToggleRadio) -- Scroll: navega entre estações bindKey("mouse_wheel_up", "down", function() changeRadioStation(1) end) bindKey("mouse_wheel_down", "down", function() changeRadioStation(-1) end) -- Colchetes: volume bindKey("bracketright", "down", volumeUp) -- ] = volume + bindKey("bracketleft", "down", volumeDown) -- [ = volume - startEnvironmentEngine() end) -- ========================================================================= -- EVENTOS DE ELEMENTO / VEÍCULO -- ========================================================================= addEventHandler("onClientElementDestroy", root, function() if radioSound[source] then safeStopSound(source) end end) addEventHandler("onClientVehicleExplode", root, function() if radioSound[source] then safeStopSound(source) end end) addEventHandler("onClientSoundStream", root, function(success, length, streamName) -- Mantido para debug futuro. end) addEventHandler("onClientSoundChangedMeta", root, function(streamTitle) -- Mantido para debug futuro. end) -- ========================================================================= -- EVENTO: Servidor manda ligar/desligar o rádio -- ========================================================================= addEvent("onServerToggleRadio", true) addEventHandler("onServerToggleRadio", localPlayer, function(toggle, url, veh, volume) if not isElement(veh) then safeStopSound(veh) return end if toggle == true then createVehicleRadioSound(url, veh, volume) else safeStopSound(veh) end end) -- ========================================================================= -- EVENTO: Servidor manda trocar URL (via /setradio) -- ========================================================================= addEvent("onServerRadioURLChange", true) addEventHandler("onServerRadioURLChange", localPlayer, function(newurl, veh, volume) if isElement(veh) then createVehicleRadioSound(newurl, veh, volume) -- Atualiza o índice local para a nova URL se estiver na lista if radioSound[veh] then radioSound[veh].currentURL = newurl currentRadioIndex = findRadioIndexByURL(newurl) end end end) -- ========================================================================= -- EVENTO: Servidor confirma troca de estação pelo scroll -- Recebido por TODOS os clientes para sincronizar o áudio 3D -- ========================================================================= addEvent("onServerRadioStationChanged", true) addEventHandler("onServerRadioStationChanged", localPlayer, function(newurl, nome, veh, volume) if not isElement(veh) then return end createVehicleRadioSound(newurl, veh, volume) -- Sincroniza o índice local com a nova estação if radioSound[veh] then radioSound[veh].currentURL = newurl currentRadioIndex = findRadioIndexByURL(newurl) end end) -- ========================================================================= -- EVENTO: Servidor confirma novo volume -- ========================================================================= addEvent("onServerVolumeChangeAccept", true) addEventHandler("onServerVolumeChangeAccept", localPlayer, function(veh, newVolume) if veh and radioSound[veh] and isElement(radioSound[veh].soundElement) then radioSound[veh].serverVolume = newVolume or 1.0 updateOneRadio(veh, radioSound[veh]) end end)