Skip to content

Sound and music

Lesson 14.5

A click, a chime, a little tune: sound makes a mod feel finished. You can play any vanilla sound, add your own, and even change the music.

Sound ids and categories

Every sound has an id like "minecraft:block.note_block.bell" or "minecraft:entity.experience_orb.pickup". These are sound events, the same names /playsound uses. Each sound also has a category, which decides which volume slider in the options controls it:

"master", "music", "records", "weather", "blocks", "hostile", "neutral", "players", "ambient", "voice", "ui"

Sounds in the world (server)

On the server, sounds are played at a position and every player nearby hears them:

cl.on("block.break", function(event)
  if event.block == "my_mod:ruby_block" then
    event.world:play(event.pos:center(), "minecraft:block.amethyst_block.chime", "blocks", 1.0, 1.0)
  end
end)

world:play(pos, sound, category, volume, pitch):

  • pos is a position with decimals; block_pos:center() gives the middle of a block.
  • volume is usually 1.0. Above 1 doesn’t get louder, but can be heard from further away (16 blocks times the volume).
  • pitch from 0.5 (low) to 2.0 (high). 1.0 is normal.

Other ways to play on the server:

Call Who hears it
world:play(pos, sound, category, volume, pitch) Everybody near pos.
player:play_to_player(pos, sound, category, volume, pitch) Only that player.
entity:play_at_entity(sound, category, volume, pitch) Everybody near the entity, following it.
player:stop_for_player(sound, category) Stops a sound for one player. "" stops all sounds in the category.

These go over vanilla’s sound packets, so players without your mod hear vanilla sounds too.

Sounds on the client

In the game window, sounds play only for you:

cl.sound.play_ui("minecraft:ui.button.click", 1.0, 1.0)

cl.sound.play_ui(sound, volume, pitch) is a sound without a position, like a button click. It is the one to use in screens and for HUD notifications.

cl.sound.play_local(pos, sound, category, volume, pitch, looping) plays at a position in the world, only on this client. It returns a sound object, so long or looping sounds can be stopped:

local hum = cl.sound.play_local(cl.vec(0.5, 64.5, 0.5), "minecraft:block.beacon.ambient", "blocks", 1.0, 1.0, true)
-- later
hum:stop()

sound:is_playing() tells you whether it is still going, and cl.sound.exists(id) whether a sound id exists at all.

Your own sounds

Your sounds are resources in assets/my_mod/:

assets/my_mod/
  sounds.json
  sounds/
    ruby_chime.ogg

The audio file must be Ogg Vorbis (.ogg). Most audio editors, like Audacity, can export it. Use mono files for sounds in the world: only mono sounds get quieter with distance.

sounds.json names the sound event and lists its files:

{
  "ruby_chime": {
    "sounds": ["my_mod:ruby_chime"],
    "subtitle": "subtitles.my_mod.ruby_chime"
  }
}

The key ruby_chime becomes the sound id my_mod:ruby_chime. "my_mod:ruby_chime" in the list means the file sounds/ruby_chime.ogg. With several files in the list, the game picks one at random each time, which is nice for footsteps and hits.

Add the subtitle to your language file, for players who use subtitles:

{
  "subtitles.my_mod.ruby_chime": "Ruby chimes"
}

Then play it like any other sound:

event.world:play(event.pos:center(), "my_mod:ruby_chime", "blocks", 1.0, 1.0)

Music

cl.music.play(sound, replace_vanilla) replaces the background music on this client:

cl.music.play("my_mod:boss_theme", true)
-- when the fight is over
cl.music.stop()

With replace_vanilla = false, vanilla’s music comes back by itself after your track. With true, it stays quiet until you call cl.music.stop(). cl.music.is_playing() tells you whether your track is playing.

For music, mark the file as streamed in sounds.json, so long files don’t sit in memory:

{
  "boss_theme": {
    "category": "music",
    "sounds": [{ "name": "my_mod:boss_theme", "stream": true }]
  }
}

Changing sounds

The client event client.sound_play happens before any sound plays. Change volume or pitch, or cancel it:

-- Quieter cows.
cl.on("client.sound_play", function(e)
  if e.sound:find("entity.cow.", 1, true) then   -- plain search, no pattern
    e.volume = e.volume * 0.3
  end
end)

It runs for every sound, so keep the handler quick.

Try it

When a player joins, play the level-up sound for only that player, at their position, at a slightly higher pitch than normal.

Show a solution
cl.on("player.join", function(event)
  local p = event.player
  p:play_to_player(p.pos, "minecraft:entity.player.levelup", "players", 1.0, 1.2)
end)

player.pos already has decimals, so it can be passed straight in.

Recap: world:play and player:play_to_player play sounds from the server, cl.sound.play_ui and play_local play them on the client, sounds.json adds your own .ogg files, and cl.music.play changes the music.