{"id":"this-fifo/audioscape-sdk","name":"audioscape-sdk","scope":"this-fifo","platform":"roblox","description":"Luau SDK for the AudioScape Developer API — search, browse, and discover music and sound effects, manage playlists, and play audio in Roblox experiences.","version":"0.20.1","latest":"0.20.1","versions":["0.1.0","0.2.0","0.2.1","0.2.2","0.3.0","0.4.0","0.5.0","0.5.1","0.7.0","0.8.0","0.9.0","0.9.1","0.10.0","0.10.1","0.11.0","0.12.0","0.14.0","0.14.1","0.15.0","0.16.0","0.17.0","0.18.0","0.19.0","0.20.0","0.20.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"0cb76e491d37d38ea0b4bdb9951347bcb20071bb4842746d48058e4cf818b393","likes":0,"downloads":1,"install":"forest install this-fifo/audioscape-sdk","url":"https://forest.dev/p/roblox/this-fifo/audioscape-sdk","files":"https://api.forest.dev/ai/package/roblox/this-fifo/audioscape-sdk/files","readme":"# AudioScape SDK for Roblox\n\nA Luau SDK for the [AudioScape Developer API](https://developer.audioscape.ai) — search music and sound effects, browse the catalog, sync to beat-level track structure, and track analytics for your Roblox experiences.\n\n> **Note:** This SDK uses `HttpService:RequestAsync()` and must run on the **server** (Script, not LocalScript). You must enable **Allow HTTP Requests** in your experience's Game Settings → Security.\n\n## Installation\n\n### Wally\n\nAdd to your `wally.toml`:\n\n```toml\n[server-dependencies]\nAudioScape = \"this-fifo/audioscape-sdk@0.20.1\"\n```\n\nThen run:\n\n```bash\nwally install\n```\n\nThe SDK's realm is `server`, so it must be declared under `[server-dependencies]` (Wally rejects server-realm packages placed under `[dependencies]`). Wally installs it to `ServerScriptService.Packages` (or your configured server packages location).\n\n### Roblox Model\n\nDownload `AudioScape.rbxm` from the [latest release](https://github.com/AudioScapeInc/sdk/releases/latest) and drop it into `ServerStorage` or `ServerScriptService` in Roblox Studio.\n\n### Manual\n\nCopy the `src/` folder into your project under `ServerStorage` or `ServerScriptService`. If using Rojo, add it to your server-side project tree.\n\n## Prerequisites\n\n1. **Enable HTTP Requests** — In Roblox Studio, go to Game Settings → Security → Allow HTTP Requests and turn it **on**.\n2. **API Key** — Get your key at [developer.audioscape.ai](https://developer.audioscape.ai). Use the [Roblox Secrets Store](https://create.roblox.com/docs/cloud-services/secret-stores) to securely store your key in production.\n\n## Quick Start\n\n```lua\nlocal ServerStorage = game:GetService(\"ServerStorage\")\nlocal HttpService = game:GetService(\"HttpService\")\nlocal RunService = game:GetService(\"RunService\")\n\nlocal AudioScape = require(ServerStorage.AudioScape)\n\nlocal apiKey = if RunService:IsStudio()\n    then \"your-test-key\"\n    else HttpService:GetSecret(\"AudioScapeKey\")\n\nAudioScape.setApiKey(apiKey)\nlocal player = AudioScape:createPlayer()\n\n-- Search and play\nlocal result, err = AudioScape:search({ query = \"chill lo-fi beats\", limit = 10 })\nif result then\n    player:queue(result.tracks)\n    player:play()\nend\n```\n\nThe `AudioScapeMusicPlayer` handles Sound lifecycle, queue advancement, and analytics tracking automatically — no manual `trackPlay`/`trackStop` calls needed.\n\n## Telemetry\n\nThe SDK automatically sends your game's Universe ID and Place ID with every request to help you track usage across your experiences. You can also pass an optional `playerId` to tie requests to specific players:\n\n```lua\nlocal result, err = AudioScape:search({\n    query = \"epic battle music\",\n    playerId = player.UserId,\n})\n```\n\n## API\n\n### `AudioScape.setApiKey(apiKey)`\n\nPoints the module at your API key. After this, call every method on `AudioScape` itself — there's no object to create or name.\n\n```lua\nAudioScape.setApiKey(\"your-api-key\")\n```\n\nAccepts a plain string or the `Secret` userdata returned by `HttpService:GetSecret()`, which is what you should use in production so the key never exists as a string in your code.\n\n### `AudioScape.new(apiKey)` *(advanced)*\n\nReturns a separate object with its own key. You only need this if one server has to talk to AudioScape under more than one API key — otherwise use `setApiKey`.\n\n```lua\nlocal secondary = AudioScape.new(\"another-api-key\")\nlocal result = secondary:search({ query = \"chill lo-fi\" })\n```\n\n### `AudioScape.setEndpoints(options)` *(advanced)*\n\nPoints the SDK at a different API host. You only need this to develop against a locally-running API or a staging environment.\n\n```lua\nAudioScape.setEndpoints({\n    baseUrl = \"http://localhost:3000/developer\",\n    analyticsUrl = \"http://localhost:3001/analytics\",\n})\n```\n\n| Option | Type | Default |\n| --- | --- | --- |\n| `baseUrl` | `string?` | `https://api.audioscape.ai/developer` |\n| `analyticsUrl` | `string?` | `https://api.audioscape.ai/analytics` |\n\nOmitted fields keep their current value, and it's safe to call before or after `setApiKey`. Trailing slashes are trimmed.\n\n> **Note:** Roblox Studio can reach `http://localhost`, but a published Roblox server cannot. This is a development affordance, not a deployment mechanism.\n\n### `AudioScape:search(options)`\n\nSearch the catalog using natural language, or look up specific tracks by asset ID.\n\n```lua\nlocal result, err = AudioScape:search({\n    query = \"epic orchestral battle music\",  -- required (unless asset_ids set)\n    limit = 20,                              -- optional (default: 20, max: 100)\n    offset = 0,                              -- optional\n    playerId = player.UserId,                -- optional\n    filters = {                              -- optional\n        -- Each entry accepts the canonical name (\"Hip Hop / Rap\"),\n        -- the URL-safe slug (\"hip-hop-rap\", round-trip from track.genre_slug),\n        -- or a legacy Roblox music_genre slug (\"hip-hop\") — all resolve to\n        -- the same canonical taxonomy server-side.\n        genres = { \"Hip Hop / Rap\", \"Electronic\" },\n        duration = { min = 60, max = 180 },  -- seconds\n        min_play_count = 100000,             -- min lifetime Roblox plays\n        min_likes = 500,                     -- min lifetime Roblox likes\n        created_after = \"2024-01-01\",        -- YYYY-MM-DD\n    },\n})\n-- result = { tracks, artists, albums, meta }\n-- result.tracks[i] = { asset_id, name, artist, album, genre, genre_slug, duration, bpm, ... }\n```\n\n`query` is required unless you pass `asset_ids` for a direct batch lookup\n(see `AudioScape:lookup` below). Pass either one. When both are present, the API\nprefers `asset_ids` and skips text search. `result.meta.search_method` will be\n`\"semantic\"` / `\"text\"` / `\"id\"` / `\"ids-batch\"` depending on how the\nrequest resolved.\n\n**Result ordering:** pass `sort = \"popular\"` to rank by popularity (most-played first) or `sort = \"recent\"` for newest-first; omit `sort` (or use `\"relevance\"`) for the default best-match ordering. `result.meta.sort` echoes the applied ordering. (`sort` is ignored for `asset_ids` lookups.)\n\n### `AudioScape:lookup(options)`\n\nResolve up to 100 known asset IDs in a single request. Returned tracks are in\ninput order, filters are bypassed, and IDs that didn't match (deleted,\ndelisted, non-public, or never existed) come back in `result.meta.missing_ids`.\nSugar over `AudioScape:search({ asset_ids = ... })`.\n\n```lua\nlocal result, err = AudioScape:lookup({\n    asset_ids = { \"1843209165\", \"9120386436\", \"1234567890\" },\n    playerId = player.UserId,  -- optional\n})\n-- result = { tracks, artists, albums, meta }\n-- result.meta.missing_ids = { \"1234567890\" }  -- IDs that didn't resolve\n```\n\n### `AudioScape:similar(input, extras?)`\n\nFind tracks that sound similar to a given track. `input` is anything that\ncarries an asset_id — a string, a Track from a previous response, a `Sound`,\nor an `AudioPlayer`.\n\n```lua\nlocal result, err = AudioScape:similar({\n    asset_id = \"123456789\",      -- required\n    limit = 10,                  -- optional\n    offset = 0,                  -- optional\n    playerId = player.UserId,    -- optional\n    filters = {                  -- optional\n        genres = { \"electronic\" },\n        duration = { min = 60, max = 180 },\n    },\n})\n-- result = { tracks, meta }\n\n-- Shorthand: pass a Track from a previous result\nlocal result = AudioScape:similar(playlist.tracks[1], { limit = 10 })\n\n-- Shorthand: pass a playing Sound directly\nlocal result = AudioScape:similar(soundInstance)\n```\n\n### `AudioScape:browse(options)`\n\nBrowse by artist, album, genre, mood, trending, or game.\n\n```lua\n-- List all genres\nlocal result, err = AudioScape:browse({ type = \"genre\" })\n-- result = { items, meta }\n\n-- Get tracks for a specific genre (defaults to popularity-ranked)\nlocal result, err = AudioScape:browse({ type = \"genre\", name = \"electronic\", limit = 20 })\n-- result = { tracks, meta }\n\n-- Same drill-down, alpha sort to surface fresh uploads\nlocal result, err = AudioScape:browse({ type = \"genre\", name = \"electronic\", sort = \"alpha\", limit = 20 })\n\n-- Trending music (no name needed — returns the top tracks directly)\nlocal result, err = AudioScape:browse({ type = \"trending\", limit = 50 })\n-- result = { tracks, meta }\n```\n\n**Browse types:** `artist`, `album`, `genre`, `mood`, `trending`, `game`\n\n**Sort (drill-down only):** `popular` (default — global popularity ranking), `alpha` (track name A→Z), `recent` (newest first). Ignored for list mode and for `trending` (already popularity-ordered). Under `popular`, `genre` and `mood` omit tracks with no engagement — pick `alpha` or `recent` to surface them; `artist`, `album`, and `game` include them, sorted last.\n\nTrending is a popularity-ranked list of music tracks refreshed daily, capped at 200 entries. Player engagement signals (plays, favorites, votes, queue adds, listen duration, plus custom events) are exponentially decayed over a 60-day window with a 30-day half-life, so recent activity dominates.\n\n**Regional trending:** pass `region` on a `type = \"trending\"` call to get a list ranked by activity from a single part of the world instead of the global list. Use `region = \"auto\"` to auto-detect the region from your server's location, or pass an explicit `\"americas\"`, `\"eu\"`, or `\"apac\"`. Omit `region` for the global list (the default).\n\n```lua\n-- Auto-detected regional trending (falls back to global if unavailable)\nlocal result, err = AudioScape:browse({ type = \"trending\", region = \"auto\", limit = 50 })\n\n-- Explicit region\nlocal result, err = AudioScape:browse({ type = \"trending\", region = \"eu\", limit = 50 })\n```\n\n> Regional trending must be enabled for your API key. Until then, `region` is ignored and you get the global list. Contact us via the [Developer Portal](https://developer.audioscape.ai) to enable it.\n\n**Browse by game:** `type = \"game\"` browses Roblox experiences by the catalog music heard in them — the mapping refreshes weekly from Roblox's own music-discovery data.\n\n```lua\n-- List games with catalog music, ordered by player count (min 5 tracks)\nlocal result, err = AudioScape:browse({ type = \"game\", limit = 20 })\n-- items = { { universe_id, name, creator_name, root_place_id, playing, visits, track_count } }\n\nfor _, item in result.items do\n    local game = item :: AudioScape.BrowseGameItem\n    -- Icons come free in Roblox clients:\n    icon.Image = `rbxthumb://type=GameIcon&id={game.universe_id}&w=150&h=150`\nend\n\n-- Drill into a game's tracks (universe_id travels as a string in `name`)\nlocal tracks = AudioScape:browse({ type = \"game\", name = \"66654135\", limit = 25 })\n\n-- Reverse lookup: the games a track has been heard in\nlocal games = AudioScape:browse({ type = \"game\", asset_id = \"1841647093\" })\n```\n\n`playing` and `visits` come from the last catalog sync, not live CCU. Any mapped game resolves by `universe_id`, even below the 5-track list floor.\n\n### `AudioScape:sfxBrowse(options)`\n\nBrowse the SFX catalog. v1 only supports `type = \"trending\"` — a popularity-ranked list of sound effects, refreshed daily on the same schedule as music trending.\n\n```lua\nlocal result, err = AudioScape:sfxBrowse({ type = \"trending\", limit = 50 })\n-- result = { tracks, meta }\n-- result.tracks = { { asset_id, name, description, category, subcategory, tags, duration, ... } }\n\n-- Scope to a region (same options as music trending; requires regional\n-- trending to be enabled for your API key — otherwise the global list)\nlocal result, err = AudioScape:sfxBrowse({ type = \"trending\", region = \"auto\", limit = 50 })\n```\n\n### `AudioScape:sfxSearch(options)`\n\nSearch the sound effects catalog. Pass a free-text `query`, or browse a UCS category by passing `filters.categories` (the API synthesizes the query under the hood).\n\n```lua\nlocal result, err = AudioScape:sfxSearch({\n    query = \"metal sword impact short\",  -- optional if filters.categories is set\n    limit = 20,                          -- optional (default: 20, max: 100)\n    offset = 0,                          -- optional\n    playerId = player.UserId,            -- optional\n    filters = {                          -- optional\n        categories = { \"WEAPON\" },       -- UCS category names\n        subcategories = { \"SWORD\" },     -- UCS subcategories\n        duration = { min = 0, max = 1 }, -- seconds\n        min_likes = 100,                 -- min lifetime Roblox likes\n        created_after = \"2024-01-01\",    -- YYYY-MM-DD\n    },\n})\n-- result = { tracks, categories, subcategories, meta }\n```\n\n**Result ordering:** like music search, pass `sort = \"popular\"` (most-popular first) or `sort = \"recent\"` (newest first); omit for the default relevance ordering.\n\n### `AudioScape:sfxSimilar(input, extras?)`\n\nFind sound effects acoustically similar to a given asset. Same polymorphic\ninput as `AudioScape:similar`.\n\n```lua\nlocal result, err = AudioScape:sfxSimilar({\n    asset_id = \"9120386436\",     -- required\n    limit = 10,                  -- optional\n    offset = 0,                  -- optional\n    playerId = player.UserId,    -- optional\n    filters = {                  -- optional\n        categories = { \"AMBIENCE\" },\n        duration = { min = 0, max = 5 },\n    },\n})\n-- result = { tracks, meta }\n\n-- Or pass an SfxTrack / Sound / asset_id string:\nlocal result = AudioScape:sfxSimilar(sfx.tracks[1], { limit = 10 })\n```\n\n### `AudioScape:getSfxTaxonomy()`\n\nFetch the full `broader_category → category → subcategory` hierarchy for building SFX picker UIs. Server-cached for 10 minutes, so polling is cheap.\n\n```lua\nlocal taxonomy, err = AudioScape:getSfxTaxonomy()\n-- taxonomy.taxonomy = { { broader_category, categories = { { category, subcategories = { string } } } } }\n```\n\n### `AudioScape:getStructure(input, extras?)`\n\nFetch the beat grid and section structure for a track. Use this to sync\nanimations, lighting, or VFX to the music. Same polymorphic input as\n`AudioScape:similar` — pass a string, a Track, a `Sound`, or an `AudioPlayer`.\n\n```lua\nlocal structure, err = AudioScape:getStructure({\n    asset_id = \"1843209165\",  -- required\n})\n-- structure = { asset_id, duration, bpm, track_energy, beat_grid, sections, phrases }\n-- structure.beat_grid = { times = { number }, downbeats = { number } }\n-- structure.sections = { { start, end, label, energy, bar_start, bar_end, color } }\n-- structure.phrases  = same shape as sections, but a coarser layer — fewer,\n--                      longer segments. sections are the finer layer (more,\n--                      shorter). Both carry the same label set.\n\n-- Or pull structure straight from the playing Sound:\nlocal structure = AudioScape:getStructure(soundInstance)\n```\n\n`label` values come from: `Intro`, `Verse`, `Chorus`, `Drop`, `Bridge`, `Climax`, `Outro`, `Main`, `Break`, `Build`, `Breakdown`, `Transition`, `Peak`. `energy` is `1`–`4`.\n\n**Section metadata & lanes:** pass `include_metadata = true` and every section/phrase carries its authored key/value pairs verbatim in `metadata` — custom cue parameters (lighting amounts, movement paths, easing styles) beyond the flattened fields (empty table when none) — plus a `lane` name grouping entries into parallel timeline rows, with the ordered distinct names in a top-level `lanes` array. Entries sharing a lane string belong to the same row; time-overlapping entries in different lanes run concurrently. Drive custom events straight from authored cue sections:\n\n```lua\nlocal structure = AudioScape:getStructure({ asset_id = \"1843209165\", include_metadata = true })\n\nfor _, section in structure.sections do\n    if section.metadata.type == \"Move\" then\n        -- e.g. { type = \"Move\", path = \"CueObjects.Part_1\", endPoint = \"0,10,0\",\n        --        startPoint = \"0,0,0\", easingStyle = \"Quad\", relative = \"true\" }\n        task.delay(section.start, function()\n            runMoveCue(section.metadata, section[\"end\"] - section.start)\n        end)\n    end\nend\n```\n\n> **Note on AudioPlayer:** v0.11.0 auto-resolves `audioPlayer.Asset` (the legacy ContentId field). If your project uses the newer `audioPlayer.AudioCont","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/this-fifo/roblox/audioscape-sdk/0.20.1/readme"}