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!