Add MP3 sound support via JLayer decoding

- build.gradle: add javazoom:jlayer:1.0.1 dependency (bundled via include())
- Mp3Decoder: new utility class that decodes MP3 to WAV using JLayer
- SplashSound: detect .mp3 files in config dir, decode to .wav for the dynamic resource pack
- Default .ogg still works; place a .mp3 file in config/justasplash/ for MP3 playback
This commit is contained in:
SashegDev
2026-06-08 13:25:21 +00:00
parent aecbde98b0
commit 3c261db111
3 changed files with 120 additions and 8 deletions
+3
View File
@@ -35,6 +35,9 @@ dependencies {
modImplementation("com.github.usefulness:webp-imageio:0.10.2") modImplementation("com.github.usefulness:webp-imageio:0.10.2")
include("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 { loom {
@@ -3,6 +3,7 @@ package net.anomaly.justasplash.splash;
import net.anomaly.justasplash.JustASplash; import net.anomaly.justasplash.JustASplash;
import net.anomaly.justasplash.config.SplashConfig; import net.anomaly.justasplash.config.SplashConfig;
import net.anomaly.justasplash.util.Compat; 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.ResourceManagerHelper;
import net.fabricmc.fabric.api.resource.ResourcePackActivationType; import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
@@ -30,14 +31,17 @@ public class SplashSound {
return; return;
} }
boolean hasCustomOgg; boolean hasCustomSound;
try (Stream<Path> files = Files.list(assetDir)) { try (Stream<Path> 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) { } catch (IOException e) {
hasCustomOgg = false; hasCustomSound = false;
} }
if (hasCustomOgg && !packRegistered) { if (hasCustomSound && !packRegistered) {
ensurePackMeta(assetDir); ensurePackMeta(assetDir);
ensureAssetStructure(assetDir); ensureAssetStructure(assetDir);
@@ -71,12 +75,25 @@ public class SplashSound {
Path soundsDir = assetDir.resolve("assets/justasplash/sounds"); Path soundsDir = assetDir.resolve("assets/justasplash/sounds");
Files.createDirectories(soundsDir); Files.createDirectories(soundsDir);
Path dst = soundsDir.resolve(soundFile); String dstName = soundFile;
if (!Files.exists(dst)) { if (soundFile.toLowerCase().endsWith(".mp3")) {
Files.copy(src, dst); 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"); Path soundsJson = assetDir.resolve("assets/justasplash/sounds.json");
if (!Files.exists(soundsJson)) { if (!Files.exists(soundsJson)) {
String json = "{\"splash\":{\"sounds\":[{\"name\":\"justasplash:" String json = "{\"splash\":{\"sounds\":[{\"name\":\"justasplash:"
@@ -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);
}
}