šŸŽ™ļø Introducing ReverbAPI | Super immersive audio for all!

okay so the material based reverb props are very cool and it performs very well but i dont think it worked as intended for me (it only changed the AmbientReverb in SoundService which is not customizable, it just has presets) and then i noticed another issue: it doesnt take into account the player’s surroundings - by that i mean walls and ceilings, there was reverb even in an open space.

so as a proof of concept i modified it:
i switched out the fibonacci sphere with a regular sphere (cuz it kinda messed up the weighing imo) and added a ā€œcoverageā€ variable which uses the raycast info (pos and normal) to weigh down the WetLevel in the reverb:

local coverage = 0
-- Weight Properties:
local floorWeight = -1
local ceilingWeight = 0.1
local wallWeight = 0.1

and this is how it turned out (i know its not the perfect solution, it can definitely be better)

this is the full code (dont judge, i know write in spaghetti):
-- ===================================================================
-- // REVERBAPI BY SYLZYRICAL | ORIGINAL CONCEPT BY GibusWielder
-- // VERSION 1.2 (12/6/2025)
-- ===================================================================
-- !DO NOT TOUCH ANYTHING BELOW!
-- ===================================================================
-- // SERVICES & MODULES
-- ===================================================================
local Services = {
	Workspace = game:GetService("Workspace"),
	Players = game:GetService("Players"),
	Debris = game:GetService("Debris"),
	RunService = game:GetService("RunService"),
	TweenService = game:GetService("TweenService"),
	SoundService = game:GetService("SoundService")
}

local SettingsModule = script:WaitForChild("Settings")
local MaterialsModule = script:WaitForChild("Materials")
local LegacyReverbMapModule = script:WaitForChild("MaterialsLegacyAudio")

if not (SettingsModule and MaterialsModule and LegacyReverbMapModule) then
	warn("ReverbAPI: Failed to load one or more required modules! Script will not run.")
	return
end

local Settings = require(SettingsModule)
local Materials = require(MaterialsModule)
local LegacyReverbMap = require(LegacyReverbMapModule)

-- ===================================================================
-- // LOCAL VARIABLES & CONFIG
-- ===================================================================
local Player = Services.Players.LocalPlayer
local PlayerScripts = Player:WaitForChild("PlayerScripts")

local Character = Player.Character or Player.CharacterAdded:Wait()
local Head = Character:WaitForChild("Head")

local isReverbActive = PlayerScripts:WaitForChild("ReverbAPI_Active", 10)
local voiceReverbTarget = nil
local voiceChatReverbScript = script:FindFirstChild("VoiceChatReverb")

local activeReverbTweens = {}
local sphereDirections = {}
local heartbeatConnection = nil

local FALLBACK_KEY = Settings.Reverb.MaterialsFallbackKey
local FALLBACK_REVERB_SETTINGS = Materials[FALLBACK_KEY]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Settings.Raycast.FilterType
raycastParams.IgnoreWater = Settings.Raycast.IgnoreWater

local NUM_RAYS = Settings.Raycast.NumRays
local RAY_LENGTH = Settings.Raycast.RayLength
local timeSinceLastUpdate = 0
local UPDATE_INTERVAL = Settings.Performance.UpdateInterval

-- ===================================================================
-- // AUDIO API SETUP
-- ===================================================================

local coverage = 0
-- Weight Properties:
local floorWeight = -1
local ceilingWeight = 0.1
local wallWeight = 0.1

local Output = Instance.new("AudioDeviceOutput")
local Reverb = Instance.new("AudioReverb")
local AudioListener = workspace.CurrentCamera:WaitForChild("AudioListener")
local ListenerWire = AudioListener:FindFirstChild("Wire") :: Wire

local OutputWire = Instance.new("Wire")
local ReverbWire = Instance.new("Wire")

Output.Player = Player
Output.Parent = Player
OutputWire.SourceInstance = Reverb
OutputWire.TargetInstance = Output
OutputWire.Parent = Output

local ReverbWire
if ListenerWire then
	ListenerWire.TargetInstance = Reverb
	Reverb.Parent = Output
else
	ReverbWire = Instance.new("Wire")
	Reverb.Parent = Output
end
if ReverbWire then
	ReverbWire.SourceInstance = AudioListener
	ReverbWire.TargetInstance = Reverb
	ReverbWire.Parent = Reverb
end
-- ===================================================================
-- // FUNCTION DEFINITIONS
-- ===================================================================

local function getVoiceReverbTarget()
	if voiceChatReverbScript and voiceChatReverbScript.Value then
		if voiceReverbTarget and voiceReverbTarget.Parent then return voiceReverbTarget end
		local target = voiceChatReverbScript:WaitForChild("PlayerVoiceReverb", 2)
		if target and target:IsA("AudioReverb") then
			voiceReverbTarget = target
			return voiceReverbTarget
		end
	end
	return nil
