/* * Decompiled with CFR 0.152. * * Could not load the following classes: * net.minecraft.client.Minecraft * net.minecraft.client.entity.player.ClientPlayerEntity * net.minecraft.entity.Entity * net.minecraft.util.math.BlockPos * net.minecraft.util.math.BlockRayTraceResult * net.minecraft.util.math.RayTraceContext * net.minecraft.util.math.RayTraceContext$BlockMode * net.minecraft.util.math.RayTraceContext$FluidMode * net.minecraft.util.math.RayTraceResult$Type * net.minecraft.util.math.vector.Vector3d * org.lwjgl.BufferUtils * org.lwjgl.openal.AL10 * org.lwjgl.stb.STBVorbis * org.lwjgl.stb.STBVorbisInfo * org.lwjgl.system.MemoryStack * org.lwjgl.system.MemoryUtil */ package com.example.soundmod.client.audio; import com.example.soundmod.SoundMod; import com.example.soundmod.common.AttenuationMode; import com.example.soundmod.common.PitchModulationMode; import com.example.soundmod.common.SoundSettings; import com.example.soundmod.core.ModConfig; import com.example.soundmod.core.SoundDebug; import com.example.soundmod.net.NetworkHandler; import com.example.soundmod.net.packet.C2SRequestSoundPacket; import com.example.soundmod.sounddata.client.ClientSoundCache; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; import java.nio.ShortBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.UnsupportedAudioFileException; import javazoom.jl.decoder.Bitstream; import javazoom.jl.decoder.Decoder; import javazoom.jl.decoder.Header; import javazoom.jl.decoder.JavaLayerException; 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.util.math.BlockPos; import net.minecraft.util.math.BlockRayTraceResult; import net.minecraft.util.math.RayTraceContext; 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.stb.STBVorbis; import org.lwjgl.stb.STBVorbisInfo; import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; public final class AudioPlaybackService { private static final int REPLAY_DEBOUNCE_TICKS = 6; private static final AudioPlaybackService INSTANCE = new AudioPlaybackService(); private final Map active = new HashMap(); public static AudioPlaybackService get() { return INSTANCE; } private AudioPlaybackService() { } public synchronized void play(String sourceId, BlockPos pos, SoundSettings settings) { long gameTime; SoundSettings safe = SoundSettings.sanitizedCopy(settings); if (safe.volume != settings.volume || safe.pitch != settings.pitch || safe.radius != settings.radius) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "sanitized params vol={}=>{} pitch={}=>{} radius={}=>{}", Float.valueOf(settings.volume), Float.valueOf(safe.volume), Float.valueOf(settings.pitch), Float.valueOf(safe.pitch), Float.valueOf(settings.radius), Float.valueOf(safe.radius)); } SoundDebug.log(SoundDebug.Level.L1, "PLAYBACK", sourceId, "play request soundId={} pos={} {} {} vol={} pitch={} loop={} radius={}", safe.soundId, pos.getX(), pos.getY(), pos.getZ(), Float.valueOf(safe.volume), Float.valueOf(safe.pitch), safe.loop, Float.valueOf(safe.radius)); Path file = ClientSoundCache.get().resolveBySoundId(safe.soundId); if (file == null) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "cache miss soundId={} -> request from server", safe.soundId); NetworkHandler.CHANNEL.sendToServer((Object)new C2SRequestSoundPacket(safe.soundId)); return; } SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", sourceId, "resolved cache file={}", file); PlayingSource current = this.active.get(sourceId); long l = gameTime = Minecraft.getInstance().level != null ? Minecraft.getInstance().level.getGameTime() : Long.MIN_VALUE; if (!safe.crossfade && current != null && current.settings.soundId.equals(safe.soundId) && gameTime != Long.MIN_VALUE && gameTime - current.startedAtTick <= 6L) { SoundDebug.log(SoundDebug.Level.L3, "PLAYBACK", sourceId, "replay suppressed soundId={} dt={}", safe.soundId, gameTime - current.startedAtTick); return; } if (safe.crossfade && current != null) { current.fadeOutTicks = Math.max(1, safe.fadeOutTicks); SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", sourceId, "crossfade enabled fadeOutTicks={}", safe.fadeOutTicks); } else { this.stop(sourceId); } try { this.enforceActiveLimit(safe.priority); DecodedPcm pcm = this.decodeAudio(file); int buffer = AL10.alGenBuffers(); int alErr1 = AL10.alGetError(); if (alErr1 != 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "alGenBuffers error={}", alErr1); } int format = pcm.channels == 1 ? 4353 : 4355; AL10.alBufferData((int)buffer, (int)format, (ShortBuffer)pcm.samples, (int)pcm.sampleRate); int alErr2 = AL10.alGetError(); if (alErr2 != 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "alBufferData error={} format={} channels={} sampleRate={} samples={}", alErr2, format, pcm.channels, pcm.sampleRate, pcm.samples.remaining()); } int source = AL10.alGenSources(); int alErr3 = AL10.alGetError(); if (alErr3 != 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "alGenSources error={}", alErr3); } AL10.alSourcei((int)source, (int)4105, (int)buffer); AL10.alSourcef((int)source, (int)4106, (float)safe.volume); AL10.alSourcef((int)source, (int)4099, (float)safe.pitch); AL10.alSourcei((int)source, (int)4103, (int)(safe.loop ? 1 : 0)); this.configureSpatial(source, pos, safe.use3d); AL10.alSourcePlay((int)source); int alErr4 = AL10.alGetError(); if (alErr4 != 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "alSourcePlay error={}", alErr4); } SoundDebug.log(SoundDebug.Level.L1, "PLAYBACK", sourceId, "play started source={} buffer={} channels={} sampleRate={} frames={}", source, buffer, pcm.channels, pcm.sampleRate, pcm.frames); PlayingSource src = new PlayingSource(sourceId, source, buffer, pos, safe, gameTime); src.fadeInTicks = safe.fadeInTicks; src.targetGain = safe.volume; if (safe.reverb && safe.reverbStrength > 0.01f) { src.reverbSource = this.createReverbSource(sourceId, pos, safe, buffer); } if (src.fadeInTicks > 0) { AL10.alSourcef((int)source, (int)4106, (float)0.0f); } this.active.put(sourceId, src); } catch (Exception e) { SoundMod.LOGGER.error("Failed to play custom sound {}", (Object)safe.soundId, (Object)e); SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "play failed soundId={} reason={}", safe.soundId, e.getMessage()); } } public synchronized void updateLive(String sourceId, BlockPos pos, SoundSettings settings) { SoundSettings safe = SoundSettings.sanitizedCopy(settings); PlayingSource current = this.active.get(sourceId); if (current == null) { SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", "LIVE", "source missing -> fallback play sourceId={} soundId={}", sourceId, safe.soundId); this.play(sourceId, pos, safe); return; } if (!current.settings.soundId.equals(safe.soundId)) { SoundDebug.log(SoundDebug.Level.L1, "PLAYBACK", "LIVE", "restart reason=soundId_change sourceId={} old={} new={} crossfade={}", sourceId, current.settings.soundId, safe.soundId, safe.crossfade); if (safe.crossfade) { current.fadeOutTicks = Math.max(1, safe.fadeOutTicks); } else { this.stop(sourceId); } this.play(sourceId, pos, safe); return; } current.settings = safe; current.pos = pos; current.targetGain = safe.volume; current.fadeInTicks = Math.max(0, current.fadeInTicks); this.configureSpatial(current.source, pos, safe.use3d); AL10.alSourcei((int)current.source, (int)4103, (int)(safe.loop ? 1 : 0)); if (safe.reverb && safe.reverbStrength > 0.01f) { if (current.reverbSource <= 0) { current.reverbSource = this.createReverbSource(sourceId, pos, safe, current.buffer); } else { this.configureSpatial(current.reverbSource, pos, safe.use3d); } } else if (current.reverbSource > 0) { AL10.alSourceStop((int)current.reverbSource); AL10.alDeleteSources((int)current.reverbSource); current.reverbSource = 0; } current.lastLiveUpdateTick = this.currentGameTime(); SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", "LIVE", "updated sourceId={} soundId={} vol={} pitch={} radius={} attn={} use3d={} doppler={} occlusion={} reverb={}", sourceId, safe.soundId, Float.valueOf(safe.volume), Float.valueOf(safe.pitch), Float.valueOf(safe.radius), safe.attenuationMode.name(), safe.use3d, safe.doppler, safe.occlusion, safe.reverb); } public synchronized void stop(String sourceId) { PlayingSource src = this.active.remove(sourceId); if (src == null) { return; } SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", sourceId, "stop source={} buffer={}", src.source, src.buffer); AL10.alSourceStop((int)src.source); AL10.alDeleteSources((int)src.source); if (src.reverbSource > 0) { AL10.alSourceStop((int)src.reverbSource); AL10.alDeleteSources((int)src.reverbSource); } AL10.alDeleteBuffers((int)src.buffer); } public synchronized boolean isPlaying(String sourceId) { return this.active.containsKey(sourceId); } public synchronized void tick() { if (this.active.isEmpty()) { return; } Minecraft mc = Minecraft.getInstance(); ClientPlayerEntity player = mc.player; if (player == null || mc.level == null) { return; } Vector3d playerPos = player.position(); ArrayList remove = new ArrayList(); int topPriority = Integer.MIN_VALUE; String dominantCategory = "ambient"; for (PlayingSource src : this.active.values()) { if (src.settings.priority > topPriority) { topPriority = src.settings.priority; dominantCategory = src.settings.category; continue; } if (src.settings.priority != topPriority || this.categoryRank(src.settings.category) <= this.categoryRank(dominantCategory)) continue; dominantCategory = src.settings.category; } for (PlayingSource src : this.active.values()) { src.elapsedTicks++; int state = AL10.alGetSourcei((int)src.source, (int)4112); if (SoundDebug.enabled(SoundDebug.Level.L3)) { SoundDebug.log(SoundDebug.Level.L3, "PLAYBACK", src.sourceId, "tick state={} gainTarget={}", state, Float.valueOf(src.targetGain)); } if (state == 4116 && !src.settings.loop) { remove.add(src.sourceId); continue; } float gain = this.computeAttenuatedGain(src, playerPos); float occlusionFactor = this.computeOcclusionFactor(src, playerPos); gain *= occlusionFactor; 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)); float pitch = src.settings.pitch; pitch *= this.computePitchModulation(src); if (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f && !"voice".equals(src.settings.category)) { pitch *= Math.max(0.9f, 1.0f - src.settings.occlusionStrength * 0.12f); } if (src.settings.doppler && src.settings.use3d) { Vector3d velocity = player.getDeltaMovement(); Vector3d sourceDir = new Vector3d((double)src.pos.getX() + 0.5 - playerPos.x, (double)src.pos.getY() + 0.5 - playerPos.y, (double)src.pos.getZ() + 0.5 - playerPos.z); double len = Math.max(1.0E-4, sourceDir.length()); double radial = (velocity.x * sourceDir.x + velocity.y * sourceDir.y + velocity.z * sourceDir.z) / len; float dopplerPitch = (float)Math.max(0.75, Math.min(1.25, 1.0 + radial * 0.04)); pitch *= dopplerPitch; } pitch = Math.max(0.05f, Math.min(4.0f, pitch)); AL10.alSourcef((int)src.source, (int)4099, (float)pitch); if (src.reverbSource <= 0) continue; float wetGain = Math.max(0.0f, gain * src.settings.reverbStrength * 0.45f); AL10.alSourcef((int)src.reverbSource, (int)4106, (float)wetGain); AL10.alSourcef((int)src.reverbSource, (int)4099, (float)Math.max(0.05f, Math.min(4.0f, pitch * 0.97f))); } for (String id : remove) { this.stop(id); } } private float computePitchModulation(PlayingSource src) { if (src.settings.pitchMode == null || src.settings.pitchMode == PitchModulationMode.STATIC) { return 1.0f; } float speed = Math.max(0.05f, src.settings.pitchModSpeed); float depth = Math.max(0.0f, src.settings.pitchModDepth); float t = (float) src.elapsedTicks / 20.0f; switch (src.settings.pitchMode) { case VIBRATO: { float freq = speed * 5.0f; return 1.0f + (float) Math.sin(t * freq * (2.0 * Math.PI)) * (depth * 0.4f); } case GLITCH: { int stepTicks = Math.max(1, (int)(8.0f / speed)); int step = src.elapsedTicks / stepTicks; int hash = (step * 0x1f1f1f1f) ^ (src.sourceId.hashCode() * 31); int choice = Math.abs(hash) % 7; float glitchFactor = 1.0f; switch (choice) { case 0: glitchFactor = 0.5f; break; case 1: glitchFactor = 0.75f; break; case 2: glitchFactor = 1.25f; break; case 3: glitchFactor = 1.5f; break; case 4: glitchFactor = 2.0f; break; case 5: glitchFactor = 0.9f; break; case 6: glitchFactor = 1.15f; break; } return 1.0f + (glitchFactor - 1.0f) * Math.min(1.5f, depth); } case TAPE_STOP: { float progress = Math.min(1.0f, t * speed * 0.2f); float decay = (1.0f - progress); float factor = 1.0f - (1.0f - decay * decay) * Math.min(1.0f, depth); return Math.max(0.1f, factor); } case RAMP_UP: { float progress = t * speed * 0.25f; return Math.min(4.0f, 1.0f + progress * depth); } case RANDOM_JITTER: { float wow = (float) Math.sin(t * speed * 1.8f * Math.PI) * 0.5f; int noiseHash = (src.elapsedTicks * 1103515245 + 12345); float flutter = (((noiseHash >> 16) & 0x7fff) / 32767.0f - 0.5f); return 1.0f + (wow * 0.7f + flutter * 0.3f) * (depth * 0.25f); } default: return 1.0f; } } private float applyFades(PlayingSource src, float gain) { if (src.fadeInTicks > 0) { int elapsed = src.settings.fadeInTicks - src.fadeInTicks; float alpha = src.settings.fadeInTicks <= 0 ? 1.0f : (float)elapsed / (float)src.settings.fadeInTicks; --src.fadeInTicks; return gain * Math.min(1.0f, alpha); } if (src.fadeOutTicks > 0) { float alpha = (float)src.fadeOutTicks / (float)Math.max(1, src.settings.fadeOutTicks); --src.fadeOutTicks; if (src.fadeOutTicks <= 0) { this.stop(src.sourceId); return 0.0f; } return gain * alpha; } return gain; } private float computeOcclusionFactor(PlayingSource src, Vector3d playerPos) { if (!src.settings.occlusion || !src.settings.use3d) { return 1.0f; } Minecraft mc = Minecraft.getInstance(); BlockRayTraceResult trace = mc.level.clip(new RayTraceContext(playerPos, new Vector3d((double)src.pos.getX() + 0.5, (double)src.pos.getY() + 0.5, (double)src.pos.getZ() + 0.5), RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.NONE, (Entity)mc.player)); if (trace.getType() != RayTraceResult.Type.MISS) { float damp = Math.max(0.0f, 1.0f - src.settings.occlusionStrength); return damp; } return 1.0f; } private float computeAttenuatedGain(PlayingSource src, Vector3d playerPos) { double atten; if (!src.settings.use3d) { return src.targetGain; } double dx = playerPos.x - ((double)src.pos.getX() + 0.5); double dy = playerPos.y - ((double)src.pos.getY() + 0.5); double dz = playerPos.z - ((double)src.pos.getZ() + 0.5); double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); if (src.settings.attenuationMode == AttenuationMode.NONE || src.settings.radius <= 0.1f) { return src.targetGain; } double t = Math.min(1.0, distance / (double)src.settings.radius); switch (src.settings.attenuationMode) { case EXPONENTIAL: { atten = Math.pow(1.0 - t, 2.0); break; } case LOGARITHMIC: { atten = 1.0 - Math.log10(1.0 + 9.0 * t); break; } default: { atten = 1.0 - t; } } return (float)Math.max(0.0, (double)src.targetGain * atten); } public synchronized Collection snapshot() { ArrayList out = new ArrayList(); long nowTick = this.currentGameTime(); for (PlayingSource src : this.active.values()) { int alState = AL10.alGetSourcei((int)src.source, (int)4112); ClientSoundCache.Status status = ClientSoundCache.get().status(src.settings.soundId); boolean liveUpdated = nowTick != Long.MIN_VALUE && src.lastLiveUpdateTick != Long.MIN_VALUE && nowTick - src.lastLiveUpdateTick <= 40L; out.add(new PlayingSourceView(src.sourceId, src.settings.soundId, (double)src.pos.getX() + 0.5, (double)src.pos.getY() + 0.5, (double)src.pos.getZ() + 0.5, src.settings.radius, src.settings.priority, status.name(), alState, liveUpdated)); } return out; } private float applyPriorityDucking(PlayingSource src, float gain, int topPriority, String dominantCategory) { if (topPriority == Integer.MIN_VALUE || src.settings.priority >= topPriority) { return gain; } double strength = (Double)ModConfig.DUCKING_STRENGTH.get(); double base = Math.max(0.05, 1.0 - strength); double categoryFactor = this.categoryDuckingFactor(src.settings.category, dominantCategory); return (float)((double)gain * base * categoryFactor); } private double categoryDuckingFactor(String sourceCategory, String dominantCategory) { boolean sourceForeground = this.isForegroundCategory(sourceCategory); boolean dominantForeground = this.isForegroundCategory(dominantCategory); if (dominantForeground && !sourceForeground) { return 0.55; } if (!dominantForeground && sourceForeground) { return 0.9; } return 0.78; } private boolean isForegroundCategory(String category) { return "voice".equals(category) || "effect".equals(category); } private int categoryRank(String category) { if ("voice".equals(category)) { return 4; } if ("effect".equals(category)) { return 3; } if ("ambient".equals(category)) { return 2; } if ("music".equals(category)) { return 1; } return 0; } private void enforceActiveLimit(int incomingPriority) { int maxActive = (Integer)ModConfig.MAX_ACTIVE_SOURCES.get(); if (this.active.size() < maxActive) { return; } String victimId = null; PlayingSource victim = null; for (Map.Entry e : this.active.entrySet()) { PlayingSource src = e.getValue(); if (victim != null && src.settings.priority >= victim.settings.priority && (src.settings.priority != victim.settings.priority || src.startedAtTick >= victim.startedAtTick)) continue; victimId = e.getKey(); victim = src; } if (victim != null && victimId != null && victim.settings.priority <= incomingPriority) { SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", victimId, "evict by limiter incomingPriority={} victimPriority={}", incomingPriority, victim.settings.priority); this.stop(victimId); } } private void configureSpatial(int source, BlockPos pos, boolean use3d) { AL10.alSourcef((int)source, (int)4129, (float)0.0f); if (use3d) { AL10.alSourcei((int)source, (int)514, (int)0); AL10.alSource3f((int)source, (int)4100, (float)((float)pos.getX() + 0.5f), (float)((float)pos.getY() + 0.5f), (float)((float)pos.getZ() + 0.5f)); } else { AL10.alSourcei((int)source, (int)514, (int)1); AL10.alSource3f((int)source, (int)4100, (float)0.0f, (float)0.0f, (float)0.0f); } } private int createReverbSource(String sourceId, BlockPos pos, SoundSettings safe, int buffer) { int wet = AL10.alGenSources(); int err = AL10.alGetError(); if (err != 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "reverb source create error={}", err); return 0; } AL10.alSourcei((int)wet, (int)4105, (int)buffer); AL10.alSourcei((int)wet, (int)4103, (int)(safe.loop ? 1 : 0)); AL10.alSourcef((int)wet, (int)4106, (float)Math.max(0.0f, safe.volume * safe.reverbStrength * 0.45f)); AL10.alSourcef((int)wet, (int)4099, (float)Math.max(0.5f, safe.pitch * 0.97f)); this.configureSpatial(wet, pos, safe.use3d); if (safe.use3d) { float ox = (float)((sourceId.hashCode() >> 2 & 7) - 3) * 0.12f; float oy = (float)((sourceId.hashCode() >> 5 & 7) - 3) * 0.06f; float oz = (float)((sourceId.hashCode() >> 8 & 7) - 3) * 0.12f; AL10.alSource3f((int)wet, (int)4100, (float)((float)pos.getX() + 0.5f + ox), (float)((float)pos.getY() + 0.5f + oy), (float)((float)pos.getZ() + 0.5f + oz)); } AL10.alSourcePlay((int)wet); int playErr = AL10.alGetError(); if (playErr != 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", sourceId, "reverb play error={}", playErr); AL10.alDeleteSources((int)wet); return 0; } return wet; } private long currentGameTime() { return Minecraft.getInstance().level != null ? Minecraft.getInstance().level.getGameTime() : Long.MIN_VALUE; } /* * Loose catch block */ private DecodedPcm decodeVorbis(Path file) throws IOException { byte[] data = Files.readAllBytes(file); SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", "DECODE", "read file={} bytes={}", file, data.length); ByteBuffer ogg = MemoryUtil.memAlloc((int)data.length); ogg.put(data); ogg.flip(); try { try (MemoryStack stack = MemoryStack.stackPush();){ IntBuffer error = stack.mallocInt(1); long decoder = STBVorbis.stb_vorbis_open_memory((ByteBuffer)ogg, (IntBuffer)error, null); if (decoder == 0L) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", "DECODE", "stb_vorbis_open_memory failed file={} code={}", file, error.get(0)); throw new IOException("stb_vorbis_open_memory failed: " + error.get(0)); } STBVorbisInfo info = STBVorbisInfo.mallocStack((MemoryStack)stack); STBVorbis.stb_vorbis_get_info((long)decoder, (STBVorbisInfo)info); int channels = info.channels(); int sampleRate = info.sample_rate(); int estimatedFrames = STBVorbis.stb_vorbis_stream_length_in_samples((long)decoder); ShortBuffer pcm = BufferUtils.createShortBuffer((int)Math.max(1, estimatedFrames * channels)); int decodedFrames = STBVorbis.stb_vorbis_get_samples_short_interleaved((long)decoder, (int)channels, (ShortBuffer)pcm); STBVorbis.stb_vorbis_close((long)decoder); if (decodedFrames <= 0) { SoundDebug.warn(SoundDebug.Level.L1, "PLAYBACK", "DECODE", "decode produced no samples file={} decodedFrames={}", file, decodedFrames); throw new IOException("stb_vorbis_get_samples_short_interleaved returned " + decodedFrames); } int decodedShorts = decodedFrames * channels; pcm.position(0); pcm.limit(decodedShorts); SoundDebug.log(SoundDebug.Level.L1, "PLAYBACK", "DECODE", "decode ok channels={} sampleRate={} frames={} pcmShorts={}", channels, sampleRate, decodedFrames, pcm.remaining()); DecodedPcm decodedPcm = new DecodedPcm(pcm, channels, sampleRate, decodedFrames); return decodedPcm; } } finally { MemoryUtil.memFree((Buffer)ogg); } } private DecodedPcm decodeAudio(Path file) throws IOException { String name = file.getFileName().toString().toLowerCase(Locale.ROOT); if (name.endsWith(".wav")) { return this.decodeWav(file); } else if (name.endsWith(".mp3")) { return this.decodeMp3(file); } else { return this.decodeVorbis(file); } } private DecodedPcm decodeWav(Path file) throws IOException { try { AudioInputStream stream = AudioSystem.getAudioInputStream(file.toFile()); AudioFormat baseFormat = stream.getFormat(); int channels = baseFormat.getChannels(); int sampleRate = (int) baseFormat.getSampleRate(); AudioFormat decodedFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, baseFormat.getSampleRate(), 16, baseFormat.getChannels(), baseFormat.getChannels() * 2, baseFormat.getSampleRate(), false ); AudioInputStream pcmStream = AudioSystem.getAudioInputStream(decodedFormat, stream); ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int n; while ((n = pcmStream.read(buf)) != -1) { out.write(buf, 0, n); } pcmStream.close(); stream.close(); byte[] pcmBytes = out.toByteArray(); ShortBuffer shortBuf = BufferUtils.createShortBuffer(pcmBytes.length / 2); ByteBuffer byteBuffer = ByteBuffer.wrap(pcmBytes).order(ByteOrder.LITTLE_ENDIAN); while (byteBuffer.remaining() >= 2) { shortBuf.put(byteBuffer.getShort()); } shortBuf.flip(); int frames = shortBuf.remaining() / channels; SoundDebug.log(SoundDebug.Level.L1, "PLAYBACK", "DECODE", "wav decode ok channels={} sampleRate={} frames={} pcmShorts={}", channels, sampleRate, frames, shortBuf.remaining()); return new DecodedPcm(shortBuf, channels, sampleRate, frames); } catch (UnsupportedAudioFileException e) { throw new IOException("Unsupported WAV file: " + e.getMessage(), e); } } private DecodedPcm decodeMp3(Path file) throws IOException { try (InputStream in = Files.newInputStream(file); BufferedInputStream bin = new BufferedInputStream(in)) { Bitstream bitstream = new Bitstream(bin); Decoder decoder = new Decoder(); Header header = bitstream.readFrame(); if (header == null) { throw new IOException("Empty or invalid MP3 file: " + file); } int channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2; int sampleRate = header.frequency(); ByteArrayOutputStream out = new ByteArrayOutputStream(); while (header != null) { SampleBuffer output = (SampleBuffer) decoder.decodeFrame(header, bitstream); short[] pcm = output.getBuffer(); int len = output.getBufferLength(); for (int i = 0; i < len; i++) { short s = pcm[i]; out.write(s & 0xFF); out.write((s >> 8) & 0xFF); } bitstream.closeFrame(); header = bitstream.readFrame(); } byte[] pcmBytes = out.toByteArray(); ShortBuffer shortBuf = BufferUtils.createShortBuffer(pcmBytes.length / 2); ByteBuffer byteBuffer = ByteBuffer.wrap(pcmBytes).order(ByteOrder.LITTLE_ENDIAN); while (byteBuffer.remaining() >= 2) { shortBuf.put(byteBuffer.getShort()); } shortBuf.flip(); int frames = shortBuf.remaining() / channels; SoundDebug.log(SoundDebug.Level.L1, "PLAYBACK", "DECODE", "mp3 decode ok channels={} sampleRate={} frames={} pcmShorts={}", channels, sampleRate, frames, shortBuf.remaining()); return new DecodedPcm(shortBuf, channels, sampleRate, frames); } catch (JavaLayerException e) { throw new IOException("Failed to decode MP3 file: " + e.getMessage(), e); } } private static class PlayingSource { final String sourceId; final int source; final int buffer; BlockPos pos; SoundSettings settings; final long startedAtTick; float targetGain; int fadeInTicks; int fadeOutTicks; int reverbSource; int elapsedTicks; long lastLiveUpdateTick = Long.MIN_VALUE; PlayingSource(String sourceId, int source, int buffer, BlockPos pos, SoundSettings settings, long startedAtTick) { this.sourceId = sourceId; this.source = source; this.buffer = buffer; this.pos = pos; this.settings = settings; this.startedAtTick = startedAtTick; this.targetGain = settings.volume; } } private static class DecodedPcm { final ShortBuffer samples; final int channels; final int sampleRate; final int frames; DecodedPcm(ShortBuffer samples, int channels, int sampleRate, int frames) { this.samples = samples; this.channels = channels; this.sampleRate = sampleRate; this.frames = frames; } } public static final class PlayingSourceView { public final String sourceId; public final String soundId; public final double x; public final double y; public final double z; public final float radius; public final int priority; public final String cacheStatus; public final int alState; public final boolean liveUpdated; private PlayingSourceView(String sourceId, String soundId, double x, double y, double z, float radius, int priority, String cacheStatus, int alState, boolean liveUpdated) { this.sourceId = sourceId; this.soundId = soundId; this.x = x; this.y = y; this.z = z; this.radius = radius; this.priority = priority; this.cacheStatus = cacheStatus; this.alState = alState; this.liveUpdated = liveUpdated; } } }