Server-driven audio playback for NeoForge mods.
Acoustic lets a server-side mod start, stop, and update custom OGG sounds on
selected clients. It is built for ambience, scripted music, boss themes, events,
and other cases where vanilla sounds.json playback is too limited.
- Server-side API for targeted sound playback.
- Direct OGG streaming from mod assets.
- Loop counts, infinite loops, start offsets, and end offsets.
- Fade in, fade out, runtime volume changes, and runtime pitch changes.
- Continuous positional audio for world positions, entities, and attachments.
- Extensible emitter types for models, bones, and third-party render libraries.
- Optional player predicates evaluated on the server before packets are sent.
- Music arbitration for exclusive tracks that can fade or block vanilla music.
- Kotlin DSL and Java-friendly entry points.
- Minecraft
1.21.1 - NeoForge
21.1.x - Kotlin for Forge
5.9+
Acoustic is a library-style mod. It does not add gameplay content by itself; it provides an API that other mods and server logic can use.
Place audio files in the standard Minecraft resource location layout:
src/main/resources/assets/<namespace>/sounds/<path>.ogg
For example, this resource:
src/main/resources/assets/mymod/sounds/ambient/cave.ogg
is played with:
ResourceLocation.fromNamespaceAndPath("mymod", "ambient/cave")val sound = ResourceLocation.fromNamespaceAndPath("mymod", "ambient/cave")
val instanceId = Acoustic.soundManager
.play(sound, players)
.loop()
.fadeIn(2f)
.fadeOut(3f)
.volume(0.8f)
.condition { player -> player.isUnderWater }
.execute()
Acoustic.soundManager
.update(instanceId, players)
.volume(0.4f, transition = 1.5f)
.pitch(0.8f, transition = 0.25f)
.execute()
Acoustic.soundManager
.stop(instanceId, players)
.fadeOut(2f)
.execute()ResourceLocation sound = ResourceLocation.fromNamespaceAndPath("mymod", "ambient/cave");
String instanceId = AcousticApi.play(sound, player)
.loop()
.fadeIn(2.0F)
.fadeOut(3.0F)
.volume(0.8F)
.condition(target -> target.isUnderWater())
.execute();
AcousticApi.update(instanceId, player)
.volume(0.4F, 1.5F)
.pitch(0.8F, 0.25F)
.execute();
AcousticApi.stop(instanceId, player)
.fadeOut(2.0F)
.execute();loop()repeats forever.loop(1)plays once.loop(N)plays exactlyNtimes.start(seconds)starts inside the file.end(seconds)stops at a file offset; negative values mean end of file.fadeIn(seconds)andfadeOut(seconds)apply to the active segment.fadeIn(seconds, repeatOnLoop = false)applies fade-in only to the first loop cycle;fadeOut(seconds, repeatOnLoop = false)applies fade-out only to the final loop cycle.exclusive()lets a sound participate in music arbitration and block vanilla music until it ends or is stopped.priority(value)lets higher-priority Acoustic tracks fade out lower-priority managed tracks.
Sounds can originate at a fixed world position or follow a moving entity:
OpenAL spatializes mono sources only. Use a mono OGG for every sound that can become positional; stereo OGG remains suitable for listener-relative playback.
Acoustic.soundManager
.play(sound, players)
.at(10.0, 64.0, -4.0)
.range(48f)
.execute()
Acoustic.soundManager
.play(sound, players)
.follow(entity, AcousticEntityAnchor.CENTER)
.range(32f)
.execute()Standard Minecraft EntityAttachment points are supported as well:
Acoustic.soundManager
.play(sound, players)
.attachTo(entity, EntityAttachment.NAME_TAG)
.execute()Changing the source of an active sound does not restart its OGG stream:
Acoustic.soundManager
.update(instanceId, players)
.follow(otherEntity)
.execute()A renderer that already calculates a bone's world position can publish it through the push API. The attachment ID must be derived identically on the server and client.
val attachmentId = ResourceLocation.fromNamespaceAndPath(
"mymod",
"entity/$entityUuid/bone/engine",
)
// Client: keep this handle for the lifetime of the model or attachment owner.
val handle = AcousticClientAttachments.register(attachmentId)
handle.update(boneWorldPosition)
// Server: playback follows the published attachment.
Acoustic.soundManager
.play(sound, players)
.attachTo(AcousticAttachments.named(attachmentId))
.sourceTimeout(5f)
.execute()
// Client: close when the owner is permanently removed.
handle.close()markTemporarilyUnavailable() can be used when a model was not evaluated due
to culling. A playing sound keeps its last position instead of being interrupted.
Before the first position arrives, playback and its timeline wait for the source;
sourceTimeout(0f) disables that wait limit.
For integrations that need pull-based or otherwise custom behavior, define an
AcousticEmitterType<T> with a Codec<T> and register its client factory through
AcousticClientEmitters.register(type, factory). Trackers report Relative,
Available, Pending, TemporarilyUnavailable, or Removed, so Acoustic does
not need a direct dependency on the model library.
Use the bundled Gradle wrapper:
.\gradlew.bat buildThe release jar is produced under build/libs.