end

local function getMostHitMaterial()
	if not Head or not Head.Parent then return nil end
	local originPosition = Head.Position
	local materialCounts = {}
	local hits = 0
	for _, direction in ipairs(sphereDirections) do
		local result = Services.Workspace:Raycast(originPosition, direction * RAY_LENGTH, raycastParams)

		if result and result.Material then
			
			-- Check the normal and add (or subtract) to the coverage weight.
			local normal = result.Normal
			local pos = result.Position
			local rootPart = Character.PrimaryPart
			local rootPos = rootPart.Position
			hits+=1
			if math.abs(normal:Dot(Vector3.yAxis)) < 0.1 then
				coverage+=wallWeight
			else
				if pos.Y < rootPos.Y then
					coverage+=floorWeight
				else
					coverage+=ceilingWeight
				end
			end
			
			if Settings.Debug.VisualizeHits then
				local debugPart = Instance.new("Part")
				debugPart.CanQuery = false
				debugPart.Material = Enum.Material.Neon
				debugPart.Color = Color3.fromRGB(255, 0, 0)
				debugPart.Size = Vector3.new(0.15, 0.15, 0.15)
				debugPart.Anchored = true
				debugPart.CanCollide = false
				debugPart.Position = result.Position
				debugPart.Parent = Services.Workspace
				Services.Debris:AddItem(debugPart, Settings.Debug.VisualizeLifetime)

				if Settings.Debug.VisualizeHitMaterialName then
					local billboardGui = Instance.new("BillboardGui")
					billboardGui.Size = UDim2.fromScale(5, 1.5)
					billboardGui.StudsOffset = Vector3.new(0, 1.5, 0)
					billboardGui.AlwaysOnTop = true

					local textLabel = Instance.new("TextLabel")
					textLabel.Size = UDim2.fromScale(1, 1)
					textLabel.Text = result.Material.Name
					textLabel.TextColor3 = Color3.new(1, 1, 1)
					textLabel.TextScaled = true
					textLabel.BackgroundColor3 = Color3.new(0, 0, 0)
					textLabel.BackgroundTransparency = 0.5
					textLabel.Parent = billboardGui

					billboardGui.Parent = debugPart
				end
			end

			local materialName = result.Material.Name
			materialCounts[materialName] = (materialCounts[materialName] or 0) + 1
		end
	end
	coverage = (coverage/hits)
	local mostFrequentMaterial, highestCount = nil, 0
	for materialName, count in pairs(materialCounts) do
		if count > highestCount then
			highestCount = count
			mostFrequentMaterial = materialName
		end
	end
	return mostFrequentMaterial
end

local tweenInfo = TweenInfo.new(UPDATE_INTERVAL, Enum.EasingStyle.Linear)
local function applySettingsToVoiceReverb(settings, shouldBypass)
	local targetReverb = Reverb
	if not targetReverb then return end
	if activeReverbTweens[targetReverb] then
		activeReverbTweens[targetReverb]:Cancel()
	end
	local goalProperties = {}
	if shouldBypass then
		goalProperties.Bypass = true
	else
		for prop, value in pairs(settings) do
			if prop == "WetLevel" then
				goalProperties[prop] = value+coverage*20
				print("Coverage Weight: "..tostring(coverage*20))
				continue
			else
				goalProperties[prop] = value
			end
		end
		goalProperties.Bypass = false
	end
	local reverbTween = Services.TweenService:Create(targetReverb, tweenInfo, goalProperties)
	reverbTween:Play()
	activeReverbTweens[targetReverb] = reverbTween
	reverbTween.Completed:Connect(function() activeReverbTweens[targetReverb] = nil end)
end

local function updateReverb()
	if not Character or not Character.Parent or not Head or not Head.Parent then return end
	if not isReverbActive or not isReverbActive.Value then return end

	local dominantMaterial = getMostHitMaterial()
	local modernReverbSettings = Materials[dominantMaterial or ""] or FALLBACK_REVERB_SETTINGS
	local legacyReverbPreset = LegacyReverbMap[dominantMaterial or ""] or LegacyReverbMap[FALLBACK_KEY] or Enum.ReverbType.NoReverb

	local voiceReverbAvailable = (voiceChatReverbScript and voiceChatReverbScript.Value)
	local voiceTarget = getVoiceReverbTarget()

	if typeof(modernReverbSettings) == "table" then
		applySettingsToVoiceReverb(modernReverbSettings, false)
	else
		warn("ReverbAPI: Invalid modern reverb preset for material '" .. tostring(dominantMaterial) .. "'. Bypassing voice reverb.")
		applySettingsToVoiceReverb({}, true)
	end
end

