Sound engine running full steam

- Play at most 4 steam engine sounds at once per side of a boiler
- 3 seems to be too few for this case as there's still a perceptible
  difference between 3 and 4 sounds per side
- Track which positions want to play the sound in any given rolling
  window of ticks, and roll dice to decide which positions actually get
  to play
- Introduce SoundPool to track that and explain the perception stuffs
This commit is contained in:
Jozufozu 2024-12-24 15:46:04 -08:00
parent 628d1cfba9
commit cb19df09c1
3 changed files with 130 additions and 8 deletions

View file

@ -1,6 +1,7 @@
package com.simibubi.create.content.fluids.tank;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -8,6 +9,7 @@ import java.util.Set;
import org.jetbrains.annotations.NotNull;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllSoundEvents;
import com.simibubi.create.Create;
import com.simibubi.create.content.decoration.steamWhistle.WhistleBlock;
import com.simibubi.create.content.decoration.steamWhistle.WhistleBlockEntity;
@ -29,6 +31,8 @@ import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
@ -65,10 +69,25 @@ public class BoilerData {
public LerpedFloat gauge = LerpedFloat.linear();
// client only sound control
// re-use the same lambda for each side
private final SoundPool.Sound sound = (level, pos) -> {
float volume = 3f / Math.max(2, attachedEngines / 6);
float pitch = 1.18f - level.random.nextFloat() * .25f;
level.playLocalSound(pos.getX(), pos.getY(), pos.getZ(),
SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, volume, pitch, false);
AllSoundEvents.STEAM.playAt(level, pos, volume / 16, .8f, false);
};
// separate pools for each side so they sound distinct when standing at corners of the boiler
private final EnumMap<Direction, SoundPool> pools = new EnumMap<>(Direction.class);
public void tick(FluidTankBlockEntity controller) {
if (!isActive())
return;
if (controller.getLevel().isClientSide) {
pools.values().forEach(p -> p.play(controller.getLevel()));
gauge.tickChaser();
float current = gauge.getValue(1);
if (current > 1 && Create.RANDOM.nextFloat() < 1 / 2f)
@ -105,6 +124,15 @@ public class BoilerData {
controller.notifyUpdate();
}
public void queueSoundOnSide(BlockPos pos, Direction side) {
SoundPool pool = pools.get(side);
if (pool == null) {
pool = new SoundPool(4, 2, sound);
pools.put(side, pool);
}
pool.queueAt(pos);
}
public int getTheoreticalHeatLevel() {
return activeHeat;
}

View file

@ -0,0 +1,101 @@
package com.simibubi.create.content.fluids.tank;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import net.minecraft.world.level.Level;
/**
* One person walking sounds like one person walking, and you can easily distinguish where they are.
*
* <br>With two people walking, you can still pick out which footsteps belong to which person.
*
* <br>Try and listen to three people walking in a group, however, and you'll find that you can't distinguish
* individual footsteps anymore. You now just hear the sound of a group of people walking.
*
* <p>You'll likely find that you perceive any number of people walking in a group as a single distinguishable sound.
* This class is a helper to take advantage of that for sound effects in Create to avoid saturating the sound engine
* without a perceptible loss in quality.
*
* <p>NOTE: It's up to the user of this class to decide how to group sounds such that they are perceived as a single
* sound. There are no spatial calculations made here.
*/
public class SoundPool {
/**
* The maximum number of sounds that can be played at once.
*/
private final int maxConcurrent;
/**
* The number of ticks to wait before playing sounds. Useful if sounds are queued across many block entities,
* and you don't have control over the tick order.
*/
private final int mergeTicks;
private final Sound sound;
private final LongList queuedPositions = new LongArrayList();
private final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
private int ticks = 0;
public SoundPool(int maxConcurrent, int mergeTicks, Sound sound) {
this.maxConcurrent = maxConcurrent;
this.sound = sound;
this.mergeTicks = mergeTicks;
}
public void queueAt(BlockPos pos) {
queueAt(pos.asLong());
}
public void queueAt(long pos) {
queuedPositions.add(pos);
}
public void play(Level level) {
if (queuedPositions.isEmpty()) {
return;
}
ticks++;
if (ticks < mergeTicks) {
// Wait for more sounds to be queued in further ticks.
return;
}
ticks = 0;
var numberOfPositions = queuedPositions.size();
if (numberOfPositions <= maxConcurrent) {
// Fewer sound positions than maxConcurrent, play them all.
for (long pos : queuedPositions) {
playAt(level, pos);
}
} else {
// Roll for n random positions and play there.
while (!queuedPositions.isEmpty() && queuedPositions.size() > numberOfPositions - maxConcurrent) {
rollNextPosition(level);
}
}
queuedPositions.clear();
}
private void rollNextPosition(Level level) {
int index = level.random.nextInt(queuedPositions.size());
long pos = queuedPositions.removeLong(index);
playAt(level, pos);
}
private void playAt(Level level, long pos) {
sound.playAt(level, this.pos.set(pos));
}
public interface Sound {
void playAt(Level level, Vec3i pos);
}
}

View file

@ -6,7 +6,6 @@ import java.util.List;
import javax.annotation.Nullable;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllSoundEvents;
import com.simibubi.create.content.contraptions.bearing.WindmillBearingBlockEntity.RotationDirection;
import com.simibubi.create.content.equipment.goggles.IHaveGoggleInformation;
import com.simibubi.create.content.fluids.tank.FluidTankBlockEntity;
@ -26,8 +25,6 @@ import net.minecraft.core.Direction;
import net.minecraft.core.Direction.Axis;
import net.minecraft.core.Direction.AxisDirection;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
@ -208,11 +205,7 @@ public class SteamEngineBlockEntity extends SmartBlockEntity implements IHaveGog
if (sourceBE != null) {
FluidTankBlockEntity controller = sourceBE.getControllerBE();
if (controller != null && controller.boiler != null) {
float volume = 3f / Math.max(2, controller.boiler.attachedEngines / 6);
float pitch = 1.18f - level.random.nextFloat() * .25f;
level.playLocalSound(worldPosition.getX(), worldPosition.getY(), worldPosition.getZ(),
SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, volume, pitch, false);
AllSoundEvents.STEAM.playAt(level, worldPosition, volume / 16, .8f, false);
controller.boiler.queueSoundOnSide(worldPosition, SteamEngineBlock.getFacing(getBlockState()));
}
}