Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## v0.19.0

### Removed

- **`createSoundBank({ playlist_id = ... })` is gone**, along with the `slots` field on the playlist response and the `label` field on each playlist sound. SFX playlists are flat.

The slot layer was never used: across the whole catalog only two playlists ever carried a slot label, and both were tests. Real SFX playlists organise by having more than one playlist — "ObbySounds: Hits", "LittleMonsters-Vocalizations" — where the playlist name is the role. And because an unlabelled sound became its own slot, a flat playlist resolved to pools of exactly one, so `pick` had nothing to choose between.

Variation for a single sound is what Audio Packs are for: layers with per-variation gain, cuts and fades.

Seed-based banks are unchanged and remain the way to expand one asset into a pool:

```lua
local bank = AudioScape:createSoundBank({
seeds = { footstep = "rbxassetid://1837879082" },
})
bank:resolveAsync()

sound.SoundId = "rbxassetid://" .. bank:pick("footstep")
```

## v0.18.0

### Added
Expand Down
17 changes: 1 addition & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Add to your `wally.toml`:

```toml
[server-dependencies]
AudioScape = "this-fifo/audioscape-sdk@0.18.0"
AudioScape = "this-fifo/audioscape-sdk@0.19.0"
```

Then run:
Expand Down Expand Up @@ -445,28 +445,13 @@ sound.SoundId = "rbxassetid://" .. bank:pick("footstep")
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `seeds` | `{ [string]: string }?` | — | Named asset IDs, e.g. `{ footstep = "rbxassetid://123" }` |
| `playlist_id` | `string?` | — | Build from a sound bank curated in the portal instead. Pass this or `seeds`, not both |
| `kind` | `string?` | `"sfx"` | `"sfx"` expands through the sound-effects catalog, `"music"` through music |
| `poolSize` | `number?` | `8` | How many assets to end up with per seed |
| `mode` | `string?` | `"extend"` | `"extend"` keeps your asset in the pool and adds neighbours; `"replace"` uses neighbours only |
| `playerId` | `number?` | — | |

**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. `replace` exists for cases where you genuinely don't care which specific clip plays, and you have to ask for it.

### Building from a portal-curated bank

Slots you set up in the portal become pools, so the sounds a game plays can change without shipping code.

```lua
local bank = AudioScape:createSoundBank({ playlist_id = "sfx-1785256484251" })
bank:resolveAsync()

sound.SoundId = "rbxassetid://" .. bank:pick("footstep")
```

Each slot is used **exactly as curated** — no similarity expansion. Whoever built the bank already decided what belongs in each slot, and adding sounds they never listened to isn't ours to do. Widen a slot in the portal, where you can hear it first. `bank.Pools[name].source` reads `"playlist"` on this path.

Everything else is unchanged: `pick` still avoids immediate repeats, and `reportUnavailable` still drops an asset that fails to load.

### Methods

