diff --git a/build.gradle b/build.gradle index bde5e8b..f5e2ec2 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,9 @@ dependencies { modImplementation("com.github.usefulness:webp-imageio:0.10.2") include("com.github.usefulness:webp-imageio:0.10.2") + + modImplementation("javazoom:jlayer:1.0.1") + include("javazoom:jlayer:1.0.1") } loom { diff --git a/src/main/java/net/anomaly/justasplash/splash/SplashSound.java b/src/main/java/net/anomaly/justasplash/splash/SplashSound.java index db1aba8..5a01f30 100644 --- a/src/main/java/net/anomaly/justasplash/splash/SplashSound.java +++ b/src/main/java/net/anomaly/justasplash/splash/SplashSound.java @@ -3,6 +3,7 @@ package net.anomaly.justasplash.splash; import net.anomaly.justasplash.JustASplash; import net.anomaly.justasplash.config.SplashConfig; import net.anomaly.justasplash.util.Compat; +import net.anomaly.justasplash.util.Mp3Decoder; import net.fabricmc.fabric.api.resource.ResourceManagerHelper; import net.fabricmc.fabric.api.resource.ResourcePackActivationType; import net.fabricmc.loader.api.FabricLoader; @@ -30,14 +31,17 @@ public class SplashSound { return; } - boolean hasCustomOgg; + boolean hasCustomSound; try (Stream files = Files.list(assetDir)) { - hasCustomOgg = files.anyMatch(p -> p.toString().endsWith(".ogg")); + hasCustomSound = files.anyMatch(p -> { + String name = p.toString().toLowerCase(); + return name.endsWith(".ogg") || name.endsWith(".mp3"); + }); } catch (IOException e) { - hasCustomOgg = false; + hasCustomSound = false; } - if (hasCustomOgg && !packRegistered) { + if (hasCustomSound && !packRegistered) { ensurePackMeta(assetDir); ensureAssetStructure(assetDir); @@ -71,12 +75,25 @@ public class SplashSound { Path soundsDir = assetDir.resolve("assets/justasplash/sounds"); Files.createDirectories(soundsDir); - Path dst = soundsDir.resolve(soundFile); - if (!Files.exists(dst)) { - Files.copy(src, dst); + String dstName = soundFile; + if (soundFile.toLowerCase().endsWith(".mp3")) { + dstName = soundFile.replaceAll("(?i)\\.mp3$", ".wav"); } - String name = soundFile.replaceAll("\\.\\w+$", ""); + Path dst = soundsDir.resolve(dstName); + if (!Files.exists(dst)) { + if (soundFile.toLowerCase().endsWith(".mp3")) { + try { + Mp3Decoder.decodeToWav(src, dst); + } catch (Exception e) { + return; + } + } else { + Files.copy(src, dst); + } + } + + String name = soundFile.replaceAll("(?i)\\.\\w+$", ""); Path soundsJson = assetDir.resolve("assets/justasplash/sounds.json"); if (!Files.exists(soundsJson)) { String json = "{\"splash\":{\"sounds\":[{\"name\":\"justasplash:" diff --git a/src/main/java/net/anomaly/justasplash/util/Mp3Decoder.java b/src/main/java/net/anomaly/justasplash/util/Mp3Decoder.java new file mode 100644 index 0000000..8b351ab --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/util/Mp3Decoder.java @@ -0,0 +1,92 @@ +package net.anomaly.justasplash.util; + +import javazoom.jl.decoder.Bitstream; +import javazoom.jl.decoder.Decoder; +import javazoom.jl.decoder.Header; +import javazoom.jl.decoder.SampleBuffer; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Mp3Decoder { + public static void decodeToWav(Path mp3Path, Path wavPath) throws Exception { + byte[] pcmData; + int sampleRate = 44100; + int channels = 2; + + try (InputStream input = Files.newInputStream(mp3Path)) { + Bitstream bitstream = new Bitstream(input); + Decoder decoder = new Decoder(); + ByteArrayOutputStream pcmOut = new ByteArrayOutputStream(); + + boolean first = true; + while (true) { + Header header = bitstream.readFrame(); + if (header == null) break; + + SampleBuffer output = (SampleBuffer) decoder.decodeFrame(header, bitstream); + if (first) { + sampleRate = decoder.getOutputFrequency(); + channels = decoder.getOutputChannels(); + first = false; + } + + short[] samples = output.getBuffer(); + int len = output.getBufferLength(); + byte[] bytes = new byte[len * 2]; + for (int i = 0; i < len; i++) { + bytes[i * 2] = (byte) (samples[i] & 0xFF); + bytes[i * 2 + 1] = (byte) ((samples[i] >> 8) & 0xFF); + } + pcmOut.write(bytes); + + bitstream.closeFrame(); + } + + pcmData = pcmOut.toByteArray(); + } + + writeWav(pcmData, sampleRate, channels, 16, wavPath); + } + + private static void writeWav(byte[] pcmData, int sampleRate, int channels, int bitsPerSample, Path output) throws IOException { + int byteRate = sampleRate * channels * bitsPerSample / 8; + int blockAlign = channels * bitsPerSample / 8; + int dataSize = pcmData.length; + + try (DataOutputStream dos = new DataOutputStream(Files.newOutputStream(output))) { + dos.writeBytes("RIFF"); + writeLEInt(dos, 36 + dataSize); + dos.writeBytes("WAVE"); + + dos.writeBytes("fmt "); + writeLEInt(dos, 16); + writeLEShort(dos, (short) 1); + writeLEShort(dos, (short) channels); + writeLEInt(dos, sampleRate); + writeLEInt(dos, byteRate); + writeLEShort(dos, (short) blockAlign); + writeLEShort(dos, (short) bitsPerSample); + + dos.writeBytes("data"); + writeLEInt(dos, dataSize); + dos.write(pcmData); + } + } + + private static void writeLEInt(DataOutputStream dos, int value) throws IOException { + dos.writeByte(value & 0xFF); + dos.writeByte((value >> 8) & 0xFF); + dos.writeByte((value >> 16) & 0xFF); + dos.writeByte((value >> 24) & 0xFF); + } + + private static void writeLEShort(DataOutputStream dos, short value) throws IOException { + dos.writeByte(value & 0xFF); + dos.writeByte((value >> 8) & 0xFF); + } +}