From 723dcc72ace0cf095fa2efadde2458c810ed9289 Mon Sep 17 00:00:00 2001 From: Filipe Herculano Date: Tue, 28 Jul 2026 18:07:26 -0400 Subject: [PATCH] =?UTF-8?q?v0.19.0=20=E2=80=94=20SFX=20playlists=20are=20f?= =?UTF-8?q?lat,=20playlist-sourced=20banks=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `createSoundBank({ playlist_id = ... })` is gone, along with the `slots` field on the playlist response and the `label` field on each playlist sound. The slot layer was never used. Across the whole catalog only two playlists ever carried a slot label and both were tests — every real SFX playlist is flat, and organises by having more than one playlist ("ObbySounds: Hits", "LittleMonsters-Vocalizations", "BigScreech"), where the playlist name is the role. The mode was also inert by construction: an unlabelled sound became its own slot, so a flat playlist resolved to pools of exactly one and `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 of neighbours. The Open Cloud smoke keeps integration coverage on both halves: it asserts the `slots` field is genuinely absent (a regression would bring it back) and that the replacement path works end to end — read an SFX playlist, seed a bank from one of its sounds, resolve it against the real catalog and pick without repeats. --- CHANGELOG.md | 21 +++++ README.md | 17 +--- src/AudioScapeSoundBank.luau | 80 +----------------- src/init.luau | 15 +--- tests/openCloud/smokeScript.luau | 80 +++++++++--------- tests/soundBank.spec.luau | 134 ++----------------------------- wally.toml | 2 +- 7 files changed, 73 insertions(+), 276 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584d685..6aec017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 959a41f..c33f98c 100644 --- a/README.md +++ b/README.md @@ -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: @@ -445,7 +445,6 @@ 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 | @@ -453,20 +452,6 @@ 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. `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 diff --git a/src/AudioScapeSoundBank.luau b/src/AudioScapeSoundBank.luau index b18698e..0c8bc6a 100644 --- a/src/AudioScapeSoundBank.luau +++ b/src/AudioScapeSoundBank.luau @@ -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?), @@ -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?, @@ -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"') @@ -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), @@ -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 diff --git a/src/init.luau b/src/init.luau index b9bf020..3c28343 100644 --- a/src/init.luau +++ b/src/init.luau @@ -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, @@ -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, @@ -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, } diff --git a/tests/openCloud/smokeScript.luau b/tests/openCloud/smokeScript.luau index 2730fab..1ecd20e 100644 --- a/tests/openCloud/smokeScript.luau +++ b/tests/openCloud/smokeScript.luau @@ -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 @@ -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, diff --git a/tests/soundBank.spec.luau b/tests/soundBank.spec.luau index 9f7e639..8a885da 100644 --- a/tests/soundBank.spec.luau +++ b/tests/soundBank.spec.luau @@ -313,136 +313,14 @@ do assert(not okSeed, "unusable seed id rejected") end --- 14. A bank sourced from a portal-curated playlist uses each slot exactly as --- chosen. Quietly expanding a slot with similar sounds would put audio the --- developer never reviewed into their game — the same mistake as --- auto-refreshing a gunshot. +-- 14. Seeds are the only source. The portal-playlist mode is gone: SFX +-- playlists are flat, so a "slot" was either the whole playlist or a pool of +-- one, and variation for a single sound belongs to an Audio Pack. do - local http = mockHttp.new() - local sdk = loadSdk({ http = http }) - local AudioScape: any = sdk - AudioScape.setApiKey("test-key") - - http:queueResponse({ - Success = true, - StatusCode = 200, - Body = serde.encode("json", { - playlist = { - id = "sfx-123", - name = "Bank", - genre = "", - playback_mode = "order", - track_count = 0, - sound_count = 4, - kind = "sfx", - slots = { - { label = "footstep", asset_ids = { "111", "222", "333" } }, - { label = "impact", asset_ids = { "444" } }, - }, - }, - tracks = {}, - sounds = {}, - meta = { total = 4 }, - }), - }) - - local bank = AudioScape:createSoundBank({ playlist_id = "sfx-123" }) - local ok, err = bank:resolveAsync() - assert(ok, "resolve from playlist failed: " .. tostring(err)) - assert(#http.requests == 1, "one request — the playlist fetch; got " .. tostring(#http.requests)) - - local footstep = bank:getPool("footstep") - assert(#footstep == 3, "the slot is the pool, verbatim; got " .. tostring(#footstep)) - assert(footstep[1] == "111", "order preserved; got " .. tostring(footstep[1])) - assert(#bank:getPool("impact") == 1, "single-sound slot stays a single-sound pool") - assert(bank.Pools.footstep.source == "playlist", "source reports 'playlist'") - assert(bank.IsResolved, "IsResolved set") -end - --- 15. Picking still refuses immediate repeats on a playlist-sourced pool. -do - local http = mockHttp.new() - local sdk = loadSdk({ http = http }) - local AudioScape: any = sdk - AudioScape.setApiKey("test-key") + local AudioScape = newSdk() - http:queueResponse({ - Success = true, - StatusCode = 200, - Body = serde.encode("json", { - playlist = { - id = "sfx-9", - name = "B", - genre = "", - playback_mode = "order", - track_count = 0, - slots = { { label = "step", asset_ids = { "1", "2", "3" } } }, - }, - tracks = {}, - meta = { total = 3 }, - }), - }) - - local bank = AudioScape:createSoundBank({ playlist_id = "sfx-9" }) - bank:resolveAsync() - - local previous: string? = nil - for i = 1, 100 do - local pick = bank:pick("step") - assert(pick, "pick returned nil at " .. tostring(i)) - assert(pick ~= previous, "immediate repeat at " .. tostring(i)) - previous = pick - end - - -- reportUnavailable still heals a curated pool. - assert(bank:reportUnavailable("2"), "first report returns true") - for i = 1, 40 do - assert(bank:pick("step") ~= "2", "unavailable sound never picked (" .. tostring(i) .. ")") - end -end - --- 16. Validation and failure paths for the playlist source -do - local http = mockHttp.new() - local sdk = loadSdk({ http = http }) - local AudioScape: any = sdk - AudioScape.setApiKey("test-key") - - local okBoth = pcall(function() - AudioScape:createSoundBank({ seeds = { s = "1" }, playlist_id = "sfx-1" }) - end) - assert(not okBoth, "seeds and playlist_id together are rejected") - - local okNeither = pcall(function() + local okNoSeeds = pcall(function() AudioScape:createSoundBank({}) end) - assert(not okNeither, "neither seeds nor playlist_id is rejected") - - local okEmpty = pcall(function() - AudioScape:createSoundBank({ playlist_id = "" }) - end) - assert(not okEmpty, "empty playlist_id is rejected") - - -- A playlist that can't be fetched fails the resolve rather than yielding a - -- silently empty bank that picks nil at runtime. - http:queueResponse({ Success = false, StatusCode = 404, Body = "" }) - local missing = AudioScape:createSoundBank({ playlist_id = "sfx-nope" }) - local ok404 = missing:resolveAsync() - assert(not ok404, "a 404 playlist fails the resolve") - assert(not missing.IsResolved, "and leaves the bank unresolved") - - -- A music playlist has no slots; say so rather than resolving to nothing. - http:queueResponse({ - Success = true, - StatusCode = 200, - Body = serde.encode("json", { - playlist = { id = "m", name = "M", genre = "", playback_mode = "order", track_count = 5, slots = {} }, - tracks = {}, - meta = { total = 5 }, - }), - }) - local musicBank = AudioScape:createSoundBank({ playlist_id = "music-1" }) - local okMusic, errMusic = musicBank:resolveAsync() - assert(not okMusic, "a playlist with no slots fails the resolve") - assert(tostring(errMusic):find("slots", 1, true), "error says why; got: " .. tostring(errMusic)) + assert(not okNoSeeds, "a bank with no seeds is rejected") end diff --git a/wally.toml b/wally.toml index bda8803..094cfd9 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "this-fifo/audioscape-sdk" -version = "0.18.0" +version = "0.19.0" registry = "https://github.com/UpliftGames/wally-index" realm = "server" license = "MIT"