Custom Sound Object

I’ve actually made this a while ago, and I thought I’d make it into a module and share it with everyone here. It’s a sound object that gives you a little bit more control over what your sounds do ingame, and it’s specialized for something often referred to as ‘dynamic sound’ - meaning that as your distance to the sound source varies, the type of sound does too.

I currently use it to ‘muffle’ explosion and gun shot sounds as your camera gets further away from the sound source.

Here’s the source of the ModuleScript:

[code]–Custom Sound Object–
– VolcanoINC 2014 –

Sound = {}
Sound.__index = Sound

Sound.SpeedOfSound = 1020 – 340m/s = 1020 studs/sec
Sound.Sounds = {}
Sound.Running = false

Sound.SoundModel = Instance.new(“Model”)
Sound.SoundModel.Name = “SoundModel”
Sound.SoundModel.Parent = workspace

function Sound.new(data)
local v1 = data.Volume1
local v2 = data.Volume2
local d1 = data.Distance1
local d2 = data.Distance2
local ds = data.DopplerScale
local i1 = data.SoundId1
local i2 = data.SoundId2
local fn = data.Function
local ls = data.Lifespan
local at = data.Parent
local pt = data.Pitch

if (v1 == nil) then v1 = 0.5 end
if (v2 == nil) then v2 = 0.5 end
if (d1 == nil) then d1 = 100 end
if (d2 == nil) then d2 = d1*5 end
if (ds == nil) then ds = 0 end
if (pt == nil) then pt = 1 end
if (i1 == nil) then i1 = "" end
if (i2 == nil) then i2 = "" end
if (ls == nil or ls == 0) then ls = math.huge end

if (fn == nil) then
	fn = function(dist,vel1,vel2,self)
		local vol1 = (self.Volume1/self.Distance1) * (-dist) + self.Volume1
		local vol2 = (self.Volume2/self.Distance2) * (-dist) + self.Volume2
		
		local pitch = (1 + (((vel1-vel2).magnitude * vel1:Dot(vel2)) / Sound.SpeedOfSound) * self.DopplerScale) * self.Pitch
		
		return vol1,vol2,pitch
	end
elseif (fn == "LINEAR") then
	fn = function(dist,vel1,vel2,self)
		local vol1 = (self.Volume1/self.Distance1) * (-dist) + self.Volume1
		local vol2 = (self.Volume2/self.Distance2) * (-dist) + self.Volume2
		
		local pitch = (1 + (((vel1-vel2).magnitude * vel1:Dot(vel2)) / Sound.SpeedOfSound) * self.DopplerScale) * self.Pitch
		
		return vol1,vol2,pitch
	end
elseif (fn == "INVSQUARE") then
	fn = function(dist,vel1,vel2,self)
		local vol1 = self.Volume1 * (1/math.pow(dist/self.Distance1,2))
		local vol2 = self.Volume2 * (1/math.pow(dist/self.Distance2,2))
		
		local pitch = (1 + (((vel1-vel2).magnitude * vel1:Dot(vel2)) / Sound.SpeedOfSound) * self.DopplerScale) * self.Pitch
		
		return vol1,vol2,pitch
	end
end

local snd = {}
snd.Volume1 = v1
snd.Volume2 = v2
snd.Distance1 = d1
snd.Distance2 = d2
snd.DopplerScale = ds
snd.SoundId1 = i1
snd.SoundId2 = i2
snd.Parent = at
snd.Function = fn
snd.IsPlaying = false
snd.Pitch = pt
snd.Lifespan = ls

snd.Snd1 = Instance.new("Sound",Sound.SoundModel)
snd.Snd2 = Instance.new("Sound",Sound.SoundModel)

snd.Snd1.SoundId = i1
snd.Snd2.SoundId = i2
snd.Lifespan = ls

snd.O_POS1 = workspace.CurrentCamera.CoordinateFrame.p
if (at) then snd.O_POS2 = at.Position end

setmetatable(snd,Sound)
table.insert(Sound.Sounds,snd)
return snd

end

function Sound.stopHandler()
local p
for _,p in pairs(Sound.Sounds) do
p:remove()
end

Sound.Sounds = {}

Sound.Running = false

end

function Sound.startHandler()
Sound.stopHandler()
local co = coroutine.create(function()
Sound.Running = true
local runSvc = game:GetService(“RunService”)
local delta = tick()
while Sound.Running do
runSvc.RenderStepped:wait()
delta = delta - tick()
local ok,err = pcall(function()
local s
for _,s in pairs(Sound.Sounds) do
s:update(delta)
end
end)
if (not ok) then
Sound.stopHandler()
error(err)
end
delta = tick()
end
end)
coroutine.resume(co)
end

function Sound:play()
self.Snd1:play()
self.Snd2:play()
self.IsPlaying = true
end

function Sound:stop()
self.Snd1:stop()
self.Snd2:stop()
self.IsPlaying = false
end

function Sound:remove()
self:stop()
self.Snd1:Destroy()
self.Snd2:Destroy()

local i,k
for i,k in pairs(Sound.Sounds) do
	if (k == self) then
		table.remove(Sound.Sounds,i)
		break
	end
