Add ATRAC3+ audio decode via FFmpeg

- AudioFramer splits concatenated audio payloads into 696-byte frames
  (8-byte PSP header + 688-byte ATRAC3+ data), spanning PES packets.
- Atrac3pDecoder decodes raw ATRAC3+ via JavaCPP FFmpeg, converting
  FLTP output to interleaved S16 PCM.
- CLI 'adecode' command decodes both audio substreams (each a full
  stereo track at 48kHz) and reports stats.
- Validated against Batman Begins: 69 frames/channel, peak ~4900,
  ~64% nonzero samples.
This commit is contained in:
2026-09-15 22:12:15 -04:00
parent 4821267d0e
commit 8267750207
4 changed files with 411 additions and 1 deletions
+99 -1
View File
@@ -3,7 +3,9 @@ package dev.quoije.umd.cli;
import dev.quoije.umd.iso.UmdIsoFile;
import dev.quoije.umd.iso.UmdIsoReader;
import dev.quoije.umd.media.Atrac3pDecoder;
import dev.quoije.umd.media.VideoDecoder;
import dev.quoije.umd.mps.AudioFramer;
import dev.quoije.umd.mps.MpsDemuxer;
import dev.quoije.umd.playlist.Playlist;
import dev.quoije.umd.sfo.Sfo;
@@ -17,7 +19,7 @@ import java.util.Map;
/** Command-line front-end for the UMD player library. */
public final class Main {
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>\n umdplayer decode <image.iso> <streamName>";
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>\n umdplayer decode <image.iso> <streamName>\n umdplayer audio <image.iso> <streamName> [count]\n umdplayer adecode <image.iso> <streamName> [maxPackets]";
private Main() {
}
@@ -34,6 +36,8 @@ public final class Main {
case "dump" -> dump(requireArg(args, 1));
case "mps" -> mps(requireArg(args, 1), requireArg(args, 2));
case "decode" -> decode(requireArg(args, 1), requireArg(args, 2));
case "audio" -> audio(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Integer.parseInt(args[3]) : 8);
case "adecode" -> adecode(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Integer.parseInt(args[3]) : 500);
case "help", "--help", "-h" -> {
System.out.println(USAGE);
yield 0;
@@ -235,6 +239,100 @@ public final class Main {
return 0;
}
private static int audio(String imagePath, String streamName, int count) throws IOException {
try (UmdIsoReader iso = new UmdIsoReader(Path.of(imagePath));
MpsDemuxer demuxer = MpsDemuxer.open(iso, streamName)) {
int seen = 0;
while (seen < count) {
MpsDemuxer.Packet packet = demuxer.nextPacket();
if (packet == null) {
break;
}
if (!packet.isAudio()) {
continue;
}
seen++;
byte[] payload = packet.payload();
System.out.printf("audio pkt %d: ch=%d pts=%d len=%d%n",
seen, packet.channel(), packet.pts(), payload.length);
System.out.println(" " + hexPrefix(payload, 32));
}
}
return 0;
}
private static int adecode(String imagePath, String streamName, int maxPackets) throws IOException {
try (UmdIsoReader iso = new UmdIsoReader(Path.of(imagePath));
MpsDemuxer demuxer = MpsDemuxer.open(iso, streamName)) {
AudioFramer[] framers = {new AudioFramer(), new AudioFramer()};
Atrac3pDecoder[] decoders = {
new Atrac3pDecoder(48000, 2, 688),
new Atrac3pDecoder(48000, 2, 688),
};
try {
int[] framesFed = new int[2];
int[] framesDecoded = new int[2];
int[] totalSamples = new int[2];
long[] checksum = new long[2];
int[] peak = new int[2];
int[] nonzero = new int[2];
boolean[] first = {true, true};
int packets = 0;
while (packets < maxPackets) {
MpsDemuxer.Packet pkt = demuxer.nextPacket();
if (pkt == null) {
break;
}
packets++;
if (!pkt.isAudio() || pkt.channel() < 0 || pkt.channel() > 1) {
continue;
}
int ch = pkt.channel();
framers[ch].feed(pkt.payload());
byte[] frame;
while ((frame = framers[ch].nextFrame()) != null) {
decoders[ch].feedFrame(frame);
framesFed[ch]++;
}
}
for (int ch = 0; ch < 2; ch++) {
decoders[ch].flush();
byte[] pcm;
while ((pcm = decoders[ch].receive()) != null) {
int interleavedSamples = pcm.length / 2;
totalSamples[ch] += interleavedSamples;
framesDecoded[ch]++;
for (int i = 0; i < pcm.length; i += 2) {
int s = (pcm[i] & 0xFF) | (pcm[i + 1] << 8);
int abs = Math.abs(s);
if (abs > peak[ch]) {
peak[ch] = abs;
}
if (s != 0) {
nonzero[ch]++;
}
if (first[ch]) {
checksum[ch] = checksum[ch] * 31 + s;
}
}
first[ch] = false;
}
System.out.printf("ch%d: %d frames fed, %d frames decoded, %d interleaved samples, first-frame checksum=%d, peak=%d, nonzero=%d%n",
ch, framesFed[ch], framesDecoded[ch], totalSamples[ch], checksum[ch], peak[ch], nonzero[ch]);
}
System.out.printf("Processed %d packets (%d audio frames total)%n",
packets, framesFed[0] + framesFed[1]);
} finally {
decoders[0].close();
decoders[1].close();
}
}
return 0;
}
private static String hexPrefix(byte[] data, int max) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.min(data.length, max); i++) {