Ability to Overlap Sound Playback

It’s difficult to play the same sound effect multiple times quickly without it cutting itself off. The current behavior of Sound:Play() is that if the sound is already playing, it will stop the current playback and start from the beginning.

This is useful for certain cases (like background music), but it creates a roadblock for many common gameplay mechanics, forcing devs to use inefficient workarounds.

The Problem and Use Case

Many games require sound effects that can overlap:

  • Rapid-Fire Weapons: Each bullet sound should be its own instance and overlap with the previous one.
  • Collecting Items: A distinct sound for each coin collected.
  • UI Feedback: Rapidly clicking buttons or scrolling through an inventory. Sounds should layer on top of each other.

Current Workaround & It’s Downsides

One way to bypass this is to clone the Sound for every playback, parent it somewhere, play it, and then destroy it after it has finished.

local sfxClone = soundEffect:Clone()
sfxClone.Parent = workspace -- Or some other parent
sfxClone:Play()
game:GetService("Debris"):AddItem(sfxClone, sfxClone.TimeLength)

This does work, but it has downsides:

  1. Constantly creating and destroying instances in rapidly can lead to garbage collection stuttering.
  2. It temporarily fills the Workspace or SoundService with lots of clones.

A Solution: Sound.Overlap

A new boolean property for the Sound object, named Overlap

  • Sound.Overlap = false (Default)
    This would maintain the current behavior.

  • Sound.Overlap = true
    Calling :Play() will start a new playback of the sound without canceling any existing playbacks of that same sound instance..

1 Like

Hey @foamycoolkid5Sound.Overlap would have a lot of similar design problems to Sound.PlayOnRemove or SoundService:PlayLocalSound, which have been bug supermagnets :sweat_smile:

These APIs create secret/hidden copies of the sound which play to completion – since they have no representation in the datamodel, it’s impossible to find & stop the copies, which can lead to pileup. Further, even though these “detached” playbacks don’t have an Instance, they’re not free – most of the overhead of creating/destroying Sounds is allocating/deallocating audio resources, not the GC or their existence in Workspace/SoundService

All that considered, I think your current workaround is the best available option – if you want to avoid re-creating and destroying Sounds, you could store the instances in a “pool”, reusing them rather than destroying them; something like

local pool : {Sound} = {}

local function cloneFromPool(sound : Sound) : Sound
    if #pool == 0 then
        return sound:Clone()
    end

    local t = table.remove(pool)
    -- assign all the properties from `sound` onto `t`
    return t
end

local function destroyToPool(sound : Sound)
    table.insert(pool, sound)
end

-- ...

local sfxClone = cloneFromPool(soundEffect)
sfxClone.Parent = workspace -- Or some other parent
sfxClone.Ended:Connect(function() 
    destroyToPool(sfxClone) 
end)
sfxClone:Play()

Thanks for your explanation, it makes perfect sense. I’ll go with your pooling method from now on.