Skip to content

Sound and music

Lesson 9.4

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:

CL_SOUND_MASTER, CL_SOUND_MUSIC, CL_SOUND_RECORDS, CL_SOUND_WEATHER, CL_SOUND_BLOCKS, CL_SOUND_HOSTILE, CL_SOUND_NEUTRAL, CL_SOUND_PLAYERS, CL_SOUND_AMBIENT, CL_SOUND_VOICE, CL_SOUND_UI

Sounds in the world (server)

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

static void on_block_break(void* payload, void* user) {
    (void)user;
    cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
    if (!cl_block_state_is(e->state, CL_STR("my_mod:ruby_block"))) return;
    cl_vec3 at = {e->pos.x + 0.5, e->pos.y + 0.5, e->pos.z + 0.5};
    cl_sound_play(e->level, at, CL_STR("minecraft:block.amethyst_block.chime"), CL_SOUND_BLOCKS, 1.0f, 1.0f);
}

cl_sound_play(level, pos, sound, category, volume, pitch):

  • pos is a position with decimals; + 0.5 puts it in the middle of the 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:

Function Who hears it
cl_sound_play(level, pos, sound, category, volume, pitch) Everybody near pos.
cl_sound_play_except(level, player, pos, ...) Everybody near pos except one player (who plays it on their own).
cl_sound_play_to_player(player, pos, sound, category, volume, pitch) Only that player.
cl_sound_play_at_entity(entity, sound, category, volume, pitch) Everybody near the entity, at its position.
cl_sound_stop_for_player(player, sound, category) Stops a sound for one player. An empty sound (CL_STR("")) 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(CL_STR("minecraft:ui.button.click"), 1.0f, 1.0f, NULL);

cl_sound_play_ui(sound, volume, pitch, &out) 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, &out) plays at a position in the world, only on this client. Its cl_sound handle lets you stop long or looping sounds:

static cl_sound g_hum;

static void start_hum(void) {
    cl_vec3 at = {0.5, 64.5, 0.5};
    cl_sound_play_local(at, CL_STR("minecraft:block.beacon.ambient"), CL_SOUND_BLOCKS, 1.0f, 1.0f, true, &g_hum);
}

static void stop_hum(void) {
    cl_sound_stop(g_hum);
}

cl_sound_is_playing(s) tells you whether it is still going, cl_sound_exists(id) whether a sound id exists at all, and cl_sound_stop_local(sound, category) stops sounds on this client by id.

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: CL_STR("my_mod:ruby_chime").

Music

cl_music_play(sound, replace_vanilla) replaces the background music on this client:

cl_music_play(CL_STR("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 CL_EV_CLIENT_SOUND_PLAY happens before any sound plays. Change volume or pitch, or cancel it:

// Quieter cows.
static void on_sound(void* payload, void* user) {
    (void)user;
    cl_ev_sound_play* e = CL_EVENT(cl_ev_sound_play, payload);
    static const char prefix[] = "minecraft:entity.cow.";
    size_t n = sizeof prefix - 1;
    if (e->sound.len >= n && memcmp(e->sound.ptr, prefix, n) == 0) {
        e->volume = e->volume * 0.3f;
    }
}

It runs for every sound, so keep the handler quick. Comparing the start with memcmp is fast, and it works on the cl_str directly (include <string.h>).

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
#include <clight_easy.h>

static void on_player_join(void* payload, void* user) {
    (void)user;
    cl_entity p = CL_EVENT(cl_ev_player, payload)->player;
    cl_vec3 pos;
    if (cl_entity_position(p, &pos) != CL_OK) return;
    cl_sound_play_to_player(p, pos, CL_STR("minecraft:entity.player.levelup"), CL_SOUND_PLAYERS, 1.0f, 1.2f);
}

static cl_result mod_main(void) {
    return CL_ON(CL_EV_PLAYER_JOIN, on_player_join);
}

CLIGHT_MOD_ENTRY(mod_main)

The player’s position already has decimals, so it can be passed straight in.

Recap: cl_sound_play and cl_sound_play_to_player play sounds from the server, cl_sound_play_ui and cl_sound_play_local play them on the client, sounds.json adds your own .ogg files, and cl_music_play changes the music.