What makes a sound happen
Verified against Minecraft 26.3 · Part X · you break a block and you hear it: three doors a sound can come through, and only one of them says what the sound is.
Watch someone place a block and the server sends ClientboundSoundPacket,
naming the sound. Watch them break the same block and it sends nothing of the kind: Block.playerWillDestroy fires a level event through Block.spawnDestroyByEntityParticles, and
ClientboundLevelEventPacket carries an int and a block-state id. The client
decides for itself what that int means — for a break,
SoundType.getBreakSound off the block’s own SoundType, the five-sound
group (break, step, place, hit, fall) every BlockBehaviour.Properties
carries — and plays the result locally. So the two
halves of one interaction reach you by different mechanisms, and the second
one is not a sound at all until your client makes it one.
Then there is the third door, and it is the absence of a packet rather than a packet: some sounds never cross the wire at all. Two quite different things come through it. Biome loops, the cave mood, music, the underwater hum are decided by your client and nobody else’s. And so, more surprisingly, are your own place and break — neither of the calls above is sent to the player who caused the sound, because that player’s client has already played it locally. Who hears it below is the one rule that arranges both.
This page is the content model and those three doors, and it takes them first: the table below is what the page is for, and the cast comes after it, because half of the eight classes only make sense once you know which door they stand in. The machine that turns any of them into an OpenAL source is the sound engine.
The three doors
flowchart TD
SERVER["something happens on the server"]
NAMED["door 1 — Level.playSound names a SoundEvent"]
NOWIRE["door 3 — nothing crosses the wire"]
EVENT["door 2 — Level.levelEvent sends an int and a block-state id"]
P1["ClientboundSoundPacket or ClientboundSoundEntityPacket"]
P2["ClientboundLevelEventPacket"]
LEH["LevelEventHandler reads the int against this client's block data"]
LOCAL["ClientLevel.playSeededSound — the excluded entity is you, so the test passes"]
MINE["your own place, through the shared Level.playSound"]
MINEB["your own break, a level event run locally"]
AMB["BiomeAmbientSoundsHandler, MusicManager, the underwater and bubble-column handlers"]
SM["SoundManager.play"]
SERVER --> NAMED --> P1 --> LOCAL
NOWIRE --> MINE --> LOCAL
NOWIRE --> AMB --> SM
NOWIRE --> MINEB --> LEH
LOCAL --> SM
SERVER --> EVENT --> P2 --> LEH --> SM
Doors one and three meet, which is the point of the picture: a packet and your own placing hand both arrive at ClientLevel.playSeededSound, and both pass its one test because the excluded entity is you. Your own break runs door two’s handler locally instead, and door two never names a sound on the wire at all.
The three columns are how to reason about the wire, and the third is the one the usual summary leaves out entirely.
| a named sound | a level event | nothing on the wire | |
|---|---|---|---|
| what crosses | a SoundEvent holder, a position in eighths of a block, a seed | an int and a block-state id | nothing |
| who chooses the sound | the server | this client, from its own resource pack and block data | this client |
| can it name a sound in no registry | yes — SoundEvent.STREAM_CODEC sends either a registry id or an id plus a range | only a jukebox song’s, whose entry may carry one inline | yes, when the data behind it carries one inline: the synced ambience and music attributes, a goat horn’s instrument, an item’s consume sound |
| examples | a block placed, /playsound, a mob’s voice | a block broken, a dispenser, fire extinguished, a ghast warning, the wither spawn, the dragon’s death | your own place and break; biome loops, cave mood, music, underwater, bubble columns |
That third row is a genuine hole in the usual summary. Data packs cannot
register sound events — Registries.SOUND_EVENT is a static registry —
but a packet may carry an inline SoundEvent, so a server can name a
sound that is in no registry at all. The two statements are both true and are
usually run together into a false one.
The other clientbound members of the family are
ClientboundSoundEntityPacket, which follows a moving entity,
ClientboundStopSoundPacket, which /stopsound sends, and
ClientboundExplodePacket, which carries its own sound alongside everything
else an explosion needs. There is no serverbound sound packet anywhere: the server infers what you did from
other packets and tells everyone else about the sound.
The cast
| class | what it decides | thread |
|---|---|---|
SoundEvent | a name and an optional fixed range — never a file | both sides |
SoundEvents | the static registry of every event the game defines | both sides |
SoundSource | which volume slider applies | both sides |
SoundEventRegistration | what one sounds.json entry says, and whether it replaces or appends | worker pool |
WeighedSoundEvents | the weighted list a name resolves to, and the redirects in it | worker pool, then Render thread |
LevelEventHandler | what an int from ClientboundLevelEventPacket means | Render thread |
BiomeAmbientSoundsHandler | the loop, the random additions and the cave mood | Render thread |
MusicManager | which track, how often, and the fade that is not the slider | Render thread |
What a sound is, as data
SoundEvent — in net/minecraft/sounds, and therefore shared — is a record
of an Identifier and an optional fixed range. SoundEvents is the static registry of every one the game defines. It is a name, not
a file.
The file comes from sounds.json, one per namespace in every resource pack,
which SoundEventRegistrationSerializer parses into a
SoundEventRegistration per event name: a weighted list of
Sound entries — a file, or a redirect to another event, per the
Sound.Type enum — each with volume, pitch, weight, attenuation distance,
and whether to stream rather than load whole. SoundManager owns the
loaded form, a map of Identifier to WeighedSoundEvents, rebuilt on every
resource reload.
Packs merge rather than replace, as tags, languages and atlas sources do among the lists the resource system merges instead of letting the top pack win, and, as a tag can, an entry may say otherwise.
SoundEventRegistration carries a replace flag; without it a higher pack’s
entries are appended to the lower pack’s list, so a pack that adds one
variant gets a mix rather than an override. A redirect entry multiplies
volume and pitch through and ORs the streaming flag.
And there are two kinds of silence, which is worth knowing because only one of
them is a mistake. The identifier
SoundManager.INTENTIONALLY_EMPTY_SOUND_LOCATION is short-circuited by name in
AbstractSoundInstance.getOrResolve before the registry is consulted at all, so
anything asking for it is silenced with no log warning. An event that
simply does not resolve is the other kind, and it logs — including a pack that
empties an event’s list, which gets the warning rather than the quiet.
One development flag makes sounds visible: SharedConstants.DEBUG_SUBTITLES gives every sound that plays a subtitle, its path, whether or not its event declares one. Neither kind of silence gets one, because neither reaches the listeners.
SoundSource is the volume category and each one is an options slider:
SoundSource.MASTER, SoundSource.MUSIC, SoundSource.RECORDS,
SoundSource.WEATHER, SoundSource.BLOCKS, SoundSource.HOSTILE,
SoundSource.NEUTRAL, SoundSource.PLAYERS, SoundSource.AMBIENT,
SoundSource.VOICE, SoundSource.UI. Two of those carry more than the
options screen says: SoundSource.RECORDS, Jukebox/Note Blocks, also carries the goat horn, and SoundSource.WEATHER carries thunder and the End flash as well as rain. Neither is SoundSource.AMBIENT.
Who hears it
ServerLevel.playSeededSound asks SoundEvent.getRange for the audible
radius — a fixed range if the event declares one, otherwise sixteen blocks
scaled up by volumes above one — and PlayerList.broadcast sends the packet
to every player in that dimension within range, skipping the excluded
player. The seed travels in the packet so that every client picks the same
variant and samples the same pitch multiplier off the chosen Sound, which is
why a sound that is one of eight variants sounds the same to two players
standing together.
And the excluded player is not left out; they are served first. The two
sides read that one word oppositely. ServerLevel.playSeededSound broadcasts
to everyone in range but the excluded entity; ClientLevel.playSeededSound —
the same method name, the client’s override, in both its positional and its
entity-bound form — plays a sound only when the excluded entity is the
local player.
That inversion is what makes the client override usable for two jobs at once,
and it is worth being explicit about which is which. When a packet arrives,
ClientPacketListener.handleSoundEvent hands the override the local player
as the excluded entity, so the test passes and a bystander hears the packet
normally. When shared block or item code calls Level.playSound while running
on your own client, the excluded entity is you for the same reason it was you
on the server — and the test passes again, locally. So the one method plays the sounds sent to you and the sounds never sent to anyone, and each machine hears exactly one copy: yours locally, theirs by packet. That is how your own place comes through the third door, with every other sound shared code plays with you excluded. Your break comes through it another way, and never reaches this method: the same shared code fires a level event on your client and on the server, ClientLevel.levelEvent plays it with no test at all, and ServerLevel.levelEvent sends it to everyone in range but the player who broke the block.
Two qualifications keep it from being a law. The rule needs an exclusion to
read: Player.playServerSideSound, which plays the six attack sounds,
excludes nobody, so your own critical hit does travel the whole way out and
back. And your local copy is genuinely a different sound from the one your
neighbour hears — your client drew its own seed (from Level.soundSeedGenerator for a place or a door, from the level’s own random for a break, which carries no seed on the wire, so every client that hears it rolls its own) rather than reading one off a packet, so the variant and the pitch are rolled twice (block interaction
follows a door through both rolls).
The position is quantised on the way:
ClientboundSoundPacket.LOCATION_ACCURACY is eight, so the wire carries
three ints in eighths of a block.
And sound has a speed, for the few callers that ask for it.
ClientLevel.playLocalSound takes a distance-delay flag, and when it is set
and the source is more than ten blocks off the sound is deferred by its
distance over a fixed rate, through SoundManager.playDelayed into
SoundEngine.queuedSounds. Firework explosions and a handful of level events
set it. Thunder, the sound everyone assumes is the reason it exists, does not:
LightningBolt passes the flag as false and the crack is instant.
LocalPlayer.playSound goes
the other way and calls ClientLevel.playLocalSound directly, skipping even
the exclusion check.
Music and ambience are environment attributes
BiomeSpecialEffects carries block tint only. Music and the ambient loop, additions and mood are carried by EnvironmentAttributes (see environment attributes and timelines): EnvironmentAttributes.BACKGROUND_MUSIC and EnvironmentAttributes.MUSIC_VOLUME for the music, EnvironmentAttributes.AMBIENT_SOUNDS for the other three together, beside EnvironmentAttributes.FIREFLY_BUSH_SOUNDS, a gate on the firefly bush’s own sound — all syncable, all resolved
through the same dimension-then-biome-then-timeline-then-weather layer stack
as fog and sky colour.
Minecraft.getSituationalMusic reads EnvironmentAttributes.BACKGROUND_MUSIC
off the camera’s attribute probe and asks BackgroundMusic.select for the
creative or underwater variant; the End boss fight overrides it directly.
Minecraft.getMusicVolume reads EnvironmentAttributes.MUSIC_VOLUME the
same way, which is how a biome dims its own music without touching the
slider.
BiomeAmbientSoundsHandler reads EnvironmentAttributes.AMBIENT_SOUNDS from
Level.environmentAttributes and drives three things from it: a cross-faded
loop, random additions on a per-tick chance (AmbientAdditionsSettings), and
the cave “mood” that accumulates in darkness (AmbientMoodSettings).
UnderwaterAmbientSoundHandler and BubbleColumnAmbientSoundHandler are the
two that remain plain client-side handlers with no attribute behind them.
MusicManager owns the rest: a MusicManager.MusicFrequency setting that caps the gap between tracks, a fade that drives
SoundManager.updateCategoryVolume rather than the player’s slider (the third
of the engine’s three volume
factors),
and the now-playing toast — a NowPlayingToast shown or withheld on the
SoundEngine.PlayResult the engine returned, and suppressed again by the
pause screen and by MusicToastDisplayState.
The remaining callers are worth naming because they are the ones that are neither the world nor the wire: the ambient handlers in client/resources/sounds for loops that exist only on the client, the GUI’s clicks and toast chimes (AbstractWidget.playButtonClickSound, ToastManager), and SoundPreviewHandler, which previews a representative sound for most categories while a volume slider is dragged outside a world.
Where to look
LevelEventHandler — the whole second door is two switches in one class, LevelEventHandler.levelEvent and LevelEventHandler.globalLevelEvent, and reading them is the fastest way to see how much of the game’s audio is not named on the wire.
ServerLevel.playSeededSound and PlayerList.broadcast for who is told;
ClientLevel.playSeededSound for the local-player branch that closes the
loop. SoundEventRegistration and WeighedSoundEvents for the pack model,
and BiomeAmbientSoundsHandler for the three ambience mechanisms in one
class.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.