Files
SoundPadMod/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java
T

791 lines
36 KiB
Java

/*
* 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.tags.FluidTags;
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.openal.ALC10;
import org.lwjgl.openal.EXTEfx;
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<String, PlayingSource> active = new HashMap<String, PlayingSource>();
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);
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) {
AL10.alSourceStop((int)src.reverbSource);
AL10.alDeleteSources((int)src.reverbSource);
}
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);
}
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();
boolean underwater = player.isEyeInFluid(FluidTags.WATER);
ArrayList<String> remove = new ArrayList<String>();
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;
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)) {
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 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;
}
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<PlayingSourceView> snapshot() {
ArrayList<PlayingSourceView> out = new ArrayList<PlayingSourceView>();
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<String, PlayingSource> 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;
int directFilter;
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;
}
}
}