local function disconnectHeartbeat()
	if heartbeatConnection then
		heartbeatConnection:Disconnect()
		heartbeatConnection = nil
	end
end

local function connectHeartbeat()
	if heartbeatConnection then return end
	heartbeatConnection = Services.RunService.Heartbeat:Connect(function(deltaTime)
		if not Character or not Character.Parent or not Head or not Head.Parent then
			disconnectHeartbeat()
			return
		end
		timeSinceLastUpdate = timeSinceLastUpdate + deltaTime
		if timeSinceLastUpdate >= UPDATE_INTERVAL then
			timeSinceLastUpdate = 0
			updateReverb()
		end
	end)
end

local function onCharacterAdded(newCharacter)
	disconnectHeartbeat()
	Character = newCharacter
	Head = Character:WaitForChild("Head", 5)
	if not Head then
		warn("ReverbAPI: CharacterAdded - Could not find Head!")
		return
	end

	local newFilterList = {Character}
	if Settings.Raycast.FilterDescendantsInstances then
		for _, item in ipairs(Settings.Raycast.FilterDescendantsInstances) do
			if item then table.insert(newFilterList, item) end
		end
	end
	raycastParams.FilterDescendantsInstances = newFilterList

	task.wait(0.1)
	connectHeartbeat()
	timeSinceLastUpdate = 0
	updateReverb()
end

-- ===================================================================
-- // INITIALIZATION & EVENT CONNECTIONS
-- ===================================================================
local samples = NUM_RAYS
local rings = math.floor(math.sqrt(samples))
local segments = math.floor(samples / rings)

-- Changed from fibonacci to a basic sphere.
for i = 0, rings - 1 do
	local phi = math.pi * (i / (rings - 1))

	for j = 0, segments - 1 do
		local theta = (2 * math.pi) * (j / segments)

		local x = math.sin(phi) * math.cos(theta)
		local y = math.cos(phi)
		local z = math.sin(phi) * math.sin(theta)

		table.insert(sphereDirections, Vector3.new(x, y, z))
	end
end


if isReverbActive then
	isReverbActive.Changed:Connect(function(isActive)
		if not isActive then
			local currentVoiceTarget = getVoiceReverbTarget()
			if currentVoiceTarget then
				applySettingsToVoiceReverb({}, true)
			end
			Services.SoundService.AmbientReverb = Enum.ReverbType.NoReverb
		else
			updateReverb()
		end
	end)
end

if voiceChatReverbScript then
	voiceChatReverbScript:GetPropertyChangedSignal("Value"):Connect(function()
		updateReverb() 
	end)
end


Player.CharacterAdded:Connect(onCharacterAdded)
Player.CharacterRemoving:Connect(disconnectHeartbeat)

if Player.Character then
	onCharacterAdded(Player.Character)
end

print("ReverbAPI_V1.2 | Sylzyrical")

you should def make something similar for the script, something thats more accurate and robust.

1 Like

for some reason the audio is delayed (shoulda recorded with obs instead of the built-in one)

Can you uncopylock the place cause its not working for me for some reason

oh i had that issue as well, turns out theres a check in the script:

local isReverbActive = PlayerScripts:WaitForChild("ReverbAPI_Active", 10)
...
if not isReverbActive or not isReverbActive.Value then return end

just create a bool named ā€œReverbAPI_Activeā€ in it and enable it.

I will be back on working on ReverbAPI once I’m back from vacation. I’ll take a look at your code suggestions :+1:

The testing place isn’t uncopylocked since I’m still working on the voice chat reverb, It will be uncopylocked tomorrow.

alr sounds good bro, enjoy ur vacay!

i added a quick hotfix for the missing ReverbAPI_Active issue, forgot to upload it with the system, it should work now

dude can you imagine playing a horror game with your buddies
then all of a sudden you hear a scream from inside a room

1 Like

omg thx u 4 making, i’ve been waiting for such a solution to appear!

Such a W resource! The material sound reverbs are very violent however - I know I can change the material settings inside the module script, but has anyone got a bit more universal and softer reverb for them all? I’m making an RPG so it helps.

Thanks for the great feedback!

I’m currently working on version 2.0 of ReverbAPI. It features the coverage system suggested by @Withoutruless. Voice chat reverb is working well, but I’m adjusting the material presets to make them sound more accurate. I also added an option to make the reverb fire from the camera if you want to hear reverb from the camera’s perspective.

About your question, I understand what you’re looking for. It sounds like you want a list of reverb presets that are less intense and more atmospheric, especially for the immersive feel of an RPG.

That’s a fantastic suggestion. As I’m already tuning the presets for version 2.0, I will definitely include a new preset switcher feature where you can switch from ā€˜Default’ to ā€˜RPG’ and etc. This preset will provide softer, more universal reverb settings across all materials so you can get that less harsh sound right out of the box.

Thanks again for the idea!