710 lines
33 KiB
Java
710 lines
33 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.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<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);
|
|
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<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;
|
|
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<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;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|