Luau SDK
v0.7.0Drop-in Wally package for Roblox — search, browse, discover music, and track player analytics in a few lines of Luau. Runs server-side with automatic batching and game telemetry.
Installation
Install via Wally, the Roblox package manager. Add the dependency to your wally.toml:
[dependencies] AudioScape = "this-fifo/audioscape-sdk@0.7.0"
Then run:
wally install
Alternatively, download AudioScape.rbxm from the latest release and drop it into ServerStorage in Roblox Studio.
Source code and examples available on GitHub.
Prerequisites: Enable Allow HTTP Requests in Game Settings → Security. The SDK uses HttpService:RequestAsync() and must run in a server Script (not a LocalScript). Roblox enforces a limit of 500 HTTP requests per minute per game server.
Setup
Require the module and create a client with your API key. Use the Roblox Secrets Store in production to keep your key secure.
local ServerScriptService = game:GetService("ServerScriptService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local AudioScape = require(ServerScriptService.Packages.AudioScape)
-- Use a test key in Studio, Secrets Store in production
local apiKey = if RunService:IsStudio()
then "your-test-key"
else HttpService:GetSecret("AudioScapeKey")
local client = AudioScape.new(apiKey)Search
Search the catalog using natural language. Returns tracks, artists, and albums.
local result, err = client:search({
query = "chill lo-fi beats",
limit = 10,
playerId = player.UserId, -- optional
filters = {
genres = { "electronic" },
duration = { min = 60, max = 180 },
min_play_count = 100000, -- Roblox lifetime plays
min_likes = 500,
created_after = "2024-01-01",
},
})
if not result then
warn("Search failed:", err)
return
end
for _, track in result.tracks do
print(track.artist, "—", track.name)
endSimilar
Find tracks that sound like a given track. Pass an asset ID and get acoustically similar results.
local result, err = client:similar({
asset_id = "123456789",
limit = 5,
playerId = player.UserId,
})
if result then
print("Found", result.meta.total, "similar tracks")
endBrowse
Browse by artist, album, genre, or mood. Omit name to list available entities, or provide it to get tracks.
-- List all genres
local genres, err = client:browse({ type = "genre" })
for _, genre in genres.items do
print(genre.display_name, "—", genre.track_count, "tracks")
end
-- Get tracks for a specific genre
local tracks, err = client:browse({
type = "genre",
name = "electronic",
limit = 20,
})Browse types: artist, album, genre, mood
Playlists
Fetch playlists configured in the Configure tab. List all playlists for your API key, or fetch a specific playlist with its tracks.
-- List all configured playlists
local list, err = client:listPlaylists()
for _, p in list.playlists do
print(p.name, "—", p.track_count, "tracks")
end
-- Fetch a specific playlist
local result, err = client:getPlaylist({
playlist_id = list.playlists[1].id,
})
-- result.playlist = { id, name, genre, playback_mode, track_count }
-- result.tracks = { { asset_id, name, artist, position, ... } }Playlist IDs are found in the Configure tab or via listPlaylists(). See the Playlist endpoint docs for the full API reference.
Track Structure
Fetch the beat grid and section labels for a track to sync animations, lighting, or VFX to the music. Returns BPM, every beat in seconds, downbeats, and labelled sections (Intro/Verse/Drop/Climax/...) with energy 1-4 and bar ranges. See the Track Structure endpoint docs for the full response shape.
-- One fetch — helpers reuse the cached structure
local structure, err = client:getStructure({ asset_id = "1843209165" })
-- Burst particles on every downbeat
for _, t in ipairs(structure.beat_grid.downbeats) do
task.delay(t, function() emitter:Emit(20) end)
end
-- Trigger different VFX on the Drop
local section = client:sectionAtTime("1843209165", currentTime)
if section and section.label == "Drop" then
camera:Shake(section.energy)
endHelpers: client:beatAtTime(asset_id, t) returns the closest beat { index, time, beat_num, is_downbeat }; client:sectionAtTime(asset_id, t, level?) returns the section (or phrase) covering time t. Both share a per-asset client-side cache, so repeated calls are free after the first fetch.
Coverage: ~96% of the catalog has beat grid analysis, ~91% has section labels. Tracks without analysis return null for beat_grid and empty arrays for sections/phrases.
Telemetry
The SDK automatically sends your game's Universe ID and Place ID with every request via headers. These are captured from game.GameId and game.PlaceId at construction time — no setup needed.
You can also pass an optional playerId to any method to tie requests to specific players for per-player analytics. This data appears in your dashboard under Unique Games, Unique Players, and the Top Games table.
| Header | Source | Sent |
|---|---|---|
| X-Universe-Id | game.GameId | Every request (auto) |
| X-Place-Id | game.PlaceId | Every request (auto) |
| X-Player-Id | player.UserId | When playerId is provided |
Analytics
Track player behavior to power music intelligence — trending charts, personalized recommendations, and search relevance. Events are automatically batched and sent in the background with no impact on game performance.
-- Track when a player listens to a song
client:trackPlay(track.asset_id, player.UserId, songDuration)
-- Track votes, favorites, skips
client:trackVote(assetId, "up", player.UserId)
client:trackFavorite(assetId, player.UserId)
client:trackSkip(assetId, player.UserId, 12.5)
-- Track custom events
client:trackCustom("station_tuned", assetId, player.UserId, {
station = "main_stage",
})Configuration
Analytics is enabled by default with sensible defaults. Override settings if needed:
client:configureAnalytics({
enabled = true, -- default true
batchInterval = 30, -- seconds between flushes
maxBatchSize = 50, -- events per batch
maxQueueSize = 500, -- max buffered events
})| Method | Signal | Description |
|---|---|---|
| trackPlay | Medium | Song started playing |
| trackStop | Info | Song stopped (natural end or user action) |
| trackSkip | Negative | Song skipped early (pass duration for signal strength) |
| trackVote | High | Upvote or downvote ("up" / "down") |
| trackFavorite | Very High | Added to favorites |
| trackUnfavorite | Info | Removed from favorites |
| trackAddToQueue | High | Added to setlist or queue |
| trackSearchClick | Medium | Clicked a search result |
| trackCustom | Custom | Any custom event with optional metadata |
Privacy: The playerId parameter is optional on all tracking methods. Omit it for fully anonymous analytics, or pass a hashed value if you need per-player insights without exposing real player IDs. Aggregate metrics (top tracks, trending, vote ratios) work without any player identification.
Error Handling
All methods return result, err. On failure, result is nil and err is a descriptive string.
local result, err = client:search({ query = "test" })
if not result then
warn("AudioScape error:", err)
-- err = "Rate limited — too many requests (429)"
return
endExamples
Complete scripts you can drop into ServerScriptService. Each one is self-contained and ready to run.
Browse genres and play a track
Lists all available genres, picks one at random, fetches its tracks, and plays a random track via SoundService.
local ServerStorage = game:GetService("ServerStorage")
local SoundService = game:GetService("SoundService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local AudioScape = require(ServerStorage.AudioScape)
local apiKey = if RunService:IsStudio()
then "your-test-key"
else HttpService:GetSecret("AudioScapeKey")
local client = AudioScape.new(apiKey)
-- List all genres
local genres, err = client:browse({ type = "genre" })
if not genres then warn("Failed:", err) return end
for _, genre in genres.items do
print(genre.display_name, "—", genre.track_count, "tracks")
end
-- Pick a genre and get its tracks (genre names are case-sensitive — use what listGenres returns)
local picked = genres.items[math.random(#genres.items)]
local tracks, trackErr = client:browse({
type = "genre",
name = picked.name,
limit = 10,
})
if not tracks then warn("Failed:", trackErr) return end
-- Play a random track
local track = tracks.tracks[math.random(#tracks.tracks)]
print("Playing:", track.artist, "—", track.name)
local sound = Instance.new("Sound")
sound.SoundId = "rbxassetid://" .. track.asset_id
sound.Parent = SoundService
sound:Play()Auto-playlist with similar tracks
When the current track ends, finds acoustically similar tracks and plays one. Add any Sound to SoundService to seed the chain.
local ServerStorage = game:GetService("ServerStorage")
local SoundService = game:GetService("SoundService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local AudioScape = require(ServerStorage.AudioScape)
local apiKey = if RunService:IsStudio()
then "your-test-key"
else HttpService:GetSecret("AudioScapeKey")
local client = AudioScape.new(apiKey)
local function playNext(currentAssetId)
local result, err = client:similar({
asset_id = currentAssetId,
limit = 5,
})
if not result or #result.tracks == 0 then
warn("No similar tracks:", err or "empty")
return
end
local next = result.tracks[math.random(#result.tracks)]
print("Up next:", next.artist, "—", next.name)
local sound = Instance.new("Sound")
sound.SoundId = "rbxassetid://" .. next.asset_id
sound.Parent = SoundService
sound.Ended:Once(function()
sound:Destroy()
playNext(next.asset_id)
end)
sound:Play()
end
-- Start from any Sound already in SoundService
local seed = SoundService:FindFirstChildWhichIsA("Sound")
if seed then
local id = string.match(seed.SoundId, "%d+")
if id then
seed.Ended:Once(function() playNext(id) end)
end
endPlay a configured playlist
Fetches a playlist created in the Configure tab and plays its tracks sequentially. Respects shuffle mode.
local ServerStorage = game:GetService("ServerStorage")
local SoundService = game:GetService("SoundService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local AudioScape = require(ServerStorage.AudioScape)
local apiKey = if RunService:IsStudio()
then "your-test-key"
else HttpService:GetSecret("AudioScapeKey")
local client = AudioScape.new(apiKey)
-- List all playlists for this API key
local list, listErr = client:listPlaylists()
if not list or #list.playlists == 0 then
warn("No playlists found:", listErr or "none configured")
return
end
-- Fetch the first playlist's tracks
local result, err = client:getPlaylist({ playlist_id = list.playlists[1].id })
if not result then warn("Failed:", err) return end
-- Shuffle if configured
local tracks = result.tracks
if result.playlist.playback_mode == "shuffle" then
for i = #tracks, 2, -1 do
local j = math.random(i)
tracks[i], tracks[j] = tracks[j], tracks[i]
end
end
-- Play tracks sequentially
for _, track in tracks do
print("Now playing:", track.artist, "—", track.name)
local sound = Instance.new("Sound")
sound.SoundId = "rbxassetid://" .. track.asset_id
sound.Parent = SoundService
sound:Play()
sound.Ended:Wait()
sound:Destroy()
end
print("Playlist finished!")Search box with RemoteEvent
Listens for search requests from clients via a RemoteEvent. Create a RemoteEvent named SearchRequest in ReplicatedStorage, then fire it from a LocalScript with the query string.
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local AudioScape = require(ServerStorage.AudioScape)
local apiKey = if RunService:IsStudio()
then "your-test-key"
else HttpService:GetSecret("AudioScapeKey")
local client = AudioScape.new(apiKey)
local searchEvent = ReplicatedStorage:WaitForChild("SearchRequest")
searchEvent.OnServerEvent:Connect(function(player, query)
if type(query) ~= "string" or #query == 0 then return end
local result, err = client:search({
query = query,
limit = 10,
playerId = player.UserId,
})
if not result then
warn("Search failed for", player.Name, ":", err)
return
end
print(player.Name, "searched", query, "—", result.meta.total, "results")
-- Send results back to client for display
searchEvent:FireClient(player, result)
end)Source code for all examples available on GitHub.