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