Chapter 19, The Soundtrack

Three sound folders, a shuffled music ticker, a jukebox record, and how numbered variants quietly become one event.

Sound in beta is wonderfully low-ceremony: you almost never register anything. Chapter 3 already showed the autoloader scanning assets/<modid>/sounds/ and turning file paths into event ids; this chapter picks up where that left off and walks the three channels, the background music ticker, jukebox records, and the trailing-digit variant trick the showcase's Aerbunny leans on. There's a little code here, but most of it is just dropping an ogg in the right folder.

The three sound channels

Every sound belongs to one of three channels, and the channel is simply which subfolder you drop the file in under sounds/. The folder name is the API, same as everywhere else in RetroAPI:

src/main/resources/assets/example_mod/sounds, the channel layout
sounds/
├── sound/              // short effects, random-variant collapse ON
│   ├── counter/click.ogg
│   └── mobs/aerbunny/aerbunnyhurt1.ogg
├── streaming/          // long tracks: records, forced themes, no collapse
│   └── sample_disc.ogg
└── music/              // background tracks fed to beta's shuffle ticker
    └── aether1.ogg
ChannelFolderWhat it's for
SOUNDsounds/sound/short effects: footsteps, clicks, mob hurt/death. Variant collapse on.
STREAMINGsounds/streaming/long, streamed-from-disk tracks: jukebox records and forced themes. No collapse.
MUSICsounds/music/background tracks handed to beta's random music ticker.

The event id is derived from the path inside the channel folder: strip .ogg, turn / into ., lowercase, prefix the mod id. So sound/counter/click.ogg becomes example_mod:counter.click. The autoloader scans all three channels for every loaded mod; the enum is the only place the channels are named in code, and you rarely touch it:

src/main/java/com/periut/retroapi/sound/api/RetroSounds.java, the channel enum
/** Vanilla sound channels. SOUND and MUSIC collapse random variants; STREAMING does not. */
public enum SoundChannel {
	/** Effect sounds (random-variant collapse enabled). */
	SOUND,
	/** Streaming records (no random-variant collapse). */
	STREAMING,
	/** Music tracks. */
	MUSIC
}

There is also a RetroSounds.register(channel, pathId, url) for mods that want to register a sound programmatically, but you almost certainly don't need it. The autoloader covers the file-in-a-folder case completely; this method is convenience sugar for when a sound lives somewhere odd on the classpath. Playing an effect is one line, the same call Chapter 3 used for the counter's click:

src/main/java/com/example/example_mod/ExampleCounterBlock.java, the play line
world.playSound(x + 0.5, y + 0.5, z + 0.5, "example_mod:counter.click", 1.0F, 1.0F);

Shuffled background music

There are two completely different ways to play music, and the difference is which channel the file lives in. The easy one needs no code at all.

The lazy way: drop it in music/

Any ogg under sounds/music/ is handed straight to beta's own background music ticker, the thing that plays a random track at random quiet intervals. The showcase ships sounds/music/aether1.ogg, and that's the whole setup: beta now shuffles it into the rotation alongside the vanilla menu and game tracks. You don't choose which track plays or when; you've simply added one more card to the deck the ticker draws from. The client entrypoint says exactly this, and writes no code to do it:

src/main/java/com/example/example_mod/ExampleModClient.java, the music comment
// Background music, the vanilla way: aether1.ogg lives in sounds/music/, so beta's
// own music ticker shuffles it into the random rotation alongside the vanilla
// tracks (SoundEntry.getSounds() picks randomly across everything registered). No
// code needed. For a FORCED theme on every world load instead, there is
// RetroMusic.playOnWorldLoad(streamingId) (puts the track in sounds/streaming/).

The forced way: RetroMusic.playOnWorldLoad

When you want a specific track at a known moment, the moment beta's mod author cares about most being world load, the shuffle ticker is no good: it controls both which and when. So a forced theme goes in the streaming/ channel instead and is played outright. RetroMusic.playOnWorldLoad wires that up; call it once from your client entrypoint with a streaming event id:

src/main/java/com/periut/retroapi/sound/api/RetroMusic.java, the registration
/**
 * Plays streamingEventId once each time a world finishes loading on this client.
 * The id is a STREAMING-channel event id (file at sounds/streaming/<name>.ogg).
 */
public static void playOnWorldLoad(String streamingEventId) {
	MinecraftClientEvents.READY_WORLD.register(minecraft -> play(minecraft, streamingEventId));
}

So the same audio file behaves very differently depending on its folder: music/theme.ogg joins the random rotation, while streaming/theme.ogg handed to playOnWorldLoad("yourmod:theme") fires on the dot every time a world finishes loading. Pick the folder that matches the intent. Note this whole subsystem is client-only; there's no networking and nothing touches the save.

Jukebox records

