From f11366088c4bd876b34940d90414d349e009b17a Mon Sep 17 00:00:00 2001 From: root Date: Tue, 4 Aug 2026 05:55:41 +0200 Subject: [PATCH] fix: prevent overlapping fade and loop playback --- README.md | 2 +- .../ambientaudio/service/PlaybackService.java | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 73daeb4..ad3447e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ playback: loop-restart-ticks: 2680 ``` -When a selected zone remains selected, Ambient Audio Zone normally sends no new sound packet. In follow-player mode it re-emits only when the calculated fade changes materially, and it uses an entity-attached packet so the sound follows the player. Leaving starts a configurable grace timer; returning before it expires preserves the existing client playback instead of restarting it. `loop-restart-ticks` is a fallback for sounds whose resource-pack definition does not loop; set it to the exact sound length, while resource-pack `loop: true` remains the seamless option. Reloading or editing a zone intentionally replaces its active sound. +When a selected zone remains selected, Ambient Audio Zone normally sends no new sound packet. In follow-player mode it uses rate-limited fade updates and stops the previous packet before re-emitting, preventing layered copies of the sound. Bukkit has no mutable-volume packet, so these updates are necessarily discrete; resource-pack loops are the smoothest option. Leaving starts a configurable grace timer; returning before it expires preserves the existing client playback instead of restarting it. `loop-restart-ticks` is a fallback for sounds whose resource-pack definition does not loop; set it to the exact sound length, while resource-pack `loop: true` remains the seamless option. Reloading or editing a zone intentionally replaces its active sound. ## Resource-pack looping note diff --git a/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java b/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java index a2c4b79..a3a6c0e 100644 --- a/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java +++ b/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java @@ -35,21 +35,21 @@ public final class PlaybackService { float volume = volumeFor(zone, player); if (sound == null) { play(player, zone, volume); - current.put(zone.normalizedName(), new ActiveSound(zone, Long.MAX_VALUE, now, volume)); + current.put(zone.normalizedName(), new ActiveSound(zone, Long.MAX_VALUE, now, now, volume)); } else if (zone.loop() && loopRestartTicks > 0 && now - sound.startedAtTick() >= loopRestartTicks) { play(player, zone, volume); - current.put(zone.normalizedName(), new ActiveSound(zone, Long.MAX_VALUE, now, volume)); - } else if (followPlayer && Math.abs(sound.volume() - volume) >= 0.05f) { - // A sound packet has no mutable volume. Re-emit only when the fade changed materially; - // inside the zone the packet remains attached to the player and is not restarted. + current.put(zone.normalizedName(), new ActiveSound(zone, Long.MAX_VALUE, now, now, volume)); + } else if (followPlayer && now - sound.lastPacketTick() >= 40 && Math.abs(sound.volume() - volume) >= 0.10f) { + // Bukkit has no volume-update packet. Rate-limit fade packets and stop the previous + // packet first; otherwise repeated entity sound packets overlap on the client. play(player, zone, volume); - current.put(zone.normalizedName(), new ActiveSound(zone, Long.MAX_VALUE, sound.startedAtTick(), volume)); + current.put(zone.normalizedName(), new ActiveSound(zone, Long.MAX_VALUE, sound.startedAtTick(), now, volume)); } } for (Iterator> it = current.entrySet().iterator(); it.hasNext();) { Map.Entry entry = it.next(); ActiveSound sound = entry.getValue(); - if (desired.contains(entry.getKey())) { entry.setValue(new ActiveSound(sound.zone(), Long.MAX_VALUE, sound.startedAtTick(), sound.volume())); continue; } - if (sound.stopAtTick() == Long.MAX_VALUE) { entry.setValue(new ActiveSound(sound.zone(), now + leaveGraceTicks, sound.startedAtTick(), sound.volume())); continue; } + if (desired.contains(entry.getKey())) { entry.setValue(new ActiveSound(sound.zone(), Long.MAX_VALUE, sound.startedAtTick(), sound.lastPacketTick(), sound.volume())); continue; } + if (sound.stopAtTick() == Long.MAX_VALUE) { entry.setValue(new ActiveSound(sound.zone(), now + leaveGraceTicks, sound.startedAtTick(), sound.lastPacketTick(), sound.volume())); continue; } if (now >= sound.stopAtTick()) { player.stopSound(sound.zone().sound(), sound.zone().category()); it.remove(); } } if (current.isEmpty()) active.remove(player.getUniqueId()); @@ -63,6 +63,9 @@ public final class PlaybackService { } active.clear(); } private void play(Player player, AudioZone zone, float volume) { + // A sound event is not a handle. Stop the matching event before re-emitting it so + // fade updates and loop restarts cannot layer copies of the same sound. + player.stopSound(zone.sound(), zone.category()); if (followPlayer) player.playSound(player, zone.sound(), zone.category(), volume, zone.pitch()); else player.playSound(zone.originLocation(player.getWorld()), zone.sound(), zone.category(), zone.volume(), zone.pitch()); } @@ -73,5 +76,5 @@ public final class PlaybackService { double multiplier = fade == 0 ? (distance == 0 ? 1 : 0) : Math.max(0, 1 - distance / fade); return (float) Math.max(0.001, zone.volume() * multiplier); } - private record ActiveSound(AudioZone zone, long stopAtTick, long startedAtTick, float volume) { } + private record ActiveSound(AudioZone zone, long stopAtTick, long startedAtTick, long lastPacketTick, float volume) { } }