end

end

function Sound:update(delta)
local vel1,vel2,dist

vel1 = (self.O_POS1 - workspace.CurrentCamera.CoordinateFrame.p) / delta
self.O_POS1 = workspace.CurrentCamera.CoordinateFrame.p

if (self.Parent) then
	vel2 = (self.O_POS2 - self.Parent.Position) / delta
	self.O_POS2 = self.Parent.Position
	
	dist = (self.O_POS1 - self.O_POS2).magnitude
end

local vol1,vol2,pitch = self.Function(dist,vel1,vel2,self)

self.Snd1.Volume = math.max(math.min(1,vol1),0)
self.Snd2.Volume = math.max(math.min(1,vol2),0)

self.Snd1.Pitch = pitch
self.Snd2.Pitch = pitch

self.Lifespan = self.Lifespan + delta
if (self.Lifespan < 0) then
	self:remove()
end

end

return Sound
[/code]

It has the following API:

Functions (These are not part of the object, but part of the table the module returns):
-Sound.new(table data)
-Sound.startHandler()
-Sound.stopHandler()

Global properties (Not part of the object, but part of the table the module returns):
-Sound.SpeedOfSound

Methods:
-Sound:play()
-Sound:stop()
-Sound:remove()
-Sound:update(float deltaTime)

Properties:
-Sound.Volume1
-Sound.Volume2
-Sound.Distance1
-Sound.Distance2
-Sound.DopplerScale
-Sound.SoundId1
-Sound.SoundId2
-Sound.Parent
-Sound.Function
-Sound.IsPlaying
-Sound.Pitch
-Sound.Lifespan

Here’s a quick run-down of what all of these do:
[ul]
[li]Sound.new(table data) - creates a new sound object from the information you’ve given it in the table. The table uses the same names as the properties of the sound object, and any of these that you don’t set will be set to a default value.[/li]
[li]Sound.startHandler() - the object uses a handler, which continuously calls Sound:update(float deltaTime). That in turn calls Sound.Function every frame, which calculates the volumes for both sounds, and a pitch for both of them. Trying to start a handler while another one is running will stop the current handler and start a new one.[/li]
[li]Sound.stopHandler() - Stops the sound handler. While sounds will still play when the handler is stopped, they will both play at a volume of 1 no matter the distance.[/li]
[li]Sound.SpeedOfSound - The speed of sound in studs per second. This is used to calculate the pitch, along with Sound.DopplerScale[/li]
[li]Sound:play() - plays the sound once. If you call this method while the sound is already playing, it will cause that sound to start from the beginning.[/li]
[li]Sound:stop() - stops the sound if it’s playing, does nothing if it’s not.[/li]
[li]Sound:remove() - stops the sound if it’s playing and removes the object.[/li]
[li]Sound:update(float deltaTime) - calls Sound.Function and updates the sound accordingly. This is used internally and you should never have to call this unless you’re using your own handler.[/li]
[li]Sound.Volume1/2 - The volume factor of the two sounds.[/li]
[li]Sound.Distance1/2 - The range of both sounds, in studs.[/li]
[li]Sound.DopplerScale - Velocity scaling factor when calculating the doppler effect. A DopplerScale of 0 will cause the sound not to be affected by velocity.[/li]
[li]Sound.SoundId1/2 - The SoundIDs of both sounds, in the format “http://www.roblox.com/Asset?id=SOUNDID”.[/li]
[li]Sound.Parent - The source of the sound. Should be a BasePart. If it’s nil, the sound will play at full volume globally.[/li]
[li]Sound.Function - This is called every frame to calculate the pitch and volume of the sounds. You can set your own, but “LINEAR” and “INVSQUARE” are pre-defined in the constructor. The function receives distance,velocity1,velocity2,soundobject as parameters and returns volume1,volume2,pitch.[/li]
[li]Sound.IsPlaying - Becomes TRUE when you call Sound:play() and FALSE when you call Sound:stop().[/li]
[li]Sound.Pitch - The pitch of both sounds. This is the same for both objects as of now.[/li]
[li]Sound.Lifespan - the amount of time after which the object removes itself. This is a feature intended to clean up sounds that are no longer in use, simply pass math.huge to this to prevent it from being removed.[/li]
[/ul]

A quick example of use:

[code]Sound = require(script.Parent.Sound)
Sound.startHandler()

data = {}
data.Volume1 = 1
data.Volume2 = 1
data.Distance1 = 70
data.Distance2 = 700
data.DopplerScale = 0
data.SoundId1 = “http://www.roblox.com/Asset?id=134589225
data.SoundId2 = “http://www.roblox.com/Asset?id=177174605
data.Function = “LINEAR”
data.Lifespan = 3
data.Parent = workspace.Part

while true do
wait(1)
local snd = Sound.new(data)
snd:play()
end

[/code]
Run this code from a LocalScript, and it will play a gunshot sound at Workspace.Part, which removes itself after 3 seconds.

I hope some of you will find this useful - and as always, feedback is much appreciated!

4 Likes