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:
clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
const cl_vec3 at{e.pos.x + 0.5, e.pos.y + 0.5, e.pos.z + 0.5};
clight::Level(e.level).sound_play(at, "minecraft:block.amethyst_block.chime", CL_SOUND_BLOCKS, 1.0f, 1.0f);
}, {.filter = "my_mod:ruby_block"}).detach();
level.sound_play(pos, sound, category, volume, pitch):
posis a position with decimals;+ 0.5puts it in the middle of the block.volumeis usually1.0. Above 1 doesn’t get louder, but can be heard from further away (16 blocks times the volume).pitchfrom0.5(low) to2.0(high).1.0is normal.
Other ways to play on the server:
| Method | Who hears it |
|---|---|
level.sound_play(pos, sound, category, volume, pitch) |
Everybody near pos. |
level.sound_play_except(player, pos, sound, ...) |
Everybody near pos except one player (who plays it on their own). |
player.sound_play_to_player(pos, sound, category, volume, pitch) |
Only that player. |
entity.sound_play_at_entity(sound, category, volume, pitch) |
Everybody near the entity, at its position. |
player.sound_stop_for_player(sound, category) |
Stops a sound for one player. An empty sound ("") 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:
clight::sound::play_ui("minecraft:ui.button.click", 1.0f, 1.0f);
clight::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.
clight::sound::play_local(pos, sound, category, volume, pitch, looping) plays at a position in the world, only on this client. It gives you a clight::Sound in its Expected, which lets you stop long or looping sounds:
class HumMod final : public clight::Mod {
public:
void start_hum() {
if (auto s = clight::sound::play_local({0.5, 64.5, 0.5}, "minecraft:block.beacon.ambient", CL_SOUND_BLOCKS, 1.0f, 1.0f, true)) {
hum_ = *s;
}
}
void stop_hum() { hum_.stop(); }
private:
clight::Sound hum_;
};
hum_.is_playing() tells you whether it is still going, clight::sound::exists(id) whether a sound id exists at all, and clight::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: "my_mod:ruby_chime".
Music
clight::sound::music_play(sound, replace_vanilla) replaces the background music on this client:
clight::sound::music_play("my_mod:boss_theme", true);
// when the fight is over
clight::sound::music_stop();
With replace_vanilla = false, vanilla’s music comes back by itself after your track. With true, it stays quiet until you call music_stop(). clight::sound::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 ev::ClientSoundPlay happens before any sound plays. Change volume or pitch, or cancel it:
// Quieter cows.
clight::on<clight::ev::ClientSoundPlay>([](cl_ev_sound_play& e) {
if (clight::sv(e.sound).starts_with("minecraft:entity.cow.")) {
e.volume = e.volume * 0.3f;
}
}).detach();
It runs for every sound, so keep the handler quick. starts_with on a std::string_view is fast and needs no copy.
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.hpp>
class LevelUpJoinMod final : public clight::Mod {
public:
void on_construct() override {
clight::on<clight::ev::PlayerJoin>([](cl_ev_player& e) {
clight::Entity p = e.player;
auto pos = p.position();
if (!pos) return;
p.sound_play_to_player(*pos, "minecraft:entity.player.levelup", CL_SOUND_PLAYERS, 1.0f, 1.2f);
}).detach();
}
};
CLIGHT_MOD(LevelUpJoinMod)
The player’s position already has decimals, so it can be passed straight in.
Recap: level.sound_play and player.sound_play_to_player play sounds from the server, clight::sound::play_ui and play_local play them on the client, sounds.json adds your own .ogg files, and clight::sound::music_play changes the music.