Expand Down
80 changes: 2 additions & 78 deletions src/AudioScapeSoundBank.luau
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ local DEFAULT_POOL_SIZE = 8
-- approach AudioScapeMusicPlayer uses for its analytics dependency, so this
-- module stays independent of init.luau's concrete type.
type CatalogClient = {
getPlaylist: (self: any, options: any) -> (any, string?),
lookup: (self: any, options: any) -> (any, string?),
search: (self: any, options: any) -> (any, string?),
sfxSearch: (self: any, options: any) -> (any, string?),
Expand All @@ -42,12 +41,7 @@ type CatalogClient = {

export type SoundBankOptions = {
-- Named seeds: { footstep = "rbxassetid://123", impact = "456" }.
-- Provide these or `playlist_id`, not both.
seeds: { [string]: string }?,
-- Build the bank from a sound bank curated in the portal. Each slot becomes
-- a pool of exactly the sounds chosen for it, so the sounds a game plays can
-- change without shipping code.
playlist_id: string?,
seeds: { [string]: string },
-- "sfx" (default) expands through the sound-effects catalog; "music"
-- through the music catalog.
kind: string?,
Expand Down Expand Up @@ -97,13 +91,7 @@ end
function AudioScapeSoundBank.new(catalog: CatalogClient, options: SoundBankOptions): SoundBankInstance
assert(type(options) == "table", "createSoundBank requires an options table")

local playlistId = options.playlist_id
if playlistId ~= nil then
assert(type(playlistId) == "string" and #playlistId > 0, "createSoundBank playlist_id must be a non-empty string")
assert(options.seeds == nil, "createSoundBank takes seeds or playlist_id, not both")
else
assert(type(options.seeds) == "table", "createSoundBank requires a seeds table or a playlist_id")
end
assert(type(options.seeds) == "table", "createSoundBank requires a seeds table")

local kind = options.kind or "sfx"
assert(kind == "sfx" or kind == "music", 'createSoundBank kind must be "sfx" or "music"')
Expand All @@ -130,7 +118,6 @@ function AudioScapeSoundBank.new(catalog: CatalogClient, options: SoundBankOptio
_catalog = catalog,
_seeds = seeds,
_names = names,
_playlistId = playlistId,
_kind = kind,
_mode = mode,
_poolSize = math.max(1, options.poolSize or DEFAULT_POOL_SIZE),
Expand Down Expand Up @@ -259,70 +246,7 @@ end
-- already decided what belongs in each slot, and quietly adding sounds they
-- never reviewed is the same mistake as auto-refreshing their gunshot. Widening
-- a slot is a decision made in the portal, where they can hear it first.
function AudioScapeSoundBank._resolveFromPlaylist(self: any): (boolean, string?)
local result, err = self._catalog:getPlaylist({
playlist_id = self._playlistId,
playerId = self._playerId,
})
if not result then
return false, err or "could not load sound bank " .. tostring(self._playlistId)
end

local slots = if type(result.playlist) == "table" then result.playlist.slots else nil
if type(slots) ~= "table" then
return false, "playlist " .. tostring(self._playlistId) .. " returned no slots"
end

local pools: { [string]: SoundBankPool } = {}
local names: { string } = {}
for _, slot in slots do
local label = if type(slot) == "table" then slot.label else nil
local assetIds = if type(slot) == "table" then slot.asset_ids else nil
if type(label) == "string" and #label > 0 and type(assetIds) == "table" and #assetIds > 0 then
local assets: { string } = {}
for _, raw in assetIds do
local id = normalizeId(tostring(raw))
if id then
table.insert(assets, id)
end
end
if #assets > 0 then
pools[label] = {
name = label,
-- The first sound in the slot stands in as the seed, so the
-- pool shape matches the seeded path for every consumer.
seed_asset_id = assets[1],
source = "playlist",
assets = assets,
}
table.insert(names, label)
end
end
end

if #names == 0 then
return false, "playlist " .. tostring(self._playlistId) .. " has no usable slots"
end

table.sort(names)
self._names = names
self.Pools = pools
self.IsResolved = true

self._catalog:trackCustom("audio_resolve", nil, self._playerId, {
source = "playlist",
playlist_id = self._playlistId,
slots = #names,
})

return true
end

function AudioScapeSoundBank.resolveAsync(self: any): (boolean, string?)
if self._playlistId then
return self:_resolveFromPlaylist()
end

local known, err = self:_classifySeeds()
if err then
return false, err
Expand Down
15 changes: 3 additions & 12 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -414,13 +414,6 @@ export type PlaylistOptions = {
playerId: number?,
}

-- A slot of interchangeable sounds. Sound effects fire on events rather than
-- playing in sequence, so an SFX playlist is grouped by slot, not ordered.
export type PlaylistSlot = {
label: string,
asset_ids: { string },
}

export type PlaylistInfo = {
id: string,
name: string,
Expand All @@ -430,11 +423,11 @@ export type PlaylistInfo = {
-- "music", "sfx", or "mixed". Absent on responses from before SFX support.
kind: string?,
sound_count: number?,
slots: { PlaylistSlot }?,
}

-- A sound in a playlist. Same shape as an sfxSearch result plus the two fields
-- that only mean something inside a playlist.
-- A sound in a playlist. Same shape as an sfxSearch result plus its position.
-- SFX playlists are flat: a pool of variants for one sound is what an Audio
-- Pack is for, and grouping by role is what having more than one playlist is for.
export type PlaylistSound = {
asset_id: string,
name: string,
Expand All @@ -454,8 +447,6 @@ export type PlaylistSound = {
true_peak_dbtp: number?,
play_count: number,
like_count: number,
-- The slot this sound belongs to; sounds sharing a label are variants.
label: string?,
position: number,
}

Expand Down
80 changes: 39 additions & 41 deletions tests/openCloud/smokeScript.luau
Original file line number Diff line number Diff line change
Expand Up @@ -360,17 +360,21 @@ step("getPlaylist (sfx)", function()
assert(r.playlist.kind == "sfx", "kind is sfx; got " .. tostring(r.playlist.kind))
assert(r.sounds and #r.sounds > 0, "sounds returned")
assert(#r.tracks == 0, "an sfx playlist carries no music tracks")
assert(r.playlist.slots and #r.playlist.slots > 0, "slots returned")

-- Every sound belongs to a slot, and the slots account for every sound.
local inSlots = 0
for _, slot in r.playlist.slots do
assert(#slot.asset_ids > 0, "slot '" .. tostring(slot.label) .. "' is not empty")
inSlots += #slot.asset_ids
-- SFX playlists are flat. Assert the grouping layer is really gone, not
-- merely unused: a `slots` field reappearing means the API regressed.
assert(r.playlist.slots == nil, "no slots field on a flat sfx playlist")
assert(r.playlist.sound_count == #r.sounds, "sound_count matches the sounds returned")

-- Each sound carries the fields a caller actually plays it with, and no
-- slot/label field.
for _, sound in r.sounds do
assert(type(sound.asset_id) == "string" and #sound.asset_id > 0, "sound has an asset_id")
assert(type(sound.position) == "number", "sound has a position")
assert(sound.label == nil, "sound carries no slot label")
end
assert(inSlots == #r.sounds, "slots cover every sound: " .. tostring(inSlots) .. " vs " .. tostring(#r.sounds))

return { id = r.playlist.id, slots = #r.playlist.slots, sounds = #r.sounds }
return { id = r.playlist.id, sounds = #r.sounds }
end)

-- Analytics: enqueue one event per public track method, force-flush, assert
Expand Down Expand Up @@ -581,51 +585,45 @@ step("auditAudio", function()
)
end)

-- A bank built from a portal-curated sound bank. The slots are used exactly as
-- chosen, so this asserts the pools match what the playlist declared rather than
-- whatever similarity would have produced.
step("createSoundBank (from playlist)", function()
-- The portal-playlist source is gone. What replaces it: read the playlist and
-- seed a bank from a sound in it. Asserts both halves against the real API —
-- that the removed option is genuinely rejected, and that the flat path works.
step("createSoundBank (playlist source removed)", function()
local id = fixtures.sfx_playlist_id
if not id then
return "skipped — no sfx playlists for this key"
end

local rejected = pcall(function()
client:createSoundBank({ playlist_id = id })
end)
assert(not rejected, "playlist_id is no longer a sound-bank source")

local declared, err = client:getPlaylist({ playlist_id = id })
assert(declared, "getPlaylist failed: " .. tostring(err))
assert(#declared.sounds > 0, "playlist has sounds to seed from")

local bank = client:createSoundBank({ playlist_id = id })
-- The flat replacement: one sound from the playlist becomes the seed, and
-- the bank expands it through the catalog.
local seedId = declared.sounds[1].asset_id
local bank = client:createSoundBank({ seeds = { fromPlaylist = seedId } })
local ok, resolveErr = bank:resolveAsync()
assert(ok, "resolveAsync from playlist failed: " .. tostring(resolveErr))

local slots = 0
for _, slot in declared.playlist.slots do
slots += 1
local pool = bank:getPool(slot.label)
assert(
#pool == #slot.asset_ids,
"slot '" .. slot.label .. "' pool is " .. tostring(#pool) .. ", declared " .. tostring(#slot.asset_ids)
)
for i, assetId in slot.asset_ids do
assert(pool[i] == assetId, "slot '" .. slot.label .. "' differs at index " .. tostring(i))
end
assert(
bank.Pools[slot.label].source == "playlist",
"source should be 'playlist', got " .. tostring(bank.Pools[slot.label].source)
)

-- Picking must still avoid immediate repeats on a curated pool.
if #pool > 1 then
local previous = nil
for n = 1, 20 do
local pick = bank:pick(slot.label)
assert(pick, "pick returned nil at " .. tostring(n))
assert(pick ~= previous, "immediate repeat at " .. tostring(n))
previous = pick
end
assert(ok, "resolveAsync from a playlist sound failed: " .. tostring(resolveErr))

local pool = bank:getPool("fromPlaylist")
assert(#pool > 0, "pool is not empty")

if #pool > 1 then
local previous = nil
for n = 1, 20 do
local pick = bank:pick("fromPlaylist")
assert(pick, "pick returned nil at " .. tostring(n))
assert(pick ~= previous, "immediate repeat at " .. tostring(n))
previous = pick
end
end

return string.format("slots=%d", slots)
return string.format("seed=%s pool=%d", tostring(seedId), #pool)
end)

-- createSoundBank. The bridging path calls AssetService:GetAudioMetadataAsync,
Expand Down
Loading