A music disc is the one place a sound needs a real item behind it. RetroRecordItem is that item, and it's gloriously thin because it just subclasses vanilla's MusicDiscItem. The whole play mechanism, slotting the disc into a jukebox, firing the world event, broadcasting to other players in multiplayer, is pure vanilla; the record carries a streaming/ event id in its sound field and vanilla does the rest. The showcase's SAMPLE_DISC is the entire pattern:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// A jukebox record. RetroRecordItem is a vanilla MusicDiscItem under the hood,
// so dropping it in a vanilla jukebox plays its streamed track exactly like a
// vanilla disc (including the multiplayer broadcast). The audio is the
// file-examples.com 1MB sample ogg at sounds/streaming/sample_disc.ogg.
SAMPLE_DISC = (Item) RetroItemAccess.of(
		new com.periut.retroapi.sound.api.RetroRecordItem(
			RetroItemAccess.allocateId(), "example_mod:sample_disc"))
	.register(id("sample_disc"));

The record resolves its inventory icon through .texture(id("sample_disc")), so the disc draws from sample_disc.png, saved at the path below.

sample_disc
sample_disc.png
download

src/main/resources/assets/example_mod/textures/item/sample_disc.png

The disc's item model is a plain generated model pointing at that texture, the last file the disc needs:

src/main/resources/assets/example_mod/retroapi/models/item/sample_disc.json
{
  "parent": "minecraft:item/generated",
  "textures": { "layer0": "example_mod:item/sample_disc" }
}

Two things to notice. First, the constructor takes a placeholder id from RetroItemAccess.allocateId() and the streaming event id, "example_mod:sample_disc", which points at sounds/streaming/sample_disc.ogg. Wrapping a hand-built instance with RetroItemAccess.of(...) is the exact same custom-item pattern from Chapter 5; a record is just an item whose class happens to be RetroRecordItem. Second, max stack size is forced to 1 inside the constructor, so you don't set it yourself.

The "now playing" name

Vanilla hard-codes the jukebox overlay to "C418 - " + disc.sound, which for a modded disc would read as the raw streaming id, ugly. RetroRecordItem fixes this by re-setting the overlay right after the vanilla call, pulling the display text from your lang file so it localises. The key is record.<namespace>.<name>, derived from the streaming id by swapping the colon for a dot:

src/main/java/com/periut/retroapi/sound/api/RetroRecordItem.java, the key derivation
// record.<namespace>.<name>, e.g. "record.example_mod.sample_disc".
this.descriptionKey = "record." + streamingId.replace(':', '.');

So for example_mod:sample_disc the key is record.example_mod.sample_disc, and the showcase's lang file gives it a friendly title:

src/main/resources/assets/example_mod/lang/en_US.lang, the record line
record.@.sample_disc=sample ogg - file-examples.com

The @ is the namespace placeholder RetroAPI's lang loader expands to your mod id (covered in Chapter 3), so this line registers record.example_mod.sample_disc. If you forget the line entirely nothing breaks: the overlay simply falls back to the raw id. Add it and the jukebox reads "sample ogg - file-examples.com" instead.

Sound variants

The SOUND channel does one quiet, very useful thing the other two don't: it collapses a trailing digit. Two files named aerbunnyhurt1.ogg and aerbunnyhurt2.ogg in the same folder register the same event, and the engine picks one at random each time you play it. This is exactly how vanilla ships its own footstep and dig sounds, four numbered oggs behind one event, and you get it for free just by numbering files. The showcase's Aerbunny (the puffy mob from Chapter 7) uses it for its hurt sound:

src/main/java/com/example/example_mod/AerbunnyEntity.java, getHurtSound()
@Override
protected String getHurtSound() {
	// Subfolder + variant sounds: files at sounds/sound/mobs/aerbunny/aerbunnyhurt1.ogg
	// and aerbunnyhurt2.ogg collapse to one event id, and the engine picks one at
	// random. The autoloader handles the nesting and the digit collapse for free.
	return "example_mod:mobs.aerbunny.aerbunnyhurt";
}

Walk the path through the deriver to see where the event id comes from. The two files live at sounds/sound/mobs/aerbunny/aerbunnyhurt1.ogg and ...hurt2.ogg. Inside the sound/ channel the relative path is mobs/aerbunny/aerbunnyhurt1; slashes become dots, the trailing 1 (and the 2) is collapsed away, and you're left with the single event example_mod:mobs.aerbunny.aerbunnyhurt. Both files feed it; play it and you hear one or the other at random.

Two caveats worth keeping straight. The collapse is SOUND-channel only, by design: a record or a forced theme in streaming/ keeps its exact name, because you never want "play a random one of these" for a 1MB track. And the digit must be trailing, so aerbunnyhurt1 collapses but aerbunny1hurt would not. Number your variants at the end of the name and nesting them in subfolders, like mobs/aerbunny/, is fine; the autoloader walks the whole tree.

That's the soundtrack: drop files in three folders, and music, records, effects, and random variants all fall out of the path names. With the world now lit, named, and audible, the last big system is the ground itself. Next, Chapter 20, New Lands, where we generate custom terrain.