Add comparator output, tab-completion, soundlist/soundstopall, and OpenAL low-pass occlusion/underwater filter
This commit is contained in:
@@ -58,6 +58,7 @@ import javazoom.jl.decoder.SampleBuffer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.entity.player.ClientPlayerEntity;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.BlockRayTraceResult;
|
||||
import net.minecraft.util.math.RayTraceContext;
|
||||
@@ -65,6 +66,8 @@ import net.minecraft.util.math.RayTraceResult;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.openal.AL10;
|
||||
import org.lwjgl.openal.ALC10;
|
||||
import org.lwjgl.openal.EXTEfx;
|
||||
import org.lwjgl.stb.STBVorbis;
|
||||
import org.lwjgl.stb.STBVorbisInfo;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
@@ -200,6 +203,14 @@ public final class AudioPlaybackService {
|
||||
return;
|
||||
}
|
||||
SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", sourceId, "stop source={} buffer={}", src.source, src.buffer);
|
||||
if (src.directFilter > 0) {
|
||||
try {
|
||||
AL10.alSourcei(src.source, EXTEfx.AL_DIRECT_FILTER, EXTEfx.AL_FILTER_NULL);
|
||||
EXTEfx.alDeleteFilters(src.directFilter);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
src.directFilter = 0;
|
||||
}
|
||||
AL10.alSourceStop((int)src.source);
|
||||
AL10.alDeleteSources((int)src.source);
|
||||
if (src.reverbSource > 0) {
|
||||
@@ -209,6 +220,13 @@ public final class AudioPlaybackService {
|
||||
AL10.alDeleteBuffers((int)src.buffer);
|
||||
}
|
||||
|
||||
public synchronized void stopAll() {
|
||||
ArrayList<String> all = new ArrayList<>(this.active.keySet());
|
||||
for (String id : all) {
|
||||
this.stop(id);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isPlaying(String sourceId) {
|
||||
return this.active.containsKey(sourceId);
|
||||
}
|
||||
@@ -223,6 +241,7 @@ public final class AudioPlaybackService {
|
||||
return;
|
||||
}
|
||||
Vector3d playerPos = player.position();
|
||||
boolean underwater = player.isEyeInFluid(FluidTags.WATER);
|
||||
ArrayList<String> remove = new ArrayList<String>();
|
||||
int topPriority = Integer.MIN_VALUE;
|
||||
String dominantCategory = "ambient";
|
||||
@@ -248,9 +267,13 @@ public final class AudioPlaybackService {
|
||||
float gain = this.computeAttenuatedGain(src, playerPos);
|
||||
float occlusionFactor = this.computeOcclusionFactor(src, playerPos);
|
||||
gain *= occlusionFactor;
|
||||
if (underwater) {
|
||||
gain *= 0.72f;
|
||||
}
|
||||
gain = this.applyFades(src, gain);
|
||||
gain = this.applyPriorityDucking(src, gain, topPriority, dominantCategory);
|
||||
AL10.alSourcef((int)src.source, (int)4106, (float)Math.max(0.0f, gain));
|
||||
this.updateDirectFilter(src, occlusionFactor, underwater);
|
||||
float pitch = src.settings.pitch;
|
||||
pitch *= this.computePitchModulation(src);
|
||||
if (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f && !"voice".equals(src.settings.category)) {
|
||||
@@ -276,6 +299,63 @@ public final class AudioPlaybackService {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean efxSupported = false;
|
||||
private static boolean efxChecked = false;
|
||||
|
||||
private static boolean isEfxSupported() {
|
||||
if (!efxChecked) {
|
||||
efxChecked = true;
|
||||
try {
|
||||
long context = ALC10.alcGetCurrentContext();
|
||||
long device = ALC10.alcGetContextsDevice(context);
|
||||
efxSupported = ALC10.alcIsExtensionPresent(device, "ALC_EXT_EFX");
|
||||
} catch (Throwable t) {
|
||||
efxSupported = false;
|
||||
}
|
||||
}
|
||||
return efxSupported;
|
||||
}
|
||||
|
||||
private void updateDirectFilter(PlayingSource src, float occlusionFactor, boolean underwater) {
|
||||
if (!isEfxSupported()) {
|
||||
return;
|
||||
}
|
||||
boolean needFilter = (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f) || underwater;
|
||||
if (!needFilter) {
|
||||
if (src.directFilter > 0) {
|
||||
try {
|
||||
AL10.alSourcei(src.source, EXTEfx.AL_DIRECT_FILTER, EXTEfx.AL_FILTER_NULL);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (src.directFilter <= 0) {
|
||||
src.directFilter = EXTEfx.alGenFilters();
|
||||
if (src.directFilter > 0) {
|
||||
EXTEfx.alFilteri(src.directFilter, EXTEfx.AL_FILTER_TYPE, EXTEfx.AL_FILTER_LOWPASS);
|
||||
}
|
||||
}
|
||||
if (src.directFilter > 0) {
|
||||
float gainHF = 1.0f;
|
||||
float gainLF = 1.0f;
|
||||
if (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f) {
|
||||
float str = Math.max(0.0f, Math.min(1.0f, src.settings.occlusionStrength));
|
||||
gainHF = Math.max(0.08f, 1.0f - str * 0.82f);
|
||||
}
|
||||
if (underwater) {
|
||||
gainHF *= 0.18f;
|
||||
gainLF *= 0.75f;
|
||||
}
|
||||
EXTEfx.alFilterf(src.directFilter, EXTEfx.AL_LOWPASS_GAIN, gainLF);
|
||||
EXTEfx.alFilterf(src.directFilter, EXTEfx.AL_LOWPASS_GAINHF, gainHF);
|
||||
AL10.alSourcei(src.source, EXTEfx.AL_DIRECT_FILTER, src.directFilter);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private float computePitchModulation(PlayingSource src) {
|
||||
if (src.settings.pitchMode == null || src.settings.pitchMode == PitchModulationMode.STATIC) {
|
||||
return 1.0f;
|
||||
@@ -653,6 +733,7 @@ public final class AudioPlaybackService {
|
||||
int fadeOutTicks;
|
||||
int reverbSource;
|
||||
int elapsedTicks;
|
||||
int directFilter;
|
||||
long lastLiveUpdateTick = Long.MIN_VALUE;
|
||||
|
||||
PlayingSource(String sourceId, int source, int buffer, BlockPos pos, SoundSettings settings, long startedAtTick) {
|
||||
|
||||
@@ -40,9 +40,12 @@ import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -60,18 +63,32 @@ import net.minecraftforge.fml.network.PacketDistributor;
|
||||
public final class ModCommands {
|
||||
private static final Map<String, DiagRequest> DIAG_REQUESTS = new ConcurrentHashMap<String, DiagRequest>();
|
||||
|
||||
private static final SuggestionProvider<CommandSource> SOUND_SUGGESTIONS = (ctx, builder) -> {
|
||||
ServerSoundRepository repo = ServerSoundRepository.get();
|
||||
if (repo != null) {
|
||||
for (SoundManifestEntry entry : repo.getManifest()) {
|
||||
if (entry.flags == 0) {
|
||||
builder.suggest(entry.soundId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.buildFuture();
|
||||
};
|
||||
|
||||
private ModCommands() {
|
||||
}
|
||||
|
||||
public static void register(CommandDispatcher<CommandSource> dispatcher) {
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"playsoundcustom").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).then(Commands.argument((String)"pos", (ArgumentType)Vec3Argument.vec3()).executes(ctx -> ModCommands.playCustom((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound"), Vec3Argument.getVec3((CommandContext)ctx, (String)"pos"))))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"playsoundcustom").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).then(Commands.argument((String)"pos", (ArgumentType)Vec3Argument.vec3()).executes(ctx -> ModCommands.playCustom((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound"), Vec3Argument.getVec3((CommandContext)ctx, (String)"pos"))))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"stopsoundcustom").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.stopCustom((CommandContext<CommandSource>)ctx)));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundstopall").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.stopAllCustom((CommandContext<CommandSource>)ctx)));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundlist").requires(src -> src.hasPermission(0))).executes(ctx -> ModCommands.listSounds((CommandContext<CommandSource>)ctx)));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddebug").requires(src -> src.hasPermission(2))).then(Commands.literal((String)"on").executes(ctx -> ModCommands.setDebug((CommandContext<CommandSource>)ctx, true)))).then(Commands.literal((String)"off").executes(ctx -> ModCommands.setDebug((CommandContext<CommandSource>)ctx, false)))).then(Commands.literal((String)"level").then(Commands.argument((String)"level", (ArgumentType)IntegerArgumentType.integer((int)1, (int)3)).executes(ctx -> ModCommands.setDebugLevel((CommandContext<CommandSource>)ctx, IntegerArgumentType.getInteger((CommandContext)ctx, (String)"level"))))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundreload").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.reload((CommandContext<CommandSource>)ctx)));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundsync").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).executes(ctx -> ModCommands.syncSound((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundsync").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).executes(ctx -> ModCommands.syncSound((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundcacheclear").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.clearCache((CommandContext<CommandSource>)ctx)));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiag").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).executes(ctx -> ModCommands.soundDiag((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound")))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiagclient").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).executes(ctx -> ModCommands.soundDiagClient((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiag").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).executes(ctx -> ModCommands.soundDiag((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound")))));
|
||||
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiagclient").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).executes(ctx -> ModCommands.soundDiagClient((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
|
||||
}
|
||||
|
||||
private static int playCustom(CommandContext<CommandSource> ctx, String sound, Vector3d pos) {
|
||||
@@ -96,6 +113,32 @@ public final class ModCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int stopAllCustom(CommandContext<CommandSource> ctx) {
|
||||
for (ServerPlayerEntity player : ((CommandSource)ctx.getSource()).getLevel().players()) {
|
||||
NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), (Object)new S2CStopSoundPacket("*"));
|
||||
}
|
||||
((CommandSource)ctx.getSource()).sendSuccess((ITextComponent)new StringTextComponent("§a[SoundMod] Все пользовательские звуки принудительно остановлены."), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int listSounds(CommandContext<CommandSource> ctx) {
|
||||
CommandSource src = (CommandSource) ctx.getSource();
|
||||
List<SoundManifestEntry> list = ServerSoundRepository.get().getManifest();
|
||||
if (list.isEmpty()) {
|
||||
src.sendSuccess((ITextComponent)new StringTextComponent("§7[SoundMod] Список звуков пуст. Добавьте .ogg, .wav или .mp3 в customsounds/"), false);
|
||||
return 0;
|
||||
}
|
||||
src.sendSuccess((ITextComponent)new StringTextComponent("§6=== Список доступных звуков (" + list.size() + ") ==="), false);
|
||||
for (SoundManifestEntry e : list) {
|
||||
String sizeStr = (e.size >= 1024 * 1024)
|
||||
? String.format(Locale.ROOT, "%.2f MB", e.size / (1024.0 * 1024.0))
|
||||
: String.format(Locale.ROOT, "%.1f KB", e.size / 1024.0);
|
||||
String status = e.flags == 0 ? "§a[OK]" : "§c[Ошибка]";
|
||||
src.sendSuccess((ITextComponent)new StringTextComponent("§e• §f" + e.soundId + " §7(" + e.relativePath + ", " + sizeStr + ") " + status), false);
|
||||
}
|
||||
return list.size();
|
||||
}
|
||||
|
||||
private static int setDebug(CommandContext<CommandSource> ctx, boolean enabled) {
|
||||
SoundDebug.setEnabled(enabled);
|
||||
int levelCode = SoundDebug.getLevel().code();
|
||||
|
||||
@@ -80,6 +80,28 @@ public class SoundPlayerBlock extends Block {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnalogOutputSignal(BlockState state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAnalogOutputSignal(BlockState state, World world, BlockPos pos) {
|
||||
TileEntity te = world.getBlockEntity(pos);
|
||||
if (te instanceof SoundPlayerTileEntity) {
|
||||
SoundPlayerTileEntity soundTe = (SoundPlayerTileEntity) te;
|
||||
if (soundTe.isPlaying()) {
|
||||
if (soundTe.getScenario() != null && !soundTe.getScenario().steps.isEmpty()) {
|
||||
int total = soundTe.getScenario().steps.size();
|
||||
int cur = soundTe.getScenarioStepIndex();
|
||||
return Math.max(1, Math.min(15, (int) Math.ceil(((double) (cur + 1) / total) * 15.0)));
|
||||
}
|
||||
return 15;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public void animateTick(BlockState state, World world, BlockPos pos, Random rand) {
|
||||
|
||||
@@ -292,6 +292,7 @@ ITickableTileEntity {
|
||||
SoundDebug.log(SoundDebug.Level.L2, "TRIGGER", this.debugPosId(), "stop requested sourceId=sound_player:{}", this.worldPosition.asLong());
|
||||
NetworkHandler.CHANNEL.send(PacketDistributor.TRACKING_CHUNK.with(() -> this.level.getChunkAt(this.worldPosition)), (Object)new S2CBlockStateSyncPacket(this.worldPosition, this.save(new CompoundNBT()), false));
|
||||
this.setChanged();
|
||||
this.notifyNeighbors();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +544,7 @@ ITickableTileEntity {
|
||||
this.playing = true;
|
||||
SoundDebug.log(SoundDebug.Level.L1, "TRIGGER", this.debugPosId(), "play requested sourceId=sound_player:{} soundId={}", this.worldPosition.asLong(), this.settings.soundId);
|
||||
NetworkHandler.CHANNEL.send(PacketDistributor.TRACKING_CHUNK.with(() -> this.level.getChunkAt(this.worldPosition)), (Object)new S2CBlockStateSyncPacket(this.worldPosition, this.save(new CompoundNBT()), true));
|
||||
this.notifyNeighbors();
|
||||
}
|
||||
|
||||
private void emitScenarioStepPlay(String soundId) {
|
||||
@@ -551,6 +553,17 @@ ITickableTileEntity {
|
||||
this.playing = true;
|
||||
SoundDebug.log(SoundDebug.Level.L1, "TRIGGER", this.debugPosId(), "scenario step play sourceId=sound_player:{} stepSoundId={} nextDelay={}", this.worldPosition.asLong(), soundId, this.scenarioWaitTicks);
|
||||
NetworkHandler.CHANNEL.send(PacketDistributor.TRACKING_CHUNK.with(() -> this.level.getChunkAt(this.worldPosition)), (Object)new S2CPlaySoundPacket("sound_player:" + this.worldPosition.asLong(), this.worldPosition, stepSettings));
|
||||
this.notifyNeighbors();
|
||||
}
|
||||
|
||||
public int getScenarioStepIndex() {
|
||||
return this.scenarioStepIndex;
|
||||
}
|
||||
|
||||
private void notifyNeighbors() {
|
||||
if (this.level != null && !this.level.isClientSide) {
|
||||
this.level.updateNeighborsAt(this.worldPosition, this.getBlockState().getBlock());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSourceRuntimeActive() {
|
||||
|
||||
@@ -28,7 +28,13 @@ public class S2CStopSoundPacket {
|
||||
}
|
||||
|
||||
public static void handle(S2CStopSoundPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> AudioPlaybackService.get().stop(msg.sourceId));
|
||||
ctx.get().enqueueWork(() -> {
|
||||
if (msg.sourceId == null || msg.sourceId.isEmpty() || "*".equals(msg.sourceId)) {
|
||||
AudioPlaybackService.get().stopAll();
|
||||
} else {
|
||||
AudioPlaybackService.get().stop(msg.sourceId);
|
||||
}
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user