EasySFX - A sound effect management module

EasySFX

An easy to understand roblox module for managing sounds and sound effects.

  

EasySFX Allows you to:

  • Easily apply effects to sounds dynamically via scripts.
  • Manage effects on sounds effortlessly.
  • Have clean code.

Here’s a quick code example:


local EasySFX = require(game.ReplicatedStorage.EasySFX)
local soundObj = script.Sound
local sound = EasySFX:Load(soundObj) -- (Returns EasySFX object)

sound.Sound -- Reference to soundObj (kinda useless but its there.)
-- Apply effects (returns their instance):
local sound_Echo = sound:Echo() -- Adds echo.

-- Apply effects with properties:
local sound_Reverb = sound:Reverb({
    DryLevel = 1,
    WetLevel = -2
})

-- Apply properties **after** initializing the effect:
sound_Echo.DryLevel = 0

-- Remove an effect (takes the effect instance):
sound.RemoveEffect(sound_Echo) -- Remove the echo effect.

--  Set a function to be executed on .Unload() 
--  Must be used before Unloading else your function wont fire.
sound.OnUnload(function()
-- Your code goes here.
end)

-- Unload the sound and remove all effects:
sound.Unload() -- Unload the sound. Returns true on success, nil otherwise.

-- Or you can use Destroy (same as Unload() + Destroys the sound)

sound.Destroy() -- Destroys the sound. Returns true on success, nil otherwise.

DO NOTE:

this is my first time releasing a module, there may be bugs or more severe issues, so be sure to report anything you encounter and i’ll do my best to respond swiftly.
If you have any suggestions on what i should add, please leave them down below.

2 Likes

Here is the code if anyone wants to take a look. I was looking for something like this until recently and looks pretty good. It would be nice if this module used metatables and metamethods for the sound objects.

--!optimize 2
--!native

-- Version: [1.0.0]
-- EasySFX, a simple sound effect management module by [@Withoutruless].
-- Feel free to modify anything and use it for your games,
-- Crediting me would be appreciated but its not necessary.

local api = {}
------------------------

-- Exporting Properties --
local types = require(script.types)
export type reverbProperties = types.ReverbProperties
export type echoProperties = types.EchoProperties
export type equalizerProperties = types.EqualizerProperties
export type compressorProperties = types.CompressorProperties
export type chorusProperties = types.ChorusProperties
export type tremoloProperties = types.TremoloProperties
export type distortionProperties = types.DistortionProperties
export type pitchshiftProperties = types.PitchShiftProperties
export type flangeProperties = types.FlangeProperties
------------------------

-- This table contains all of the loaded sounds (hence its name).
local loadedSounds = {}

------------------------

-- You can set this here or through the '.Logging()' function.
local logging = false

------------------------

-- Simple logging function that puts the name of the module beforehand.
local function log(str: string)
	if logging and typeof(str) == "string" then
		warn("EasySFX: "..str)
	else return
	end
end

-- Set logging to true or false (Default: false).
function api.Logging(value: boolean)
	if typeof(value) == "boolean" then
		logging = value
		log("Logging set to "..tostring(value))
	end
end

-- This function returns an effect instance based on the name it receives.
local function returnEffect(s: string)
	return Instance.new(s)
end

-- This function executes functions passed through .OnUnload()
local function callbackFunction(con: {})
	for _,func in ipairs(con) do
		func()
	end
	con = nil
end

-- This function applies properties to the provided effect.
local function applyProperties(instance: Instance, properties: {[string]: any}?)
	if not properties then return end
	if typeof(properties) ~= "table" then return end
	for property, value in pairs(properties) do
		if instance[property] == nil then continue end
		if instance[property] ~= value then
			instance[property] = value
			log(instance.Name.."."..property.."="..tostring(value))
		end
	end
end

