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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user