Update AudioExtractor.java

main
Nes370 2024-07-25 01:32:02 -07:00
parent 80760bc534
commit 21efae2c2a
1 changed files with 300 additions and 357 deletions

View File

@ -38,63 +38,6 @@ public class AudioExtractor {
*/
private final static boolean CMD_MODE = true;
/**
* ANSI code to go up one line, followed by a carriage return.
* Useful for overwriting user input.
*/
private final static String ANSI_BACKLINE = "\033[F\r";
/**
* ANSI code to switch to beige colored text.
* Useful for highlighting user input.
*/
private final static String ANSI_BEIGE = "\u001B[93m";
/**
* Hex value for the ASCII sequence "RIFF".
* Indicates the start of a RIFF header in WAV files.
*/
private final static int ASCII_RIFF = 0x5249_4646;
/**
* Hex value for the ASCII sequence "RIFF".
* Indicates the start of a RIFF header in WAV files.
*/
private final static int ASCII_WAVE = 0x5741_5645;
/**
* Hex value for the ASCII sequence "fmt ".
* Indicates the start of a format chunk in WAV files.
*/
private final static int ASCII_fmt = 0x666D_7420;
/**
* Hex value for the ASCII sequence "smpl".
* Indicates the start of a sample chunk in WAV files.
*/
private final static int ASCII_smpl = 0x736D_706C;
/**
* Hex value for the ASCII sequence "data".
* Indicates the start of a data chunk in WAV files.
*/
private final static int ASCII_data = 0x6461_7461;
/**
* Hex value for the ASCII sequence "ALIG".
* Indicates the start of an alignment chunk in WAV files.
*/
private final static int ASCII_ALIG = 0x414C_4947;
/**
* Hex value for the ASCII sequence "x2st".
* Indicates the start of an XMA stream chunk in WAV files.
*/
private final static int ASCII_x2st = 0x7832_7374;
/**
* Hex value for the ASCII sequence "seek".
* Indicates the start of an XMA stream chunk in WAV files.
*/
private final static int ASCII_seek = 0x7365_656B;
/**
* Hex value for the ASCII sequence "fLaC".
* Indicates the start of an FLAC file.
*/
private final static int ASCII_fLaC = 0x664C_6143;
/**
* BIN files must first be extracted from game disk to use this program.
* By default uses program directory for I/O and saves audio as WAV.<br>
@ -186,6 +129,9 @@ public class AudioExtractor {
}
case "convert": {
// File audioFile = retrieveAudio(reader);
// if(audioFile.isFile())
// convert(audioFile, encoding);
}
@ -214,87 +160,83 @@ public class AudioExtractor {
}
/*
System.exit(0);
if(args.length > 0) {
System.out.println("Arguments\t:");
for(int i = 0; i < args.length; i++)
System.out.print(Ansi.colorize(args[i] + ' ', Attribute.BRIGHT_GREEN_TEXT()));
}
if(args.length > 0) {
switch(args[0]) {
case "help":
if(args.length > 1)
switch(args[1]) {
case "extract":
break;
case "pack":
break;
case "print":
break;
}
System.out.println("java -jar RR6AudioExtractor.jar [operation] [package] [extract] [option]");
break;
case "extract":
// TODO
break;
case "pack":
// TODO
break;
}
}
// Confirm input/output mode
boolean packMode = false;
if(args.length > 0)
packMode = args[0].equalsIgnoreCase("pack");
// Confirm package directory, else abort
File packDirectory;
if(args.length > 1)
packDirectory = Path.of(args[1]).toFile();
else packDirectory = new File(new File(AudioExtractor.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent());
if(packMode && !packDirectory.exists() && !packDirectory.mkdir()) {
System.out.println("Package output directory " + packDirectory + " does not exist, and could not be created. "
+ "Program execution aborted.");
System.exit(1);
} else if(!packMode && !packDirectory.exists()) {
System.out.println("Package input directory " + packDirectory + " does not exist. Program execution aborted.");
System.exit(1);
}
// Confirm extract directory, else abort
File extractDirectory;
if(args.length > 2)
extractDirectory = Path.of(args[2]).toFile();
else extractDirectory = packDirectory;
if(!packMode && !extractDirectory.exists() && !extractDirectory.mkdir()) {
System.out.println("Extract output directory " + extractDirectory + " does not exist, and could not be created. "
+ "Program execution aborted.");
System.exit(2);
}
// Confirm BGM output format
boolean compressBGM = false;
if(args.length > 3)
compressBGM = args[3].equalsIgnoreCase("FLAC");
// Begin operation
if(packMode)
pack(packDirectory, extractDirectory);
else extract(packDirectory, extractDirectory, compressBGM);
*/
}
/**
* Examines a given file and reads some of its properties.
*
* @param givenFile
*/
public static void identify(File givenFile) {
// TODO check for characteristics of BIN or WAV files
String name = givenFile.getName();
System.out.println("Name:\t" + name);
long size = givenFile.length();
System.out.println("Size:\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "") + "\n");
// file extension
int extIndex = name.lastIndexOf('.');
String extension = null;
if(extIndex > 0 && extIndex < name.length() - 1)
extension = name.substring(extIndex + 1);
try {
RandomAccessFile file = new RandomAccessFile(givenFile, "r");
switch(extension.toLowerCase()) {
case "bin": {
LinkedHashMap<Integer, Integer> tracklist = findAudioTracks(file);
System.out.println(tracklist.size() + " Tracks (" + convertBytes(size / tracklist.size()) + " avg size)\n");
System.out.println("Format Info");
readFormatChunk(file, 0x0C);
break;
}
case "wav":
case "xma":
default: {
for(long i = 0; i < size - 4; i += 4) {
file.seek(i);
int value = file.readInt();
switch(value) {
case ASCII_RIFF: {
readRIFFChunk(file, (int) i);
break;
}
case ASCII_fmt: {
i = readFormatChunk(file, (int) i) - 4;
break;
}
case ASCII_smpl: {
i = readSampleChunk(file, (int) i) - 4;
break;
}
case ASCII_data: {
i = readDataChunk(file, (int) i) - 4;
break;
}
case ASCII_ALIG: {
i = readAlignmentChunk(file, (int) i) - 4;
break;
}
case ASCII_seek: {
i = readx2stChunk(file, (int) i) - 4;
break;
}
case ASCII_x2st: {
i = readx2stChunk(file, (int) i) - 4;
break;
}
}
}
break;
}
}
} catch (IOException e) {
System.out.println("Could not read file.");
}
}
/**
* Extracts audio tracks from the given package file.
@ -364,18 +306,6 @@ public class AudioExtractor {
}
public static void printProgressBar(int current, int total) {
int done = 50 * current / total;
int todo = 50 - done;
String doneStr = ""; // String.format("%" + done + "s", '█');
for(int i = 0; i < done; i++)
doneStr += '█';
String todoStr = ""; // String.format("%" + todo + "s", '_');
for(int i = 0; i < todo; i++)
doneStr += '_';
System.out.println(ANSI_BACKLINE + "Progress: |" + doneStr + todoStr + "| (" + current + "/" + total + ")");
}
/**
* Extracts audio files to the extract directory from package files located at the package directory.
* Also supports FLAC conversion for BGM files.
@ -383,6 +313,7 @@ public class AudioExtractor {
* @param packDirectory
* @param extractDirectory
* @param compressBGM
* @deprecated delete after adapting FLAC conversion
*/
public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) {
@ -515,6 +446,168 @@ public class AudioExtractor {
}
/**
* Packs WAV files located at the extract directory into BIN files at the package directory.
*
* @param packDirectory
* @param extractDirectory
*/
private static void pack(File packDirectory, File extractDirectory) {
// Identify extracted folders
File[] packages = new File(extractDirectory.toString()).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.toLowerCase().startsWith("pack_") && !name.toLowerCase().endsWith(".bin");
}
});
for(int i = 0; i < packages.length; i++)
System.out.println(packages[i].getName());
for(File packDir : packages) {
// 1. Compile list of WAV files in extracted directories
File[] audioTracks = new File(packDir.toString()).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(".wav");
}
});
// 2. Check if pack files already exist in package directory
File pack = new File(packDir.getPath() + ".bin");
if(pack.exists());
// 3. If pack file already exists, insert WAV files into existing file.
// 4. If pack file doesn't exist, create new file then insert WAV files.
}
// Hypothesis 1: Does Ridge Racer 6 use a predetermined address list for reading tracks from package?
// Test 1: Try changing the address of a track and see if the game can handle it successfully.
// Hypothesis 2: Does Ridge Racer 6 loop tracks in their entirety, or do they use loop points defined in the file header?
// Test 1: Try editing loop information and see if it affects in-game playback.
// Test 2: Try replacing a track with a WAV file without loop data and see if it loops in in-game playback.
}
/**
* Patches a WAV file by seeking out its data and smpl chunks,
* replacing any existing smpl chunk with a new smpl chunk,
* setting loop points at the start and end of the data chunk.
*
* @param waveFile
*/
private static void patch(File waveFile) {
try {
RandomAccessFile file = new RandomAccessFile(waveFile, "r");
// Also Find data location
int dataAddress = -1;
// Check if sample chunk already exists
int smplAddress = -1;
for(int i = 0; i < file.length(); i += 4) {
file.seek(i);
int scan = file.readInt();
// match "data" chunk header
if(scan == ASCII_data)
dataAddress = i;
// match "smpl" chunk header
else if(scan == ASCII_smpl)
smplAddress = i;
// if both chunks already located, stop seeking
if(smplAddress > -1 && dataAddress > -1)
break;
}
// Determine size of data chunk
int dataSize;
if(dataAddress > -1)
dataSize = readChunkSize(file, dataAddress);
else throw new IOException("Data chunk not in file.");
// Determine size of smpl chunk
int smplSize;
if(smplAddress > -1)
smplSize = readChunkSize(file, smplAddress);
// smpl chunk may not already exist
// Create new sample chunk
// Insert 68-byte sample chunk with loop before data chunk
byte[] smpl = new byte[68];
// smpl (0)
smpl[0] = 0x73;
smpl[1] = 0x6D;
smpl[2] = 0x70;
smpl[3] = 0x6C;
// size (4)
smpl[4] = 0x3C;
// manufacturer (8)
// product (12)
// sample period (16)
// MIDI unity note (20)
smpl[20] = 0x3C;
// MIDI pitch fraction (24)
// SMPTE Format (28)
// SMPTE Offset (32)
// Number of Sample Loops (36)
smpl[36] = 0x01;
// Sample Loops size (40)
// loop ID (44)
// loop type (48)
// data start
// loop start (52)
// data end
// loop end (56)
// tuning fraction (60)
// play count (64)
// Set loop points to start and end of data chunk
// Update the new file size in RIFF header
} catch(IOException e) {
System.out.println("Failed to patch file.");
}
}
/**
* Helper function to show current progress.
* Should only be used in CMD Mode.
*
* @param current
* @param total
*/
public static void printProgressBar(int current, int total) {
int width = 50; // progress bar total width
double progress = (double) current / total; // 0 = none, 1 = complete
int progressWidth = (int) Math.floor(progress * width);
double progressRemainder = (progress * width) % 1.0;
int x = (int) Math.floor(progressRemainder * 8);
char[] c = { ' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉' };
String bar = "";
for(int i = 0; i < width; i++) {
if(i < progressWidth)
bar += '█';
else if(i == progressWidth)
bar += c[x];
else bar += ' ';
}
System.out.println(ANSI_BACKLINE + "Progress: |" + bar + "| (" + current + "/" + total + ")");
}
/**
* Helper method to prompt user to select a package file, or folder containing one or more packages.
*
@ -637,84 +730,6 @@ public class AudioExtractor {
}
/**
* Examines a given file and reads some of its properties.
*
* @param givenFile
*/
public static void identify(File givenFile) {
// TODO check for characteristics of BIN or WAV files
String name = givenFile.getName();
System.out.println("Name:\t" + name);
long size = givenFile.length();
System.out.println("Size:\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "") + "\n");
// file extension
int extIndex = name.lastIndexOf('.');
String extension = null;
if(extIndex > 0 && extIndex < name.length() - 1)
extension = name.substring(extIndex + 1);
try {
RandomAccessFile file = new RandomAccessFile(givenFile, "r");
switch(extension.toLowerCase()) {
case "bin": {
LinkedHashMap<Integer, Integer> tracklist = findAudioTracks(file);
System.out.println(tracklist.size() + " Tracks (" + convertBytes(size / tracklist.size()) + " avg size)\n");
System.out.println("Format Info");
readFormatChunk(file, 0x0C);
break;
}
case "wav":
case "xma":
default: {
for(long i = 0; i < size - 4; i += 4) {
file.seek(i);
int value = file.readInt();
switch(value) {
case ASCII_RIFF: {
readRIFFChunk(file, (int) i);
break;
}
case ASCII_fmt: {
i = readFormatChunk(file, (int) i) - 4;
break;
}
case ASCII_smpl: {
i = readSampleChunk(file, (int) i) - 4;
break;
}
case ASCII_data: {
i = readDataChunk(file, (int) i) - 4;
break;
}
case ASCII_ALIG: {
i = readAlignmentChunk(file, (int) i) - 4;
break;
}
case ASCII_seek: {
i = readx2stChunk(file, (int) i) - 4;
break;
}
case ASCII_x2st: {
i = readx2stChunk(file, (int) i) - 4;
break;
}
}
}
break;
}
}
} catch (IOException e) {
System.out.println("Could not read file.");
}
// check if RIFF header
}
/**
* Helper function to read RIFF header information.
*
@ -1046,138 +1061,7 @@ public class AudioExtractor {
}
/**
* Packs WAV files located at the extract directory into BIN files at the package directory.
*
* @param packDirectory
* @param extractDirectory
*/
private static void pack(File packDirectory, File extractDirectory) {
// Identify extracted folders
File[] packages = new File(extractDirectory.toString()).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.toLowerCase().startsWith("pack_") && !name.toLowerCase().endsWith(".bin");
}
});
for(int i = 0; i < packages.length; i++)
System.out.println(packages[i].getName());
for(File packDir : packages) {
// 1. Compile list of WAV files in extracted directories
File[] audioTracks = new File(packDir.toString()).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(".wav");
}
});
// 2. Check if pack files already exist in package directory
File pack = new File(packDir.getPath() + ".bin");
if(pack.exists());
// 3. If pack file already exists, insert WAV files into existing file.
// 4. If pack file doesn't exist, create new file then insert WAV files.
}
// Hypothesis 1: Does Ridge Racer 6 use a predetermined address list for reading tracks from package?
// Test 1: Try changing the address of a track and see if the game can handle it successfully.
// Hypothesis 2: Does Ridge Racer 6 loop tracks in their entirety, or do they use loop points defined in the file header?
// Test 1: Try editing loop information and see if it affects in-game playback.
// Test 2: Try replacing a track with a WAV file without loop data and see if it loops in in-game playback.
}
/**
* Patches a WAV file by seeking out its data and smpl chunks,
* replacing any existing smpl chunk with a new smpl chunk,
* setting loop points at the start and end of the data chunk.
*
* @param waveFile
*/
private static void patch(File waveFile) {
try {
RandomAccessFile file = new RandomAccessFile(waveFile, "r");
// Also Find data location
int dataAddress = -1;
// Check if sample chunk already exists
int smplAddress = -1;
for(int i = 0; i < file.length(); i += 4) {
file.seek(i);
int scan = file.readInt();
// match "data" chunk header
if(scan == ASCII_data)
dataAddress = i;
// match "smpl" chunk header
else if(scan == ASCII_smpl)
smplAddress = i;
// if both chunks already located, stop seeking
if(smplAddress > -1 && dataAddress > -1)
break;
}
// Determine size of data chunk
int dataSize;
if(dataAddress > -1)
dataSize = readChunkSize(file, dataAddress);
else throw new IOException("Data chunk not in file.");
// Determine size of smpl chunk
int smplSize;
if(smplAddress > -1)
smplSize = readChunkSize(file, smplAddress);
// smpl chunk may not already exist
// Create new sample chunk
// Insert 68-byte sample chunk with loop before data chunk
byte[] smpl = new byte[68];
// smpl (0)
smpl[0] = 0x73;
smpl[1] = 0x6D;
smpl[2] = 0x70;
smpl[3] = 0x6C;
// size (4)
smpl[4] = 0x3C;
// manufacturer (8)
// product (12)
// sample period (16)
// MIDI unity note (20)
smpl[20] = 0x3C;
// MIDI pitch fraction (24)
// SMPTE Format (28)
// SMPTE Offset (32)
// Number of Sample Loops (36)
smpl[36] = 0x01;
// Sample Loops size (40)
// loop ID (44)
// loop type (48)
// data start
// loop start (52)
// data end
// loop end (56)
// tuning fraction (60)
// play count (64)
// Set loop points to start and end of data chunk
// Update the new file size in RIFF header
} catch(IOException e) {
System.out.println("Failed to patch file.");
}
}
/**
* Helper function to prompt user to select a WAV file.
@ -2443,4 +2327,63 @@ public class AudioExtractor {
return value;
}
/**
* ANSI code to go up one line, followed by a carriage return.
* Useful for overwriting user input.
*/
private final static String ANSI_BACKLINE = "\033[F\r";
/**
* ANSI code to switch to beige colored text.
* Useful for highlighting user input.
*/
private final static String ANSI_BEIGE = "\u001B[93m";
/**
* Hex value for the ASCII sequence "RIFF".
* Indicates the start of a RIFF header in WAV files.
*/
private final static int ASCII_RIFF = 0x5249_4646;
/**
* Hex value for the ASCII sequence "RIFF".
* Indicates the start of a RIFF header in WAV files.
*/
private final static int ASCII_WAVE = 0x5741_5645;
/**
* Hex value for the ASCII sequence "fmt ".
* Indicates the start of a format chunk in WAV files.
*/
private final static int ASCII_fmt = 0x666D_7420;
/**
* Hex value for the ASCII sequence "smpl".
* Indicates the start of a sample chunk in WAV files.
*/
private final static int ASCII_smpl = 0x736D_706C;
/**
* Hex value for the ASCII sequence "data".
* Indicates the start of a data chunk in WAV files.
*/
private final static int ASCII_data = 0x6461_7461;
/**
* Hex value for the ASCII sequence "ALIG".
* Indicates the start of an alignment chunk in WAV files.
*/
private final static int ASCII_ALIG = 0x414C_4947;
/**
* Hex value for the ASCII sequence "x2st".
* Indicates the start of an XMA stream chunk in WAV files.
*/
private final static int ASCII_x2st = 0x7832_7374;
/**
* Hex value for the ASCII sequence "seek".
* Indicates the start of an XMA stream chunk in WAV files.
*/
private final static int ASCII_seek = 0x7365_656B;
/**
* Hex value for the ASCII sequence "fLaC".
* Indicates the start of an FLAC file.
*/
private final static int ASCII_fLaC = 0x664C_6143;
}