-- This function does most of the work
-- Applies the effect, the properties and loads it up into the module.
local function effectInternal(audio: Sound,effect: string,props: any?)
	if not loadedSounds[audio]["SoundEffects"][effect] then
		local se = returnEffect(effect)
		applyProperties(se,props)
		loadedSounds[audio]["SoundEffects"][effect] = se
		se.Parent = audio
		log("Adding "..se.Name.. " To "..audio.Name)
		return se
	else
		local se = loadedSounds[audio]["SoundEffects"][effect]
		applyProperties(se,props)
		return se
	end
end

-- Load a sound into the module (Returns table)
function api:Load(audio: Sound)
	if audio:IsA("Sound") then
		if not loadedSounds[audio] then
			log("Loaded "..audio.Name)
			loadedSounds[audio] = {}
			loadedSounds[audio]["SoundEffects"] = {}
			local connections = {}
			local easySFXObj = {
				Sound = audio,
			}

			-- Removes the specified sound effect from the sound.
			-- Takes sound effects as input.
			function easySFXObj.RemoveEffect(effect: Instance?)
				if effect then else return nil end
				if typeof(effect) == "Instance" then else return nil end
				if effect.Parent ~= audio then
					log("Different effect parent provided for removal: "..effect.ClassName)
					return nil
				else
					if loadedSounds[audio] then
						if loadedSounds[audio]["SoundEffects"][effect.ClassName] then
							effect:Destroy()
							loadedSounds[audio]["SoundEffects"][effect.ClassName] = nil
							log(effect.ClassName.." removed from "..audio.Name)
							return true
						else
							return nil
						end
					end
				end
			end
			-- Returns effects currently on Sound.
			-- Returns a normal table containing their names.
			function easySFXObj.GetEffectNames(): { [number]: string }
				if loadedSounds[audio] then
					if loadedSounds[audio]["SoundEffects"] then
						local t = {}
						for i,_ in pairs(loadedSounds[audio]["SoundEffects"]) do
							table.insert(t,i)
						end
						return t
					end
				end
				return {}
			end
			-- Returns effects currently on Sound.
			-- Returns their instances with their names as indexes.
			function easySFXObj.GetEffects(): { [string]: Instance }
				if loadedSounds[audio] then
					if loadedSounds[audio]["SoundEffects"] then
						return loadedSounds[audio]["SoundEffects"]
					end
				end
				return {}
			end
			-- Executes connection(s) on :Unload() and :Destroy().
			function easySFXObj.OnUnload(Function: (any?))
				if typeof(Function) == "function" then
					table.insert(connections,Function)
				else
					log("Invalid function provided (OnUnload)")
				end
			end
			-- Unloads the sound from the module (removes all effects).
			function easySFXObj.Unload()
				if loadedSounds[audio] then
					local audioTable = loadedSounds[audio]
					if audioTable["SoundEffects"] then
						for _,v in pairs(audioTable["SoundEffects"]) do
							v:Destroy()
						end
						loadedSounds[audio]["SoundEffects"] = {}
						loadedSounds[audio] = nil
						log("Unloaded "..audio.Name)
						callbackFunction(connections)
						return true
					else
						loadedSounds[audio] = nil
						log("Unloaded "..audio.Name)
						callbackFunction(connections)
						return true
					end
				else
					return nil
				end
			end
			-- Removes the sound from the module and destroys it.
			function easySFXObj.Destroy()
				if loadedSounds[audio] then
					local audioTable = loadedSounds[audio]
					if audioTable["SoundEffects"] then
						for _,v in pairs(audioTable["SoundEffects"]) do
							v:Destroy()
						end
						loadedSounds[audio]["SoundEffects"] = {}
						loadedSounds[audio] = nil
						log("Destroyed "..audio.Name)
						callbackFunction(connections)
						audio:Destroy()
						return true
					else
						loadedSounds[audio] = nil
						log("Destroyed "..audio.Name)
						callbackFunction(connections)
						audio:Destroy()
						return true
					end
				else
					return nil
				end
			end
			-- Adds reverb to the sound (or returns existing instance).
			function easySFXObj:Reverb(properties: reverbProperties)
				local se = effectInternal(audio,"ReverbSoundEffect",properties)
				return se :: ReverbSoundEffect
			end
			-- Adds echo to the sound (or returns existing instance).
			function easySFXObj:Echo(properties: echoProperties)
				local se = effectInternal(audio,"EchoSoundEffect",properties)
				return se :: EchoSoundEffect
			end
			-- Adds an equalizer to the sound (or returns existing instance).
			function easySFXObj:Equalizer(properties: equalizerProperties)
				local se = effectInternal(audio,"EqualizerSoundEffect",properties)
				return se :: EqualizerSoundEffect
			end
			-- Adds a compressor to the sound (or returns existing instance).
			function easySFXObj:Compressor(properties: compressorProperties)
				local se = effectInternal(audio,"CompressorSoundEffect",properties)
				return se :: CompressorSoundEffect
			end
			-- Adds chorus to the sound (or returns existing instance).
			function easySFXObj:Chorus(properties: chorusProperties)
				local se = effectInternal(audio,"ChorusSoundEffect",properties)
				return se :: ChorusSoundEffect
			end
			-- Adds tremolo to the sound (or returns existing instance).
			function easySFXObj:Tremolo(properties: tremoloProperties)
				local se = effectInternal(audio,"TremoloSoundEffect",properties)
				return se :: TremoloSoundEffect
			end
			-- Adds distortion to the sound (or returns existing instance).
			function easySFXObj:Distortion(properties: distortionProperties)
				local se = effectInternal(audio,"DistortionSoundEffect",properties)
				return se :: DistortionSoundEffect
			end
			-- Adds pitch shift to the sound.
			function easySFXObj:PitchShift(properties: pitchshiftProperties)
				local se = effectInternal(audio,"PitchShiftSoundEffect",properties)
				return se :: PitchShiftSoundEffect
			end
			-- Adds flange to the sound (or returns existing instance).
			function easySFXObj:Flange(properties: flangeProperties)
				local se = effectInternal(audio,"FlangeSoundEffect",properties)
				return se :: FlangeSoundEffect
			end
			loadedSounds[audio]["Functions"] = easySFXObj
			return easySFXObj
		else
			if loadedSounds[audio] then
				if loadedSounds[audio]["Functions"] then
					log("Returning existing Functions.")
					return loadedSounds[audio]["Functions"]
				end
			end
		end
	else
		log(audio.Name.." Is not a Sound.")
		return nil
	end
