NEWA drop-in replacement for AssetService:SearchAudioAsyncRead the docs →
← API Reference
Luau SDK

Sound Banks

NEW
AudioScape: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.

Luau (Roblox)
-- 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.

Luau (Roblox)
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.

Luau (Roblox)
-- 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.

Luau (Roblox)
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
end

sound_start_sec is present on 99.8% of the sound-effects catalog.

Options

OptionDefaultDescription
seedsNamed asset IDs, e.g. { footstep = "rbxassetid://123" }
kind"sfx""sfx" expands through sound effects, "music" through music
poolSize8How many assets to end up with per seed
mode"extend""extend" keeps your asset and adds neighbours; "replace" uses neighbours only
playerIdAttributes 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.