Defining Bundle Sounds
Defining Bundle Sounds
Sound definitions describe named sounds that scripts can play. They live in the common sounds registry so both server and client code can refer to the same identifier.
Audio definitions describe the client-side asset and playback defaults behind a sound. They live in the client audio registry because only the client loads and plays audio files.
Use this guide when you are adding sound effects to a bundle, wiring server gameplay events to client playback, or overriding audio in a resource pack.
Sound and audio split
Selene separates a playable sound into two definitions:
- A common
soundsentry gives scripts a stable gameplay-facing identifier. - A client
audioentry points to the actual audio file and defines playback defaults.
For example, the sound myworld:ui/click can point to audio myworld:ui/click:
{
"entries": {
"ui/click": {
"audio": "myworld:ui/click"
}
}
}
The matching client audio entry can then point at the file:
{
"entries": {
"ui/click": {
"type": "simple",
"file": "client/audio/myworld/ui/click.ogg"
}
}
}
The identifiers do not have to match, but matching them keeps data easy to scan. Use separate identifiers when several gameplay sounds intentionally share the same audio asset.
Where sounds live
Put sound definitions under common/data/<namespace>/, where <namespace> becomes the namespace part of each sound identifier.
You can define multiple sounds in one registry file:
my-bundle/
bundle.json
common/
data/
myworld/
sounds.json
Or split each sound into its own JSON file:
my-bundle/
common/
data/
myworld/
sounds/
ui/
click.json
The merged sounds.json form stores entries by local name. This defines myworld:ui/click:
{
"entries": {
"ui/click": {
"audio": "myworld:ui/click"
}
}
}
The split-file form uses the file path as the local name. For example, common/data/myworld/sounds/ui/click.json also defines myworld:ui/click.
Where audio lives
Put audio definitions under client/data/<namespace>/, where <namespace> becomes the namespace part of each audio identifier.
You can define multiple audio entries in one registry file:
my-bundle/
client/
data/
myworld/
audio.json
audio/
myworld/
ui/
click.ogg
Or split each audio entry into its own JSON file:
my-bundle/
client/
data/
myworld/
audio/
ui/
click.json
The merged audio.json form stores entries by local name. This defines myworld:ui/click:
{
"entries": {
"ui/click": {
"type": "simple",
"file": "client/audio/myworld/ui/click.ogg",
"volume": 1,
"pitch": 1,
"loop": false
}
}
}
The split-file form uses the file path as the local name. For example, client/data/myworld/audio/ui/click.json also defines myworld:ui/click.
Sound identifiers
Sound identifiers use the same namespace:name format as other Selene registries:
myworld:ui/click
myworld:effects/door_open
myworld:ambient/wind
The namespace comes from the directory below common/data/. The name comes from either the key inside sounds.json or the relative path of a file below sounds/.
Use stable names once scripts, map triggers, entity components, or saved data refer to a sound. Renaming a sound definition means updating every reference to it.
Sound shape
A sound definition has these fields:
{
"audio": "myworld:ui/click",
"metadata": {},
"tags": []
}
audio is required. The other fields have defaults in the engine and can be omitted when the default is correct:
{
"audio": "myworld:ui/click"
}
Fields:
audiois the client audio identifier resolved when the sound is played.metadatastores custom structured data that can be searched throughselene.registries.tagsare strings used for filtering and classification.
Simple audio
Use simple for sound effects that can be played through selene.sounds.
{
"type": "simple",
"file": "client/audio/myworld/effects/door_open.ogg",
"volume": 0.8,
"pitch": 1,
"loop": false
}
Fields:
fileis the bundle-relative audio asset path.volumeis multiplied with the volume passed by Lua. If omitted, Selene uses1.pitchis multiplied with the pitch passed by Lua. If omitted, Selene uses1.loopmakes the sound repeat until it is stopped. If omitted, Selene usesfalse.metadatastores custom data for client-side lookup.
Use short, memory-friendly files for sound effects. Selene loads simple audio as a sound asset and caches it by file path.
Music audio
The client audio registry also supports music entries:
{
"type": "music",
"file": "client/audio/myworld/music/theme.ogg"
}
The current selene.sounds playback path resolves simple audio definitions. Use music entries only for systems that explicitly consume music audio definitions.
Audio file paths
Audio file paths are bundle-relative paths, such as:
client/audio/myworld/ui/click.ogg
client/audio/myworld/ambient/wind.ogg
The path is resolved from the loaded bundle files. Keep paths under client/audio/ for discoverability, even though the resolver only requires a valid bundle-relative path.
If you are overriding an existing audio asset in a resource pack, keep the same internal path as the original bundle. If you are defining a new audio entry, choose a path that matches your bundle's own audio layout.
Playing sounds from client Lua
Client Lua can play local sounds through selene.sounds:
local Registries = require("selene.registries")
local Sounds = require("selene.sounds")
local click = Registries.findByName("sounds", "myworld:ui/click")
Sounds.playSound(click)
Sounds.playSound(click, {
volume = 0.5,
pitch = 1.1
})
Client Lua can stop the currently tracked instance of a sound:
Sounds.stopSound(click)
Or stop all sounds managed by the client sound manager:
Sounds.stopAllSounds()
stopSound is most useful for looping sounds. Non-looping sounds usually finish on their own.
Playing sounds from server Lua
Server Lua can play a sound for a specific player:
local Registries = require("selene.registries")
local Sounds = require("selene.sounds")
local doorOpen = Registries.findByName("sounds", "myworld:effects/door_open")
Sounds.playSound(player, doorOpen, {
volume = 0.8,
pitch = 1
})
Play a positional sound near a coordinate:
Sounds.playSoundAt(100, 100, 0, doorOpen, dimension)
Or broadcast a non-positional sound in a dimension:
Sounds.playGlobalSound(doorOpen, dimension)
The server sends the sound identifier plus volume and pitch to clients. Each client then resolves the sound's audio entry locally and plays the referenced audio file.
Metadata
Use sound metadata for imported ids, sound categories, editor grouping, or migration data:
{
"entries": {
"effects/door_open": {
"audio": "myworld:effects/door_open",
"metadata": {
"legacySoundId": 21,
"category": "environment"
},
"tags": [
"myworld:effect",
"myworld:environment"
]
}
}
}
Registry metadata can be queried from Lua:
local Registries = require("selene.registries")
local doorOpen = Registries.findByMetadata("sounds", "legacySoundId", 21)
Use metadata keys consistently. If you are importing from an older content pipeline, store the old id in metadata instead of baking it into the sound name unless the name is already meaningful.
Complete example
common/data/myworld/sounds.json
{
"entries": {
"ui/click": {
"audio": "myworld:ui/click",
"tags": ["myworld:ui"]
},
"effects/door_open": {
"audio": "myworld:effects/door_open",
"metadata": {
"legacySoundId": 21
},
"tags": ["myworld:effect"]
},
"ambient/wind": {
"audio": "myworld:ambient/wind_loop",
"tags": ["myworld:ambient"]
}
}
}
client/data/myworld/audio.json
{
"entries": {
"ui/click": {
"type": "simple",
"file": "client/audio/myworld/ui/click.ogg"
},
"effects/door_open": {
"type": "simple",
"file": "client/audio/myworld/effects/door_open.ogg",
"volume": 0.8
},
"ambient/wind_loop": {
"type": "simple",
"file": "client/audio/myworld/ambient/wind_loop.ogg",
"volume": 0.6,
"loop": true
}
}
}
Recommended asset layout:
my-bundle/
common/
data/
myworld/
sounds.json
client/
data/
myworld/
audio.json
audio/
myworld/
ui/
click.ogg
effects/
door_open.ogg
ambient/
wind_loop.ogg
Resource pack overrides
Resource packs can override sounds in three ways:
- Replace the audio file at the same internal path.
- Replace the client audio definition and point it to a different file or change default volume, pitch, or looping.
- Replace the common sound definition and point it to a different audio identifier.
For example, to override only the audio file for myworld:ui/click, mirror the original path:
global_client_bundles/
my-resource-pack/
client/
audio/
myworld/
ui/
click.ogg
To override an audio definition from a merged registry file, mirror the original audio.json path:
global_client_bundles/
my-resource-pack/
client/
data/
myworld/
audio.json
If the original audio definition was split into client/data/myworld/audio/ui/click.json, mirror that file path instead.
See Making Resource Packs for the broader override workflow.
Hot reload behavior
Sound and audio definitions are loaded through the bundle registry system.
- Updating
common/data/<namespace>/sounds.jsonreloads the merged sound file. - Updating a file below
common/data/<namespace>/sounds/reloads that single sound entry. - Updating
client/data/<namespace>/audio.jsonreloads the merged audio file. - Updating a file below
client/data/<namespace>/audio/reloads that single audio entry. - If both a merged registry file and split registry directory exist for the same namespace in the same bundle, the merged file is authoritative.
The sound manager caches loaded audio files by path. If you replace the audio file itself while the client is running, restart the client or trigger a full reload if your workflow requires the cached asset to be recreated.
Checklist
- Put gameplay-facing sound definitions under
common/data/<namespace>/sounds.jsonorcommon/data/<namespace>/sounds/. - Put client audio definitions under
client/data/<namespace>/audio.jsonorclient/data/<namespace>/audio/. - Put referenced audio assets under
client/audio/. - Reference sounds from Lua through the
soundsregistry, such asmyworld:effects/door_open. - Use
simpleaudio for sounds played throughselene.sounds. - Use
loop: trueonly for sounds that scripts can deliberately stop. - Keep sound identifiers stable once scripts, triggers, or saved data refer to them.