AssetService:SearchAudioAsyncRead the docs →Sound Banks
NEWAudioScape:createSoundBank()On this pageThe problem▼
The problem
A footstep that plays the identical clip every time reads as obviously synthetic — the machine-gun effect. The usual fix is hand-picking a few assets and calling math.random.
-- The usual fix: five ids and math.random
local FOOTSTEPS = { "1837879082", "9046863579", "1839851000" }
sound.SoundId = "rbxassetid://" .. FOOTSTEPS[math.random(#FOOTSTEPS)]That works until one of those three is moderated, and then a third of your footsteps are silence. A sound bank is the same idea with the picking, the variation, and the healing handled for you.
From an asset you already have
Give a slot one asset and AudioScape finds sounds close to it, building a pool you can pick from. pick never returns the same asset twice in a row.
local bank = AudioScape:createSoundBank({
seeds = { footstep = "rbxassetid://1837879082" },
kind = "sfx",
})
bank:resolveAsync()
-- On every step:
sound.SoundId = "rbxassetid://" .. bank:pick("footstep")Your asset is never silently swapped. In the default extend mode it leads its own pool — we add to your choice rather than overriding it. mode = "replace" builds the pool from neighbours only, and you have to ask for it.
Seeds don't have to be in our catalog. If one isn't, the bank asks the engine what the asset is via GetAudioMetadataAsync — which works for any Roblox audio ID — and searches on its title and artist to find an anchor. bank.Pools[name].source tells you which path was taken: catalog, bridged, or none.
Or author the variation yourself
A sound bank expands one asset into neighbours it finds for you. When you want to choose every variation by hand — and control gain, trim points, fades and layers on each one — that is an Audio Pack. A pack is one composed sound: layers play together, and each layer picks from its own variations, so an explosion can be a different boom over a different tail every time.
SFX playlists in the console are a third, simpler thing: a flat list of sound effects your game can pull from, curated the same way a music playlist is. Fetch one with getPlaylist and pick from result.sounds yourself.
Resolve once, at startup
Roblox caps a server at 500 HTTP requests per minute, so resolving per-play would exhaust the budget almost immediately. After resolveAsync(), every pick is a local table lookup with no request — safe to call on every footstep.
When audio goes down
A pool built from our catalog only ever contains assets that are currently servable — moderation state is part of every query, so a flagged asset is gone the next time you resolve. For failures the server can't see, tell the bank and it stops picking that asset.
-- On the client, when an asset fails to load:
ContentProvider.AssetFetchFailed:Connect(function(contentId)
bank:reportUnavailable(contentId)
end)This has to run on a client. A Roblox server never fetches audio, so only a real player knows whether an asset loaded. When every asset in a pool is unavailable, the bank falls back to your seed rather than returning nothing.
Trim points
A lot of sound effects carry silence before the audio starts, which is why an impact can feel late even when you fired it on the right frame. We analyse where each clip actually begins and ends, and return it on every sound.
local result = AudioScape:getPlaylist({ playlist_id = "sfx-1785256484251" })
for _, s in result.sounds do
-- Skip the silence before the audio actually starts.
sound.SoundId = "rbxassetid://" .. s.asset_id
sound.TimePosition = s.sound_start_sec or 0
endsound_start_sec is present on 99.8% of the sound-effects catalog.
Options
| Option | Default | Description |
|---|---|---|
| seeds | — | Named asset IDs, e.g. { footstep = "rbxassetid://123" } |
| kind | "sfx" | "sfx" expands through sound effects, "music" through music |
| poolSize | 8 | How many assets to end up with per seed |
| mode | "extend" | "extend" keeps your asset and adds neighbours; "replace" uses neighbours only |
| playerId | — | Attributes the resolve to a player for analytics |
Methods
| bank:resolveAsync() | Build the pools. Returns (ok, err). |
| bank:pick(name) | An asset ID from that pool, never the same one twice running |
| bank:getPool(name) | The whole pool as a list, e.g. to replicate to clients |
| bank:reportUnavailable(assetId) | Drop an asset that failed to load |
| bank:flushPickCounts() | Emit accumulated pick analytics |
| bank:destroy() | Flush and clear |
Picks are counted locally and emitted as one rolled-up event per flush. A footstep loop would otherwise overrun the analytics buffer in seconds.