end
return api

Why though? It is unnecessary; OP likely doesn’t intend for the user to create a new class from it.

Closing the inheritance like that is probably for the better; it also allows for somewhat better performance because there are no metatable lookups.

Regardless, I’d say it’s unnecessary, and I would much rather OP keep things the way they are, simple to use, no unnecessary complexity.

Also, looking at the module code, there is caching present for objects that already exist. We should not forget Luau has lots of optimisations built in for closures and other things. The DUPCLOSURE opcode, for example or maybe the GETIMPORT op code—they’re both useful optimizations that will regardless help you making using metatables likely not worth it in my opinion. We can only measure to know the truth, really.

1 Like

thank you, i dont really know how to use metatables/metamethods tho so ill have to look into it.

By defining functions within each object, you are sacrificing memory. Using metatables is more memory efficient and should be favored for OOP in Lua, regardless of whether inheritance is needed or not.

1 Like

Realistically, we would need measuring to know truly.

However I will stand by my word, I rather copy closures than make my accesses lots of times more expensive in an effort to “decrease” memory usage

There is no need to measure. Defining functions within each object is not scalable: if you make 100 objects, you are defining the same function 100 times, when you can do it once with metatables. In Programming in Lua (first edition) chapter 16.1 on classes, they use metatables.

In this tutorial, the author says, “it might be tempting just to put the functions in the table when it is constructed however this is both inefficient and messy.”

Additionally in another tutorial, the author writes, “The issue at hand is that we have created a function inside the object itself. Let’s say we create two objects. That means both objects have its own shout function. Two functions that do exactly the same thing. This uses up memory that could otherwise be avoided.”

There absolutely is a difference in memory usage, increasing linearly as more objects are created.