- Seed lists on
similarandsfxSimilar— pass a list instead of one asset (AudioScape:similar(playlist.tracks, { limit = 20 }), or{ asset_ids = { ... }, limit = 20 }) and results sound like the set as a whole rather than any one track: playlist continuation for music, variety packs for SFX. Each element can be anything the single-seed form accepts (asset_id string, Track,Sound,AudioPlayer); up to 100 seeds; seeds are never returned.result.meta.seeds_usedcounts the seeds that resolved andresult.meta.seeds_ignoredlists the ids skipped (unknown, not yet analyzed, or not public). Available throughAudioScapeClient.similar/sfxSimilartoo.
AudioScapeClient.similarandsfxSimilarnow honourmax_scoreanddedupe. The client methods sent both, but the server-side remotes dropped them before calling the API, so near-duplicate filtering never applied to client-initiated calls.
getPlaylistandlistPlaylistsnow use GET — the requests carry the same parameters in the query string and hit the same endpoint, but GET lets CloudFront cache the response at the edge (60s TTL). When many servers boot against the same playlist at once, they now share one origin request instead of each hitting the API. No call-site changes; POST remains accepted server-side for older SDK versions.
- SFX sound banks now recognise SFX-only seeds.
createSoundBank({ kind = "sfx" })classified its seeds through the music-catalog lookup, so a seed that exists only in the SFX catalog was treated as unknown and its pool collapsed to the seed alone (source = "none"). SFX seeds are now classified through/v1/sfx/similar, which supplies the pool in the same request, so resolving an SFX bank also makes one request fewer.
- Lane types on track structure — when
include_metadata = true, each section/phrase now also carrieslane(its named parallel timeline row) and the result a top-level orderedlanesarray. This release only adds the Luau type annotations and docs — the fields already flow through at runtime on v0.20.0.
-
Browse by game —
AudioScape:browse({ type = "game" })lists Roblox experiences by the catalog music heard in them (ordered by player count, minimum 5 tracks). Drill into a game's tracks withname = "<universe_id>", or reverse-look-up the games a track has been heard in withasset_id = "<id>". NewBrowseGameItemtype carriesuniverse_id,name,creator_name,root_place_id,playing,visits, andtrack_count; build icons withrbxthumb://type=GameIcon&id=<universe_id>. The game→track mapping refreshes weekly;playing/visitsreflect the last sync, not live CCU. -
Section metadata on track structure —
AudioScape:getStructure({ asset_id = ..., include_metadata = true })returns each section/phrase's authored key/value pairs verbatim in a newmetadatafield: custom cue parameters (lighting amounts, movement paths, easing styles) beyond the flattenedlabel/energy/barsfields. Entries without authored pairs return an empty table. The metadata variant caches independently of the plain structure, sobeatAtTime/sectionAtTimeare unaffected. Also available throughAudioScapeClient.getStructurewith the same flag.
- Corrected the
sort = "popular"documentation: onlygenreandmooddrill-downs omit tracks with no engagement;artist,album, andgameinclude them, sorted last.
-
createSoundBank({ playlist_id = ... })is gone, along with theslotsfield on the playlist response and thelabelfield 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
pickhad 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:
local bank = AudioScape:createSoundBank({ seeds = { footstep = "rbxassetid://1837879082" }, }) bank:resolveAsync() sound.SoundId = "rbxassetid://" .. bank:pick("footstep")
-
AudioScape:createSoundBank({ playlist_id = "..." })builds a bank from a sound bank curated in the portal. Each slot becomes a pool, so the sounds a game plays can change without shipping code:local bank = AudioScape:createSoundBank({ playlist_id = "sfx-1785256484251" }) bank:resolveAsync() sound.SoundId = "rbxassetid://" .. bank:pick("footstep")
Slots are used exactly as curated — no similarity expansion. Whoever built the bank already decided what belongs in each slot, and quietly adding sounds they never listened to is the same mistake as swapping a game's gunshot unasked. Widening a slot is a decision made in the portal, where it can be heard first.
bank.Pools[name].sourcereads"playlist"on this path.Everything else is unchanged:
pickstill avoids immediate repeats, andreportUnavailablestill drops a sound that fails to load. -
getPlaylistunderstands playlists that carry sound effects.playlist.kindis"music","sfx", or"mixed";playlist.slotsgives the grouped view; and sounds arrive in asoundslist besidetracks, kept separate so neither needs a type test before use. Sounds carry the same shape assfxSearchresults, includingsound_start_sec/sound_end_sec— trimming tosound_start_secremoves the leading silence that makes an impact feel late. -
seedsis now optional oncreateSoundBank. Passseedsorplaylist_id, not both.
-
AudioScape.setApiKey(key)is now the documented entry point — call methods on the module directly. Previously the docs created an object and called itclient, which collides with what "client" means in Roblox: the player's machine. The collision was on both axes at once — the same variable name was used for the server-side object and the LocalScript-side object in adjacent examples, and the server object's internal type was namedAudioScapeClientwhileAudioScapeClient.luauis the LocalScript module. Now there's no object to name:local AudioScape = require(ServerScriptService.Packages.AudioScape) AudioScape.setApiKey(HttpService:GetSecret("AudioScapeKey")) local result = AudioScape:search({ query = "chill lo-fi" }) local player = AudioScape:createPlayer()
setApiKeytakes a plain string orSecretuserdata, exactly likenewdid, and tolerates bothAudioScape.setApiKey(key)andAudioScape:setApiKey(key). Calling it again swaps the key without dropping queued analytics or starting a second flush loop. -
AudioScapeClientworks the same way — call methods on the module from a LocalScript; the RemoteFunctions resolve on first use. -
Nothing is removed.
AudioScape.new(key)andAudioScapeClient.new()still work and are still the right call when one server needs more than one API key, or when you want the client's remotes resolved eagerly so a missingenableClientAccess()fails at startup rather than at first search. Existing integrations are unaffected. -
The README's "Client Access" section is now "Calling from LocalScripts", and "client" throughout the docs now only ever means the Roblox client.
AudioScape:createSoundBank(options)— expand one asset ID into a pool of related ones. Solves the "machine gun" effect where a repeated footstep or impact plays the identical clip every time. Seed it with the asset you already use;bank:pick(name)returns an asset from its neighbourhood and never the same one twice in a row.- Your asset is never silently swapped. The default
mode = "extend"keeps your seed at the head of its own pool and adds neighbours around it.mode = "replace"builds from neighbours only and has to be asked for explicitly. - Seeds we don't have still work. If a seed isn't in the catalog, the bank reads
AssetService:GetAudioMetadataAsync— which returns title and artist for any Roblox audio ID — and searches on that to find an anchor.bank.Pools[name].sourcereportscatalog,bridged, ornone.nonedegrades to just your seed, so you're never worse off than before. - Resolve once at startup, then every
pickis a local table lookup. Roblox caps a server at 500 HTTP requests/minute; resolving per-play would exhaust that almost immediately. Seed classification batches 100 per request and metadata bridging batches 30, matching the respective API limits. bank:reportUnavailable(assetId)drops an asset that failed to load from future picks. Detection is inherently client-side — a headless server never fetches audio, soGetAssetFetchStatusstaysNonethere. If a whole pool becomes unavailable,pickfalls back to your seed rather than returningnil.- Picks aggregate:
flushPickCounts()emits oneaudio_pickevent per distinct asset with acount, rather than one event per pick, which would overrun the 500-event analytics buffer during any footstep loop.resolveAsync()emitsaudio_resolveper seed;reportUnavailableemitsaudio_unavailable. - Built on the existing
similar/sfxSimilarendpoints withdedupe— near-identical uploads are the opposite of variation.
- Your asset is never silently swapped. The default
AudioScape:checkAssetHealth(assetIds, options?)— reports whether specific assets are still playable, and whether AudioScape can offer similarity for them. Statuses:ok,moderated,deleted,delisted,private,unknown. Up to 100 IDs per call, batched internally.- Two sources are combined, because neither is sufficient alone.
/v1/assets/healthknows moderation state; only the Roblox engine knows whether an asset your experience can play exists at all. An asset the engine describes (viaAssetService:GetAudioMetadataAsync) but our catalog doesn't hold is private to your experience — reported asprivaterather than lumped in with genuinely missing assets. options.reportPrivate(defaultfalse) reports detected private assets back to AudioScape, so they surface in the console under Advisor → Private audio in use, where the file can be uploaded directly and analysed. Served back only to that API key. Off by default because the result is a list of your private catalog and inferring consent from a diagnostic call isn't right; the determination is returned either way, and a failed report never fails the health check.- Private audio keeps working. Sound banks never remove your seed, so your own private assets play exactly as before; we only add public neighbours around them.
mode = "replace"now also keeps the seed unless we resolved it from our own catalog — a bridged or unresolved seed may be audio private to your experience, and a replacement might not be something your game has permission to play. What we can't do is offer similarity for a private asset, since we hold no copy; sharing access with AudioScape is what unlocks that, and it's then served back only to your API key.
- Two sources are combined, because neither is sufficient alone.
AudioScape:auditAudio(options?)— walks the place for everySoundandAudioPlayer, collects the distinct asset IDs, and reports which ones AudioScape's catalog knows about. Returns per-assetusesandsample_path(the first instance'sGetFullName()), ordered most-used first, plusmetacounts. Sounds with an emptySoundIdare counted as scanned but skipped, since an unassigned template is normal. Passrootsto narrow the scan on a large place. Read-only; intended as a startup or development diagnostic rather than something to poll.AudioScape.setEndpoints({ baseUrl, analyticsUrl })— points the SDK at a different API host for local development or staging. Previously both URLs were hardcoded with no override. Omitted fields keep their current value, trailing slashes are trimmed, and it's safe to call before or aftersetApiKey. Studio can reachhttp://localhost; a published Roblox server cannot, so this is a development affordance rather than a deployment mechanism.AudioScape:getAssetService()— a drop-in stand-in forgame:GetService("AssetService"). ExistingAssetService:SearchAudioAsync(params)call sites work unchanged: sameAudioSearchParamsin, sameAudioPagesout (GetCurrentPage,AdvanceToNextPageAsync,IsFinished), same raise-on-failure behaviour. Search runs against AudioScape's catalog, so results are matched on meaning rather than keywords.AudioSubTyperoutes the call —Musicsearches music,SoundEffectsearches SFX. The deprecatedSearchAudioalias is supported too.- Every member we don't override forwards to the real
AssetService, soGetAudioMetadataAsync,CreateEditableImage,GetBundleDetailsAsyncand the rest behave exactly as before. The shim is a complete stand-in, not a two-method object. AudioSearchParamsmapping:SearchKeyword/Title/Artist/Albumcombine into the semantic query;Tagbecomes a genre filter (music) or category filter (SFX), and doubles as the query when nothing else is set;MinDuration/MaxDurationbecome a duration filter.- Results carry every native field plus
AssetId(string form),Score,Genre,GenreSlug, andBpm.IsEndorsedis alwaysfalse— it's Roblox's own endorsement flag, which AudioScape doesn't track; the field is present so code reading it keeps working. options.fallback(defaulttrue) falls through to the realAssetService:SearchAudioAsyncwhen an AudioScape request fails, so swapping the service in can't leave a caller worse off than native.options.limit(default30) matches native's page size.- Mirrored on
AudioScapeClient(LocalScript-side) asclient:getAssetService(), over a newSearchAudioRemoteFunction.HttpServiceis server-only, so the request and the result mapping both happen server-side.
- Every member we don't override forwards to the real
AudioSearchParamssignals "unset" with sentinel values rather thannil: strings default to"",MinDurationto0,MaxDurationto2147483647(int32 max), andAudioSubTypetoMusic. Verified against a live engine; the mapping treats each sentinel as absent so no phantom filters reach the API.- Native
SearchAudioAsyncreturns 30 results per page. That isn't documented anywhere, so it's asserted in the Open Cloud smoke to catch a change.
regionoption onclient:browseandclient:sfxBrowsetrending lists. Passregionon atype = "trending"call to get a list ranked by activity from a single part of the world instead of the global list. Accepts"auto"(auto-detect from the server's location) or an explicit"americas","eu", or"apac". Omitregionfor the global list (the default), so existing call sites are unchanged. Also accepted onclient:searchfor region-scoped results.- Regional trending must be enabled for your API key. Until then,
regionis ignored and you get the global list — no error, no breakage. - Mirrored on
AudioScapeClient(LocalScript-side) over the existingBrowse,SfxBrowse, andSearchRemoteFunctions; the server forwards the field unchanged.
- Regional trending must be enabled for your API key. Until then,
sortoption onclient:searchandclient:sfxSearch. Passsort = "popular"to order results by popularity (most-played) orsort = "recent"for newest-first, instead of the default"relevance"(best semantic match). Omitsortfor relevance, so existing call sites are unchanged.result.meta.sortechoes the applied ordering. Mirrored onAudioScapeClientover theSearch/SfxSearchRemoteFunctions.
AudioScapeMusicPlayernow uses the new Roblox Audio API (AudioPlayer+AudioDeviceOutput+Wire) instead of legacySound. Public method surface is unchanged —queue,setQueue,clearQueue,play,stop,skip,setVolume,setPlayerId,playTrack,destroy, theOnTrackChanged/OnQueueFinishedcallbacks, and theNowPlaying/IsPlaying/Queuefields all behave the same. The internalAudioPlayeris now reachable via the newplayer:getAudioPlayer()accessor so consumers can wire effect chains or drive volume tweens throughTweenService.
player:getAudioPlayer()— returns the currentAudioPlayerInstance, ornilif not playing. Use forTweenService-driven volume animation (e.g. lobby-music crossfades) or for wiring custom effect chains (filters, reverbs) downstream of the player's output.PlayerOptions.output— pass a customAudioDeviceOutput/AudioEmitterInstance to opt out of the default auto-createdAudioDeviceOutput. Useful for spatial setups where the player should feed anAudioEmitteron a part instead of a global device output. When omitted, the SDK auto-creates anAudioDeviceOutputparented tooptions.parent(defaults toSoundService) for the drop-in story.
- Requires a Roblox client that supports the new audio API (
AudioPlayer/AudioDeviceOutput/Wire). No legacySoundfallback is provided. - The auto-created
AudioDeviceOutputis owned by the player and destroyed in:destroy(). A caller-providedoptions.outputis left in place on destroy — the caller owns its lifecycle.
- Internal comments on the GET helpers and call sites no longer claim the API "still accepts the equivalent POST" — that's no longer true. No public API or behavior changes.
max_scoreanddedupeoptions onclient:similarandclient:sfxSimilar. Both methods now accept two optional filters for dropping near-duplicate matches of the seed asset from the results.max_score(number, 0..1) drops results whosescorefield is at or above the threshold.dedupe(boolean) is a shortcut that enables a sensible default threshold without picking a number — most callers will reach for this. If both are set,max_scorewins.- Particularly useful with SFX libraries that contain many uploads of the same generic clip (footsteps, ticks, short impacts), where the strongest similarity matches can be exact duplicates of the seed sound.
- Accepted on the
extrasbag (client:similar(asset_id, { dedupe = true })) and on the legacy options-table form (client:similar({ asset_id = "...", max_score = 0.9 })). - Mirrored on
AudioScapeClient(LocalScript-side) over the existingSimilarandSfxSimilarRemoteFunctions; the server forwards the new fields unchanged. dedupe = falseis omitted from the wire request — no point sending the no-op default; keeps edge caches clean.
sortoption onclient:browsedrill-downs (any call withname = ...). Accepts"popular"(the new default — global popularity ranking, omits tracks with no engagement),"alpha"(track name A→Z), or"recent"(newest first). Server defaults topopularwhen omitted, so existing call sites that pass{ type = "genre", name = "..." }now return popularity-ranked tracks instead of alphabetical. Pickalphaorrecentto surface tracks that haven't accumulated engagement. Ignored for list mode (type = "genre"with noname) and fortype = "trending"(already popularity-ordered). The companion server-sideBrowseRemoteFunction handler inAudioScapeClientforwards the field unchanged.
- This release flips the default ordering for drill-down browse responses. If you were relying on alphabetical order for
client:browse({ type = "artist" | "album" | "genre" | "mood", name = ... }), passsort = "alpha"explicitly. Trending and list-mode responses are unchanged.
-
client:lookup({ asset_ids = { ... } }). Batch asset-ID lookup of up to 100 numeric strings. Filters are bypassed server-side, returned tracks preserve input order, andmeta.missing_ids: { string }?lists IDs that didn't match (deleted, delisted, non-public, never existed). Sugar overclient:search— equivalent toclient:search({ asset_ids = ... }). Mirrored onAudioScapeClient(LocalScript-side) over the existingSearchRemoteFunction; the server-side handler now accepts theasset_ids-only shape too. -
asset_idsoption onclient:search. Same wire path asclient:lookup; use this when you already have aclient:searchcall site and want to reuse it for a known-ID lookup.querybecomes optional whenasset_idsis set; one or the other is required. The single-numeric-string and comma-delimited-numeric-string short-circuits onquery(e.g.query = "12345"orquery = "12345,67890") keep working — the explicitasset_idsarray is the recommended path because it never falls through to text search on miss. -
Track.genre_slugandPlaylistTrack.genre_slug. URL-safe identifier paired withgenre(e.g.{ genre = "Hip Hop / Rap", genre_slug = "hip-hop-rap" }). Round-trippable intoclient:browse({ type = "genre", name = ... })orSearchOptions.filters.genreswithout URL-encoding. -
SearchResult.meta.missing_ids: { string }?. Only present on asset-ID lookup responses (single viaquery, batch viaasset_ids). Omitted when every requested ID resolved. -
SearchResult.meta.search_methodcan now also be"id"(single-ID short-circuit onquery) or"ids-batch"(explicitasset_ids) in addition to the existing"semantic"/"text". -
AudioScape.BrowseGenreItemtyped export for items returned byclient:browse({ type = "genre" }). Fields:name(canonical),slug(URL-safe),display_name,track_count.BrowseListResult.itemsitself stays permissively typed ({ [string]: any }) so artist/album/mood items aren't forced through a discriminated union; consumers cast genre items manually:local result = client:browse({ type = "genre" }) for _, item in result.items do local genre = item :: AudioScape.BrowseGenreItem print(genre.display_name, "→", genre.slug) end
track.genreis now uniformly the AudioScape canonical taxonomy across every endpoint. Previouslyclient:search,client:similar, andclient:getPlaylistreturned the lowercase Robloxmusic_genreslug (e.g."electronic") whileclient:browsereturned the canonical name (e.g."Electronic"). Now every track-returning endpoint returns the canonical form (e.g."Hip Hop / Rap","Electronic","R&B / Soul / Funk"). Heads-up: code that comparestrack.genreagainst a lowercase string (e.g.if track.genre == "electronic" then ...) needs to update — either compare against the canonical form, or compare againsttrack.genre_slug(new in this release). Code that displaystrack.genreas a label only sees prettier output, no change required.SearchOptions.filters.genresandSimilarOptions.filters.genresaccept any of four shapes, all resolved server-side to canonical. The shapes: AudioScape canonical ("Hip Hop / Rap"), AudioScape URL-safe slug ("hip-hop-rap", the newtrack.genre_slug), off-case canonical ("hip hop / rap"), or the legacy Robloxmusic_genreslug ("hip-hop"). Existing call sites continue to work; new code can round-triptrack.genre_slugstraight back in.client:browse({ type = "genre" })items now exposeslugalongsidename/display_name/track_count. Drill-downs (client:browse({ type = "genre", name = ... })) accept the slug, the canonical name, or off-case canonical — all resolve to the same set of tracks.
- Three previously-hidden genres now appear in the
client:browse({ type = "genre" })list response: Hardstyle, Breakcore, and Jersey Club. Filter-side and drill-down support was already in place; this release only changes their visibility in the browse list. - The
client:searchvalidation message changed: was"search requires a non-empty query string", now"search requires either a non-empty query string or a non-empty asset_ids array". Code that pattern-matches on the exact error string needs updating.
- Polymorphic input on
client:similar,client:sfxSimilar,client:getStructure. Each method now accepts an asset_id string ("1234"or"rbxassetid://1234"), a Track / PlaylistTrack / SfxTrack table from a previous response, aSoundinstance (peelsSoundId), or anAudioPlayerinstance (peelsAsset). The legacy options-table form ({ asset_id, limit, ... }) keeps working unchanged. New optional second positional argextrascarrieslimit/offset/filters/playerIdfor the non-table forms (client:similar(sound, { limit = 10 })). Resolves the "I just got a track from getPlaylist, why am I parsing it back into asset_id" papercut. CompanionAudioScapeClient(LocalScript-side) gets the same treatment — wire format unchanged, resolution happens client-side. tests/install-snippet.spec.luauLune doctest — reads README.md and wally.toml at test time and asserts (1) the install snippet uses[server-dependencies], not[dependencies], and (2) the version pin in the README matcheswally.toml. Catches the install-bug class going forward without needing a Wally consumer-fixture (Wally 0.3.2 has no path-dep support).
- README install snippet now correctly uses
[server-dependencies]. The SDK declaresrealm = "server"; Wally'sRealm::is_dependency_validrejects server-realm packages placed under[dependencies](the shared section). Consumers following the old README copy hit an install-time error. Reported by Juriaan (Velibor). Thanks Juriaan!
audioPlayer.Assetis the auto-resolved field on AudioPlayer in v0.11.0 (the legacy ContentId, still widely used). The newer.AudioContentis aContentuserdata and isn't auto-unwrapped — passaudioPlayer.AudioContent.Uriyourself for that path. Will revisit in a future release if customers ask.
client:similar(...)andclient:sfxSimilar(...)now hit the API over GET instead of POST — same migration asbrowse/sfxBrowse/getSfxTaxonomyin v0.10.0, same caching rationale (CloudFront edge cache, ~30 ms warm-cache reads). Public method signatures, options, and return shapes are unchanged. The API still accepts the equivalent POST for older SDK clients during migration; no breakage from upgrading. The internalbuildQuery()helper now JSON-encodes table values (e.g.filters) into a single querystring parameter
Section.endfield annotation now uses["end"]quoting —endis a reserved Luau keyword, and the unquoted form preventedrequire()of the SDK from parsing on any Luau analyzer or runtime. Affected every release fromv0.7.0(whengetStructureshipped) throughv0.10.0. Consumers reading the field must usesection["end"](the API wire format already returnsend, so this matches what's on the response). Reported in #3 by Sean (Rhythm Run / OffGridDude). Thanks Sean!- Tightened a few pre-existing type-narrowing holes in
AudioScapeMusicPlayerandAudioScape.createPlayerthat the new analyzer surfaced — no behavior change
- New QA toolchain: StyLua (format), selene (lint),
luau-lsp analyze(parse + type check), Lune unit tests, and an Open Cloud Luau Execution smoke suite — all wired into a GitHub Actions CI job that runs on every PR. The SDK module is nowrequire-checked end-to-end on every push, so the issue #3 class of bug can't ship again - Rokit (
rokit.toml) pins every tool version so contributors get the same toolchain as CI - 25 checks total: 10 Lune unit specs + 15 Open Cloud smoke steps. The smoke suite runs the SDK inside the real Roblox engine via Open Cloud Luau Execution — every HTTP-hitting public method plus engine-only paths (
Sound.Endedconnect,RemoteFunctiontree, realworkspace:GetServerTimeNow) exercised against the live API in ~5s. Dogfooded fixtures (IDs discovered at test time via the SDK's own browse/sfxBrowse endpoints) mean no env-var maintenance for assets. AudioScape key is retrieved viaHttpService:GetSecret— never materialises as a plain string in source - CI smoke job is gated behind a GitHub environment (
openCloud-smoke) with required-reviewer protection — a maintainer must approve each run before the Roblox/AudioScape secrets are exposed to the runner - See
CONTRIBUTING.mdfor local setup + the manual Studio checklist for audio playback and BindToClose shutdown flush (Luau Execution doesn't fire BindToClose callbacks)
client:browse(...),client:sfxBrowse(...), andclient:getSfxTaxonomy()now hit the API over GET instead of POST. Public method signatures, options, and return shapes are unchanged — but responses are now eligible for CloudFront edge caching, so identical calls served from a nearby edge return in ~30 ms anywhere globally instead of round-tripping to us-east-2 every time. Cache keys are per-API-key, so customers stay isolated. The API still accepts the equivalent POST for SDK <0.10.0 clients during migration; no breakage from upgrading- TTL at the edge is 5 min default / 1 hour ceiling. Trending lists and canonical-genre drill-downs are invalidated automatically when the underlying refresh crons run, so changes surface within seconds rather than waiting on TTL expiry
- New
requestGet()helper alongsiderequest()(POST). Asymmetric naming is intentional and isolated; the two will fold into a single method-aware helper at v1.0.0
examples/TrendingLobbyJukebox.luau— daily-trending music piped throughAudioScapeMusicPlayerwith a now-playing labelexamples/TrendingSfxBoard.luau— lobby sound board built fromclient:sfxBrowse({ type = "trending" })
- README install snippet bumped to
0.9.1; trending music description tightened - CHANGELOG copy polish on the v0.9.0 entry
client:browse({ type = "trending", limit, offset })— popularity-ranked list of music tracks. Nonamerequired; returns{ tracks, meta }directly. Refreshed daily; player engagement signals (plays, favorites, votes, queue adds, listen duration, plus custom events) are exponentially decayed over a 60-day windowclient:sfxBrowse({ type = "trending", limit, offset })— popularity-ranked list of sound effects. Mirrorsbrowse()but returnsSfxTrack-shaped entries from the SFX catalogAudioScapeClient.sfxBrowse(options)exposed on the client-side companion (RemoteFunctionSfxBrowse)- New types:
SfxBrowseOptions,SfxBrowseResult
client:sfxSearch({ query?, filters?, limit?, offset? })— search the sound effects catalog. Pass a free-textquery, or passfilters.categories(with optionalfilters.subcategories) to browse a category — the API injects a curated query under the hoodclient:sfxSimilar({ asset_id, filters?, limit?, offset? })— find sound effects acoustically similar to a given assetclient:getSfxTaxonomy()— fetch the broader-category → category → subcategory hierarchy for building SFX picker UIs (10-minute server cache, so polling is cheap)AudioScapeClient.sfxSearch(options),AudioScapeClient.sfxSimilar(options), andAudioScapeClient.getSfxTaxonomy()exposed on the client-side companion (RemoteFunctionsSfxSearch,SfxSimilar,GetSfxTaxonomy)- New types:
SfxSearchOptions,SfxSimilarOptions,SfxTrack,SfxSearchResult,SfxSimilarResult,SfxTaxonomyResult,SfxTaxonomyGroup,SfxTaxonomyCategory
client:getStructure({ asset_id })— fetch beat grid + section/phrase structure for a track. Response includesbpm,track_energy, downbeats, and labelled sections (Intro/Verse/Chorus/Drop/Bridge/Climax/Outro/etc.) with energy 1–4client:beatAtTime(asset_id, t)— find the closest beat to a given time. Useful for syncing animations or VFXclient:sectionAtTime(asset_id, t, level?)— find the section (or phrase) covering a time. Lets you trigger different effects on Verse vs DropAudioScapeClient.getStructure(options)exposed on the client-side companion (RemoteFunctionGetStructure)- New types:
StructureOptions,StructureResult,BeatGrid,Section,BeatHit - Structure responses are cached client-side per
asset_idso repeatedbeatAtTime/sectionAtTimecalls are free
SearchOptions.filtersextended withmin_play_count,min_likes, andcreated_afterfor popularity and recency filteringSimilarOptions.filtersadded — supportsgenresandduration(mirrors search filter shape)
- Analytics events are no longer silently dropped on retryable HTTP errors (429, 5xx) — they are re-queued for the next flush
- Added exponential backoff to the analytics flush loop (30s → 60s → 120s → … capped at 5 min), resets on success
flushAnalytics()andgame:BindToCloseflush no longer infinite-loop when the server is unreachable- Re-queued events now respect
maxQueueSizeinstead of growing unboundedly
client:getPlaylist(options)— fetch a configured station playlist and its tracksclient:listPlaylists(playerId?)— list all playlists configured for your API key- New types:
PlaylistOptions,PlaylistInfo,PlaylistTrack,PlaylistResult,ListPlaylistsResult - Example:
PlaylistStation.luau— fetch and play a configured station playlist
- Analytics batching and automatic flush on game close
client:configureAnalytics(config)for tuning batch settingsclient:flushAnalytics()for on-demand flush- Event tracking methods:
trackPlay,trackStop,trackSkip,trackVote,trackFavorite,trackUnfavorite,trackAddToQueue,trackSearchClick,trackCustom
- Resolved edge case where
HttpService:RequestAsynccould silently drop headers
client:browse(options)— browse by artist, album, genre, or moodclient:similar(options)— find similar tracks by asset ID
- Initial release
AudioScape.new(apiKey)client constructorclient:search(options)— natural language music search