Hi
I saw this topic today and was surprised to know that the default roblox sound script doesn’t take Humanoid.WalkSpeed
in consideration, so decided to fork and add that functionality to it.
This works for the custom walk/climb/swim sounds as well, you’ll just have to configure the pitch in the SOUND_DATA
(defined on line 4)
Comparison
Default sound script behavior
Forked sound script behavior
Steps
- Insert a
LocalScript
in the StarterPlayerScripts and make sure to call itRbxCharacterSounds
- Copy and paste the source code inside
Source Code
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local SOUND_DATA : { [string]: {[string]: any}} = {
Climbing = {
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
Looped = true,
Pitch = 1,
},
Died = {
SoundId = "rbxasset://sounds/uuhhh.mp3",
},
FreeFalling = {
SoundId = "rbxasset://sounds/action_falling.mp3",
Looped = true,
},
GettingUp = {
SoundId = "rbxasset://sounds/action_get_up.mp3",
},
Jumping = {
SoundId = "rbxasset://sounds/action_jump.mp3",
},
Landing = {
SoundId = "rbxasset://sounds/action_jump_land.mp3",
},
Running = {
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
Looped = true,
Pitch = 1.85,
},
Splash = {
SoundId = "rbxasset://sounds/impact_water.mp3",
},
Swimming = {
SoundId = "rbxasset://sounds/action_swim.mp3",
Looped = true,
Pitch = 1.6,
},
}
local DEFAULT_RUN_SOUND_SPEED = SOUND_DATA.Running.Pitch
local DEFAULT_SWIM_SOUND_SPEED = SOUND_DATA.Swimming.Pitch
local DEFAULT_CLIMB_SOUND_SPEED = SOUND_DATA.Climbing.Pitch
local DEFAULT_WALKSPEED = 16
local RUNNING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_RUN_SOUND_SPEED
local SWIMMING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_SWIM_SOUND_SPEED
local CLIMBING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_CLIMB_SOUND_SPEED
-- wait for the first of the passed signals to fire
local function waitForFirst(...) -- RBXScriptSignal
local shunt: BindableEvent = Instance.new("BindableEvent")
local slots = {...}
local function fire(...)
for i = 1, #slots do
slots[i]:Disconnect()
end
return shunt:Fire(...)
end
for i = 1, #slots do -- RBXScriptSignal
slots[i] = slots[i]:Connect(fire) -- Change to RBXScriptConnection
end
return shunt.Event:Wait()
end
-- map a value from one range to another
local function map(x: number, inMin: number, inMax: number, outMin: number, outMax: number): number
return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
end
local function playSound(sound: Sound)
sound.TimePosition = 0
sound.Playing = true
end
local function shallowCopy(t)
local out = {}
for k, v in pairs(t) do
out[k] = v
end
return out
end
local function initializeSoundSystem(player: Player, humanoid: Humanoid, rootPart: BasePart)
local sounds: {[string]: Sound} = {}
-- initialize sounds
for name: string, props: {[string]: any} in pairs(SOUND_DATA) do
local sound: Sound = Instance.new("Sound")
sound.Name = name
-- set default values
sound.Archivable = false
sound.EmitterSize = 5
sound.MaxDistance = 150
sound.Volume = 0.65
for propName, propValue: any in pairs(props) do
sound[propName] = propValue
end
sound.Parent = rootPart
sounds[name] = sound
end
local playingLoopedSounds: {[Sound]: boolean?} = {}
local function stopPlayingLoopedSounds(except: Sound?)
for sound in pairs(shallowCopy(playingLoopedSounds)) do
if sound ~= except then
sound.Playing = false
playingLoopedSounds[sound] = nil
end
end
end
-- state transition callbacks.
local stateTransitions: {[Enum.HumanoidStateType]: () -> ()} = {
[Enum.HumanoidStateType.FallingDown] = function()
stopPlayingLoopedSounds()
end,
[Enum.HumanoidStateType.GettingUp] = function()
stopPlayingLoopedSounds()
playSound(sounds.GettingUp)
end,
[Enum.HumanoidStateType.Jumping] = function()
stopPlayingLoopedSounds()
playSound(sounds.Jumping)
end,
[Enum.HumanoidStateType.Swimming] = function()
local verticalSpeed = math.abs(rootPart.Velocity.Y)
if verticalSpeed > 0.1 then
sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1)
playSound(sounds.Splash)
end
stopPlayingLoopedSounds(sounds.Swimming)
sounds.Swimming.Playing = true
playingLoopedSounds[sounds.Swimming] = true
end,
[Enum.HumanoidStateType.Freefall] = function()
sounds.FreeFalling.Volume = 0
stopPlayingLoopedSounds(sounds.FreeFalling)
playingLoopedSounds[sounds.FreeFalling] = true
end,
[Enum.HumanoidStateType.Landed] = function()
stopPlayingLoopedSounds()
local verticalSpeed = math.abs(rootPart.Velocity.Y)
if verticalSpeed > 75 then
sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1)
playSound(sounds.Landing)
end
end,
[Enum.HumanoidStateType.Running] = function()
stopPlayingLoopedSounds(sounds.Running)
sounds.Running.Playing = true
playingLoopedSounds[sounds.Running] = true
end,
[Enum.HumanoidStateType.Climbing] = function()
local sound = sounds.Climbing
if math.abs(rootPart.Velocity.Y) > 0.1 then
sound.Playing = true
stopPlayingLoopedSounds(sound)
else
stopPlayingLoopedSounds()
end
playingLoopedSounds[sound] = true
end,
[Enum.HumanoidStateType.Seated] = function()
stopPlayingLoopedSounds()
end,
[Enum.HumanoidStateType.Dead] = function()
stopPlayingLoopedSounds()
playSound(sounds.Died)
end,
}
-- updaters for looped sounds
local loopedSoundUpdaters: {[Sound]: (number, Sound, Vector3) -> ()} = {
[sounds.Climbing] = function(dt: number, sound: Sound, vel: Vector3)
sound.Playing = vel.Magnitude > 0.1
end,
[sounds.FreeFalling] = function(dt: number, sound: Sound, vel: Vector3): ()
if vel.Magnitude > 75 then
sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1)
else
sound.Volume = 0
end
end,
[sounds.Running] = function(dt: number, sound: Sound, vel: Vector3)
sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5
end,
}
-- state substitutions to avoid duplicating entries in the state table
local stateRemap: {[Enum.HumanoidStateType]: Enum.HumanoidStateType} = {
[Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running,
}
local activeState: Enum.HumanoidStateType = stateRemap[humanoid:GetState()] or humanoid:GetState()
local stateChangedConn = humanoid.StateChanged:Connect(function(_, state)
state = stateRemap[state] or state
if state ~= activeState then
local transitionFunc: () -> () = stateTransitions[state]
if transitionFunc then
transitionFunc()
end
activeState = state
end
end)
local steppedConn = RunService.Stepped:Connect(function(_, worldDt: number)
-- update looped sounds on stepped
for sound in pairs(playingLoopedSounds) do
local updater: (number, Sound, Vector3) -> () = loopedSoundUpdaters[sound]
if updater then
updater(worldDt, sound, rootPart.Velocity)
end
end
end)
local function adjustPlaybackSpeed()
local walkSpeed = humanoid.WalkSpeed
sounds.Running.PlaybackSpeed = walkSpeed / RUNNING_DIVISOR
sounds.Swimming.PlaybackSpeed = walkSpeed / SWIMMING_DIVISOR
sounds.Climbing.PlaybackSpeed = walkSpeed / CLIMBING_DIVISOR
end
adjustPlaybackSpeed()
local speedChangedConn = humanoid:GetPropertyChangedSignal('WalkSpeed'):Connect(adjustPlaybackSpeed)
local humanoidAncestryChangedConn: RBXScriptConnection
local rootPartAncestryChangedConn: RBXScriptConnection
local characterAddedConn: RBXScriptConnection
local function terminate()
speedChangedConn:Disconnect()
stateChangedConn:Disconnect()
steppedConn:Disconnect()
humanoidAncestryChangedConn:Disconnect()
rootPartAncestryChangedConn:Disconnect()
characterAddedConn:Disconnect()
end
humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(_, parent)
if not parent then
terminate()
end
end)
rootPartAncestryChangedConn = rootPart.AncestryChanged:Connect(function(_, parent)
if not parent then
terminate()
end
end)
characterAddedConn = player.CharacterAdded:Connect(terminate)
end
local function playerAdded(player: Player)
local function characterAdded(character)
-- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications:
-- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically.
-- ** must use a waitForFirst on everything and listen for hierarchy changes.
-- * the character might not be in the dm by the time CharacterAdded fires
-- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again
-- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented.
-- * RootPart probably won't exist immediately.
-- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented.
if not character.Parent then
waitForFirst(character.AncestryChanged, player.CharacterAdded)
end
if player.Character ~= character or not character.Parent then
return
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
while character:IsDescendantOf(game) and not humanoid do
waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded)
humanoid = character:FindFirstChildOfClass("Humanoid")
end
if player.Character ~= character or not character:IsDescendantOf(game) then
return
end
-- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals
local rootPart = character:FindFirstChild("HumanoidRootPart")
while character:IsDescendantOf(game) and not rootPart do
waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded)
rootPart = character:FindFirstChild("HumanoidRootPart")
end
if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then
initializeSoundSystem(player, humanoid, rootPart)
end
end
if player.Character then
characterAdded(player.Character)
end
player.CharacterAdded:Connect(characterAdded)
end
Players.PlayerAdded:Connect(playerAdded)
for _, player in ipairs(Players:GetPlayers()) do
playerAdded(player)
end
Test Place
sound_sync.rbxl (44.8 KB)