|
|
|
@ -29,19 +29,38 @@ import com.diogonunes.jcolor.Attribute;
|
|
|
|
|
*/
|
|
|
|
|
public class AudioExtractor {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ANSI code to go up one line, followed by a carriage return.
|
|
|
|
|
* Useful for overwriting user input
|
|
|
|
|
*/
|
|
|
|
|
private final static String BACKLINE = "\033[F\r";
|
|
|
|
|
private final static String PALE_YELLOW = "\u001B[93m";
|
|
|
|
|
|
|
|
|
|
private final static int RIFF = 0x52494646;
|
|
|
|
|
/**
|
|
|
|
|
* Activate alternate behavior for command line terminal.
|
|
|
|
|
* Specifically prints things in a narrower width, and overwrites previous lines to add color.
|
|
|
|
|
*/
|
|
|
|
|
private final static boolean CMD = false;
|
|
|
|
|
private final static boolean CMD_MODE = false;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 "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;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* BIN files must first be extracted from game disk to use this program.
|
|
|
|
@ -60,18 +79,24 @@ public class AudioExtractor {
|
|
|
|
|
*/
|
|
|
|
|
public static void main(String[] args) throws URISyntaxException, InterruptedException {
|
|
|
|
|
|
|
|
|
|
if(CMD) {
|
|
|
|
|
// Print Audio Extractor logo
|
|
|
|
|
if(CMD_MODE) {
|
|
|
|
|
|
|
|
|
|
// NARROW LOGO
|
|
|
|
|
String adjustment = "█████████ ████████████████████████████████████████████████████ ████████████████████████";
|
|
|
|
|
System.out.println(Ansi.colorize(logoFormat("Ridge", true, 8)/*.substring(0, 484) + adjustment*/, Attribute.BRIGHT_RED_TEXT()));
|
|
|
|
|
Thread.sleep(500);
|
|
|
|
|
System.out.println(BACKLINE + BACKLINE + Ansi.colorize(adjustment, Attribute.BRIGHT_RED_TEXT()));
|
|
|
|
|
|
|
|
|
|
String adjustment = "█████████ ████████████████████████████████████████████████████ ████████████████████████";
|
|
|
|
|
System.out.println(ANSI_BACKLINE + ANSI_BACKLINE + Ansi.colorize(adjustment, Attribute.BRIGHT_RED_TEXT()));
|
|
|
|
|
System.out.println(Ansi.colorize(logoFormat("Racer 6", true, 9), Attribute.BRIGHT_RED_TEXT()));
|
|
|
|
|
Thread.sleep(500);
|
|
|
|
|
|
|
|
|
|
System.out.println(Ansi.colorize(logoFormat("Audio", true, 8), Attribute.YELLOW_TEXT()));
|
|
|
|
|
Thread.sleep(500);
|
|
|
|
|
|
|
|
|
|
System.out.println(Ansi.colorize(logoFormat("Xtractr", true, 8), Attribute.YELLOW_TEXT()));
|
|
|
|
|
Thread.sleep(500);
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// WIDE LOGO
|
|
|
|
|
System.out.println(Ansi.colorize(logoFormat("Ridge Racer 6"), Attribute.BRIGHT_RED_TEXT()));
|
|
|
|
@ -80,11 +105,12 @@ public class AudioExtractor {
|
|
|
|
|
Thread.sleep(500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Print meta info
|
|
|
|
|
System.out.println("Program:\tRidge Racer 6 Audio Extractor " + Ansi.colorize("v1.0", Attribute.BRIGHT_GREEN_TEXT()) + " by "
|
|
|
|
|
+ Ansi.colorize("Nes", Attribute.TEXT_COLOR(252, 42, 124)));
|
|
|
|
|
System.out.println("Repository:\t" + Ansi.colorize("https://gitea.goblincave.synology.me/Nes/RR6AudioExtractor", Attribute.CYAN_TEXT()));
|
|
|
|
|
|
|
|
|
|
// Used for reading user input
|
|
|
|
|
// Initialize user input reader
|
|
|
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
|
|
|
|
|
|
|
|
|
// determine which operation to execute
|
|
|
|
@ -116,7 +142,7 @@ public class AudioExtractor {
|
|
|
|
|
break;
|
|
|
|
|
case "print": // print
|
|
|
|
|
Attribute color = retrieveColor(reader);
|
|
|
|
|
String message = retrieveInput(reader, "Please enter your logo message: " + PALE_YELLOW);
|
|
|
|
|
String message = retrieveInput(reader, "Please enter your logo message: " + ANSI_BEIGE);
|
|
|
|
|
System.out.println(Ansi.colorize(logoFormat(message), color));
|
|
|
|
|
break;
|
|
|
|
|
case "exit": // exit
|
|
|
|
@ -204,358 +230,15 @@ public class AudioExtractor {
|
|
|
|
|
else extract(packDirectory, extractDirectory, compressBGM);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static File retrieveWave(BufferedReader reader) {
|
|
|
|
|
|
|
|
|
|
String prompt = "Please provide a WAV file: " + PALE_YELLOW;
|
|
|
|
|
|
|
|
|
|
File waveFile = null;
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
|
|
|
|
|
// Retrieve an audio file
|
|
|
|
|
waveFile = retrieveFile(reader, prompt, ".wav");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// validate that chosen file is a proper Wave file
|
|
|
|
|
RandomAccessFile file = new RandomAccessFile(waveFile, "r");
|
|
|
|
|
// first 4 bytes start with RIFF
|
|
|
|
|
if(file.readInt() == RIFF) {
|
|
|
|
|
// next 4 bytes contain the size of the file in little-endian representation
|
|
|
|
|
file.seek(4);
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
file.read(buffer); // write size bytes to buffer
|
|
|
|
|
|
|
|
|
|
// calculate expected file size
|
|
|
|
|
int headerSize = 8 + buffer[3]
|
|
|
|
|
+ buffer[2] * (int) Math.pow(16, 2)
|
|
|
|
|
+ buffer[1] * (int) Math.pow(16, 4)
|
|
|
|
|
+ buffer[0] * (int) Math.pow(16, 6);
|
|
|
|
|
|
|
|
|
|
// file is valid if actual file length matches expected size
|
|
|
|
|
valid = headerSize == file.length();
|
|
|
|
|
if(!valid)
|
|
|
|
|
System.out.println("File length mismatch. Expected " + headerSize + " bytes, but found " + file.length() + " bytes.");
|
|
|
|
|
|
|
|
|
|
} else System.out.println("Invalid RIFF header.");
|
|
|
|
|
file.close();
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println("Invalid file.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return waveFile;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static File retrieveFile(BufferedReader reader, String prompt, String extension) {
|
|
|
|
|
|
|
|
|
|
File file = null;
|
|
|
|
|
|
|
|
|
|
String input = null;
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
System.out.print(prompt);
|
|
|
|
|
try {
|
|
|
|
|
input = reader.readLine();
|
|
|
|
|
file = new File(input);
|
|
|
|
|
if(file.exists()) {
|
|
|
|
|
if(file.getName().toLowerCase().endsWith(extension)) {
|
|
|
|
|
valid = true;
|
|
|
|
|
} else System.out.println("File does not have " + extension.toUpperCase() + " file extension");
|
|
|
|
|
} else System.out.println("File does not exist.");
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid file."));
|
|
|
|
|
}
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return file;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Attribute retrieveColor(BufferedReader reader) {
|
|
|
|
|
|
|
|
|
|
String prompt = "\nCommand parameters: " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT())
|
|
|
|
|
+ Ansi.colorize(" [color]", Attribute.BRIGHT_YELLOW_TEXT()) + Ansi.colorize(" [message]", Attribute.BRIGHT_RED_TEXT())
|
|
|
|
|
+ "\n"
|
|
|
|
|
+ "\n" + Ansi.colorize("Colors", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":"
|
|
|
|
|
+ "\n- " + Ansi.colorize("Default", Attribute.CLEAR())
|
|
|
|
|
+ "\n- " + Ansi.colorize("Black", Attribute.BLACK_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Gray", Attribute.BRIGHT_BLACK_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("White", Attribute.BRIGHT_WHITE_TEXT())
|
|
|
|
|
+ "\n- " + Ansi.colorize("Indigo", Attribute.TEXT_COLOR(0x7D, 0x1A, 0xFF))
|
|
|
|
|
+ ", " + Ansi.colorize("Purple", Attribute.BRIGHT_MAGENTA_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Violet", Attribute.TEXT_COLOR(0xEE, 0x82, 0xEE))
|
|
|
|
|
+ "\n- " + Ansi.colorize("Red", Attribute.RED_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Cinnabar", Attribute.BRIGHT_RED_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Pink", Attribute.TEXT_COLOR(0xFF, 0xC0, 0xCB))
|
|
|
|
|
+ "\n- " + Ansi.colorize("Orange", Attribute.TEXT_COLOR(0xFF, 0x68, 0x1F))
|
|
|
|
|
+ ", " + Ansi.colorize("Gold", Attribute.YELLOW_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Yellow", Attribute.TEXT_COLOR(0xFF, 0xEA, 0x00))
|
|
|
|
|
+ "\n- " + Ansi.colorize("Green", Attribute.BRIGHT_GREEN_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Chartreuse", Attribute.TEXT_COLOR(0xD1, 0xEB, 0x27))
|
|
|
|
|
+ ", " + Ansi.colorize("Beige", Attribute.BRIGHT_YELLOW_TEXT())
|
|
|
|
|
+ "\n- " + Ansi.colorize("Blue", Attribute.BRIGHT_BLUE_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Teal", Attribute.TEXT_COLOR(0x00, 0x93, 0xCF))
|
|
|
|
|
+ ", " + Ansi.colorize("Cyan", Attribute.BRIGHT_CYAN_TEXT())
|
|
|
|
|
+ "\n"
|
|
|
|
|
+ "\n" + Ansi.colorize("Please select a color:") + PALE_YELLOW + " ";
|
|
|
|
|
String[] values = {"Default", "Black", "Gray", "White", "Indigo", "Purple", "Violet", "Red", "Cinnabar", "Pink", "Orange", "Gold",
|
|
|
|
|
"Yellow", "Green", "Chartreuse", "Beige","Blue", "Teal", "Cyan"};
|
|
|
|
|
|
|
|
|
|
String input = retrieveInput(reader, prompt, values);
|
|
|
|
|
|
|
|
|
|
Attribute color = null;
|
|
|
|
|
switch(input.toLowerCase()) {
|
|
|
|
|
case "default": color = Attribute.CLEAR(); break;
|
|
|
|
|
case "black": color = Attribute.BLACK_TEXT(); break;
|
|
|
|
|
case "gray": color = Attribute.BRIGHT_BLACK_TEXT(); break;
|
|
|
|
|
case "white": color = Attribute.BRIGHT_WHITE_TEXT(); break;
|
|
|
|
|
case "indigo": color = Attribute.TEXT_COLOR(0x7D, 0x1A, 0xFF); break;
|
|
|
|
|
case "purple": color = Attribute.BRIGHT_MAGENTA_TEXT(); break;
|
|
|
|
|
case "violet": color = Attribute.TEXT_COLOR(0xEE, 0x82, 0xEE); break;
|
|
|
|
|
case "red": color = Attribute.RED_TEXT(); break;
|
|
|
|
|
case "cinnabar": color = Attribute.BRIGHT_RED_TEXT(); break;
|
|
|
|
|
case "pink": color = Attribute.TEXT_COLOR(0xFF, 0xC0, 0xCB); break;
|
|
|
|
|
case "orange": color = Attribute.TEXT_COLOR(0xFF, 0x68, 0x1F); break;
|
|
|
|
|
case "gold": color = Attribute.YELLOW_TEXT(); break;
|
|
|
|
|
case "yellow": color = Attribute.TEXT_COLOR(0xFF, 0xEA, 0x00); break;
|
|
|
|
|
case "green": color = Attribute.BRIGHT_GREEN_TEXT(); break;
|
|
|
|
|
case "chartreuse": color = Attribute.TEXT_COLOR(0xD1, 0xEB, 0x27); break;
|
|
|
|
|
case "beige": color = Attribute.BRIGHT_YELLOW_TEXT(); break;
|
|
|
|
|
case "blue": color = Attribute.BRIGHT_BLUE_TEXT(); break;
|
|
|
|
|
case "teal": color = Attribute.TEXT_COLOR(0x00, 0x93, 0xCF); break;
|
|
|
|
|
case "cyan": color = Attribute.BRIGHT_CYAN_TEXT(); break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(CMD)
|
|
|
|
|
System.out.println(BACKLINE + Ansi.colorize("Please select a color: ") + Ansi.colorize(input, color) + "\n");
|
|
|
|
|
else System.out.println(Ansi.colorize(""));
|
|
|
|
|
|
|
|
|
|
return color;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String retrieveOperation(BufferedReader reader, String input) {
|
|
|
|
|
|
|
|
|
|
String operation = null;
|
|
|
|
|
String[] values = {"extract", "package", "patch", "print", "exit"};
|
|
|
|
|
|
|
|
|
|
if(input != null)
|
|
|
|
|
for(int i = 0; i < values.length; i++)
|
|
|
|
|
if(values[i].equalsIgnoreCase(input))
|
|
|
|
|
operation = values[i];
|
|
|
|
|
|
|
|
|
|
if(operation == null) {
|
|
|
|
|
|
|
|
|
|
String prompt = "\n" + Ansi.colorize("Operations", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Extract", Attribute.BRIGHT_RED_TEXT()) + " audio tracks from package\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Package", Attribute.YELLOW_TEXT()) + " audio tracks into package\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Patch", Attribute.BRIGHT_GREEN_TEXT()) + " audio track to loop indefinitely\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT()) + " Ridge Racer 6 style ASCII logo\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Exit", Attribute.BRIGHT_MAGENTA_TEXT()) + " program\n"
|
|
|
|
|
+ "\nPlease select an operation: " + PALE_YELLOW;
|
|
|
|
|
|
|
|
|
|
operation = retrieveInput(reader, prompt, values);
|
|
|
|
|
|
|
|
|
|
if(CMD) {
|
|
|
|
|
|
|
|
|
|
Attribute color = null;
|
|
|
|
|
switch(operation.toLowerCase()) {
|
|
|
|
|
case "extract":
|
|
|
|
|
color = Attribute.BRIGHT_RED_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "package":
|
|
|
|
|
color = Attribute.YELLOW_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "patch":
|
|
|
|
|
color = Attribute.BRIGHT_GREEN_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "print":
|
|
|
|
|
color = Attribute.BRIGHT_BLUE_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "exit":
|
|
|
|
|
color = Attribute.BRIGHT_MAGENTA_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
System.out.println(BACKLINE + Ansi.colorize("Please select an operation: ") + Ansi.colorize(operation, color));
|
|
|
|
|
|
|
|
|
|
} else System.out.print(Ansi.colorize(""));
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return operation.toLowerCase();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prompts the user for input without strict validation.
|
|
|
|
|
* @param reader
|
|
|
|
|
* @param prompt
|
|
|
|
|
* @return input
|
|
|
|
|
*/
|
|
|
|
|
private static String retrieveInput(BufferedReader reader, String prompt) {
|
|
|
|
|
|
|
|
|
|
String input = null;
|
|
|
|
|
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
System.out.print(prompt);
|
|
|
|
|
try {
|
|
|
|
|
input = reader.readLine();
|
|
|
|
|
valid = true;
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid input."));
|
|
|
|
|
}
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return input;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prompts the user for input and validates that the input matches one of the provided values.
|
|
|
|
|
* Extracts audio files to the extract directory from package files located at the package directory.
|
|
|
|
|
* Also supports FLAC conversion for BGM files.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @param prompt
|
|
|
|
|
* @param values
|
|
|
|
|
* @return input
|
|
|
|
|
* @param packDirectory
|
|
|
|
|
* @param extractDirectory
|
|
|
|
|
* @param compressBGM
|
|
|
|
|
*/
|
|
|
|
|
private static String retrieveInput(BufferedReader reader, String prompt, String[] values) {
|
|
|
|
|
|
|
|
|
|
String input = null;
|
|
|
|
|
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
System.out.print(prompt);
|
|
|
|
|
try {
|
|
|
|
|
input = reader.readLine();
|
|
|
|
|
for(int i = 0; i < values.length; i++)
|
|
|
|
|
if(input.equalsIgnoreCase(values[i])) {
|
|
|
|
|
valid = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if(!valid)
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid selection."));
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid selection."));
|
|
|
|
|
}
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return input;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Patches the provided WAV file by seeking out its data and smpl chunks,
|
|
|
|
|
* deletes any existing smpl chunk,
|
|
|
|
|
* then inserts a new smpl chunk with loop points set 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 "smpl" chunk header
|
|
|
|
|
if(scan == 0x736D706C)
|
|
|
|
|
smplAddress = i;
|
|
|
|
|
// match "data" chunk header
|
|
|
|
|
if(scan == 0x64617461)
|
|
|
|
|
dataAddress = i;
|
|
|
|
|
if(smplAddress > -1 && dataAddress > -1)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine size of data chunk
|
|
|
|
|
if(dataAddress > -1) {
|
|
|
|
|
|
|
|
|
|
file.seek(dataAddress + 4);
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
file.read(buffer);
|
|
|
|
|
|
|
|
|
|
int dataSize = 8 + buffer[3]
|
|
|
|
|
+ buffer[2] * (int) Math.pow(16, 2)
|
|
|
|
|
+ buffer[1] * (int) Math.pow(16, 4)
|
|
|
|
|
+ buffer[0] * (int) Math.pow(16, 6);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine size of smpl chunk
|
|
|
|
|
if(smplAddress > -1) {
|
|
|
|
|
|
|
|
|
|
file.seek(smplAddress + 4);
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
file.read(buffer); // write size bytes to buffer
|
|
|
|
|
|
|
|
|
|
// calculate expected chunk size
|
|
|
|
|
int smplSize = 8 + buffer[3]
|
|
|
|
|
+ buffer[2] * (int) Math.pow(16, 2)
|
|
|
|
|
+ buffer[1] * (int) Math.pow(16, 4)
|
|
|
|
|
+ buffer[0] * (int) Math.pow(16, 6);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create new sample chunk
|
|
|
|
|
// Insert 68-byte sample chunk with loop before data chunk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) {
|
|
|
|
|
|
|
|
|
|
// TODO Delete
|
|
|
|
@ -686,6 +369,463 @@ 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
|
|
|
|
|
if(dataAddress > -1) {
|
|
|
|
|
|
|
|
|
|
file.seek(dataAddress + 4);
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
file.read(buffer);
|
|
|
|
|
|
|
|
|
|
int dataSize = 8 + buffer[3]
|
|
|
|
|
+ buffer[2] * (int) Math.pow(16, 2)
|
|
|
|
|
+ buffer[1] * (int) Math.pow(16, 4)
|
|
|
|
|
+ buffer[0] * (int) Math.pow(16, 6);
|
|
|
|
|
|
|
|
|
|
} else throw new IOException("Data chunk not in file.");
|
|
|
|
|
|
|
|
|
|
// Determine size of smpl chunk
|
|
|
|
|
if(smplAddress > -1) {
|
|
|
|
|
|
|
|
|
|
file.seek(smplAddress + 4);
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
file.read(buffer); // write size bytes to buffer
|
|
|
|
|
|
|
|
|
|
// calculate expected chunk size
|
|
|
|
|
int smplSize = 8 + buffer[3]
|
|
|
|
|
+ buffer[2] * (int) Math.pow(16, 2)
|
|
|
|
|
+ buffer[1] * (int) Math.pow(16, 4)
|
|
|
|
|
+ buffer[0] * (int) Math.pow(16, 6);
|
|
|
|
|
|
|
|
|
|
} // 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.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @return WAV file
|
|
|
|
|
*/
|
|
|
|
|
private static File retrieveWave(BufferedReader reader) {
|
|
|
|
|
|
|
|
|
|
String prompt = "Please provide a WAV file: " + ANSI_BEIGE;
|
|
|
|
|
|
|
|
|
|
File waveFile = null;
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
|
|
|
|
|
// Retrieve an audio file
|
|
|
|
|
waveFile = retrieveFile(reader, prompt, ".wav");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// validate that chosen file is a proper Wave file
|
|
|
|
|
RandomAccessFile file = new RandomAccessFile(waveFile, "r");
|
|
|
|
|
// first 4 bytes start with RIFF
|
|
|
|
|
if(file.readInt() == ASCII_RIFF) {
|
|
|
|
|
// next 4 bytes contain the size of the file in little-endian representation
|
|
|
|
|
file.seek(4);
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
file.read(buffer); // write size bytes to buffer
|
|
|
|
|
|
|
|
|
|
// calculate expected file size
|
|
|
|
|
int headerSize = 8 + buffer[3]
|
|
|
|
|
+ buffer[2] * (int) Math.pow(16, 2)
|
|
|
|
|
+ buffer[1] * (int) Math.pow(16, 4)
|
|
|
|
|
+ buffer[0] * (int) Math.pow(16, 6);
|
|
|
|
|
|
|
|
|
|
// file is valid if actual file length matches expected size
|
|
|
|
|
valid = headerSize == file.length();
|
|
|
|
|
if(!valid)
|
|
|
|
|
System.out.println("File length mismatch. Expected " + headerSize + " bytes, but found " + file.length() + " bytes.");
|
|
|
|
|
|
|
|
|
|
} else System.out.println("Invalid RIFF header.");
|
|
|
|
|
file.close();
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println("Invalid file.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return waveFile;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper function to prompt user to select a file.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @return file
|
|
|
|
|
*/
|
|
|
|
|
private static File retrieveFile(BufferedReader reader, String prompt, String extension) {
|
|
|
|
|
|
|
|
|
|
File file = null;
|
|
|
|
|
|
|
|
|
|
String input = null;
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
System.out.print(prompt);
|
|
|
|
|
try {
|
|
|
|
|
input = reader.readLine();
|
|
|
|
|
file = new File(input);
|
|
|
|
|
if(file.exists()) {
|
|
|
|
|
if(file.getName().toLowerCase().endsWith(extension)) {
|
|
|
|
|
valid = true;
|
|
|
|
|
} else System.out.println("File does not have " + extension.toUpperCase() + " file extension");
|
|
|
|
|
} else System.out.println("File does not exist.");
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid file."));
|
|
|
|
|
}
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return file;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper function to prompt user to select a color.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @return color attribute
|
|
|
|
|
*/
|
|
|
|
|
private static Attribute retrieveColor(BufferedReader reader) {
|
|
|
|
|
|
|
|
|
|
String prompt = "\nCommand parameters: " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT())
|
|
|
|
|
+ Ansi.colorize(" [color]", Attribute.BRIGHT_YELLOW_TEXT()) + Ansi.colorize(" [message]", Attribute.BRIGHT_RED_TEXT())
|
|
|
|
|
+ "\n"
|
|
|
|
|
+ "\n" + Ansi.colorize("Colors", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":"
|
|
|
|
|
+ "\n- " + Ansi.colorize("Default", Attribute.CLEAR())
|
|
|
|
|
+ "\n- " + Ansi.colorize("Black", Attribute.BLACK_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Gray", Attribute.BRIGHT_BLACK_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("White", Attribute.BRIGHT_WHITE_TEXT())
|
|
|
|
|
+ "\n- " + Ansi.colorize("Indigo", Attribute.TEXT_COLOR(0x7D, 0x1A, 0xFF))
|
|
|
|
|
+ ", " + Ansi.colorize("Purple", Attribute.BRIGHT_MAGENTA_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Violet", Attribute.TEXT_COLOR(0xEE, 0x82, 0xEE))
|
|
|
|
|
+ "\n- " + Ansi.colorize("Red", Attribute.RED_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Cinnabar", Attribute.BRIGHT_RED_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Pink", Attribute.TEXT_COLOR(0xFF, 0xC0, 0xCB))
|
|
|
|
|
+ "\n- " + Ansi.colorize("Orange", Attribute.TEXT_COLOR(0xFF, 0x68, 0x1F))
|
|
|
|
|
+ ", " + Ansi.colorize("Gold", Attribute.YELLOW_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Yellow", Attribute.TEXT_COLOR(0xFF, 0xEA, 0x00))
|
|
|
|
|
+ "\n- " + Ansi.colorize("Green", Attribute.BRIGHT_GREEN_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Chartreuse", Attribute.TEXT_COLOR(0xD1, 0xEB, 0x27))
|
|
|
|
|
+ ", " + Ansi.colorize("Beige", Attribute.BRIGHT_YELLOW_TEXT())
|
|
|
|
|
+ "\n- " + Ansi.colorize("Blue", Attribute.BRIGHT_BLUE_TEXT())
|
|
|
|
|
+ ", " + Ansi.colorize("Teal", Attribute.TEXT_COLOR(0x00, 0x93, 0xCF))
|
|
|
|
|
+ ", " + Ansi.colorize("Cyan", Attribute.BRIGHT_CYAN_TEXT())
|
|
|
|
|
+ "\n"
|
|
|
|
|
+ "\n" + Ansi.colorize("Please select a color:") + ANSI_BEIGE + " ";
|
|
|
|
|
String[] values = {"Default", "Black", "Gray", "White", "Indigo", "Purple", "Violet", "Red", "Cinnabar", "Pink", "Orange", "Gold",
|
|
|
|
|
"Yellow", "Green", "Chartreuse", "Beige","Blue", "Teal", "Cyan"};
|
|
|
|
|
|
|
|
|
|
String input = retrieveInput(reader, prompt, values);
|
|
|
|
|
|
|
|
|
|
Attribute color = null;
|
|
|
|
|
switch(input.toLowerCase()) {
|
|
|
|
|
case "default": color = Attribute.CLEAR(); break;
|
|
|
|
|
case "black": color = Attribute.BLACK_TEXT(); break;
|
|
|
|
|
case "gray": color = Attribute.BRIGHT_BLACK_TEXT(); break;
|
|
|
|
|
case "white": color = Attribute.BRIGHT_WHITE_TEXT(); break;
|
|
|
|
|
case "indigo": color = Attribute.TEXT_COLOR(0x7D, 0x1A, 0xFF); break;
|
|
|
|
|
case "purple": color = Attribute.BRIGHT_MAGENTA_TEXT(); break;
|
|
|
|
|
case "violet": color = Attribute.TEXT_COLOR(0xEE, 0x82, 0xEE); break;
|
|
|
|
|
case "red": color = Attribute.RED_TEXT(); break;
|
|
|
|
|
case "cinnabar": color = Attribute.BRIGHT_RED_TEXT(); break;
|
|
|
|
|
case "pink": color = Attribute.TEXT_COLOR(0xFF, 0xC0, 0xCB); break;
|
|
|
|
|
case "orange": color = Attribute.TEXT_COLOR(0xFF, 0x68, 0x1F); break;
|
|
|
|
|
case "gold": color = Attribute.YELLOW_TEXT(); break;
|
|
|
|
|
case "yellow": color = Attribute.TEXT_COLOR(0xFF, 0xEA, 0x00); break;
|
|
|
|
|
case "green": color = Attribute.BRIGHT_GREEN_TEXT(); break;
|
|
|
|
|
case "chartreuse": color = Attribute.TEXT_COLOR(0xD1, 0xEB, 0x27); break;
|
|
|
|
|
case "beige": color = Attribute.BRIGHT_YELLOW_TEXT(); break;
|
|
|
|
|
case "blue": color = Attribute.BRIGHT_BLUE_TEXT(); break;
|
|
|
|
|
case "teal": color = Attribute.TEXT_COLOR(0x00, 0x93, 0xCF); break;
|
|
|
|
|
case "cyan": color = Attribute.BRIGHT_CYAN_TEXT(); break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(CMD_MODE)
|
|
|
|
|
System.out.println(ANSI_BACKLINE + Ansi.colorize("Please select a color: ") + Ansi.colorize(input, color) + "\n");
|
|
|
|
|
else System.out.println(Ansi.colorize(""));
|
|
|
|
|
|
|
|
|
|
return color;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper function to prompt user to select a program task.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @param input
|
|
|
|
|
* @return operation
|
|
|
|
|
*/
|
|
|
|
|
private static String retrieveOperation(BufferedReader reader, String input) {
|
|
|
|
|
|
|
|
|
|
String operation = null;
|
|
|
|
|
String[] values = {"extract", "package", "patch", "print", "exit"};
|
|
|
|
|
|
|
|
|
|
if(input != null)
|
|
|
|
|
for(int i = 0; i < values.length; i++)
|
|
|
|
|
if(values[i].equalsIgnoreCase(input))
|
|
|
|
|
operation = values[i];
|
|
|
|
|
|
|
|
|
|
if(operation == null) {
|
|
|
|
|
|
|
|
|
|
String prompt = "\n" + Ansi.colorize("Operations", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Extract", Attribute.BRIGHT_RED_TEXT()) + " audio tracks from package\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Package", Attribute.YELLOW_TEXT()) + " audio tracks into package\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Patch", Attribute.BRIGHT_GREEN_TEXT()) + " audio track to loop indefinitely\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT()) + " Ridge Racer 6 style ASCII logo\n"
|
|
|
|
|
+ "- " + Ansi.colorize("Exit", Attribute.BRIGHT_MAGENTA_TEXT()) + " program\n"
|
|
|
|
|
+ "\nPlease select an operation: " + ANSI_BEIGE;
|
|
|
|
|
|
|
|
|
|
operation = retrieveInput(reader, prompt, values);
|
|
|
|
|
|
|
|
|
|
if(CMD_MODE) {
|
|
|
|
|
|
|
|
|
|
Attribute color = null;
|
|
|
|
|
switch(operation.toLowerCase()) {
|
|
|
|
|
case "extract":
|
|
|
|
|
color = Attribute.BRIGHT_RED_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "package":
|
|
|
|
|
color = Attribute.YELLOW_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "patch":
|
|
|
|
|
color = Attribute.BRIGHT_GREEN_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "print":
|
|
|
|
|
color = Attribute.BRIGHT_BLUE_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
case "exit":
|
|
|
|
|
color = Attribute.BRIGHT_MAGENTA_TEXT();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
System.out.println(ANSI_BACKLINE + Ansi.colorize("Please select an operation: ") + Ansi.colorize(operation, color));
|
|
|
|
|
|
|
|
|
|
} else System.out.print(Ansi.colorize(""));
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return operation.toLowerCase();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prompts the user for input without strict validation.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @param prompt
|
|
|
|
|
* @return input
|
|
|
|
|
*/
|
|
|
|
|
private static String retrieveInput(BufferedReader reader, String prompt) {
|
|
|
|
|
|
|
|
|
|
String input = null;
|
|
|
|
|
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
System.out.print(prompt);
|
|
|
|
|
try {
|
|
|
|
|
input = reader.readLine();
|
|
|
|
|
valid = true;
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid input."));
|
|
|
|
|
}
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return input;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prompts the user for input and validates that the input matches one of the provided values.
|
|
|
|
|
*
|
|
|
|
|
* @param reader
|
|
|
|
|
* @param prompt
|
|
|
|
|
* @param values
|
|
|
|
|
* @return input
|
|
|
|
|
*/
|
|
|
|
|
private static String retrieveInput(BufferedReader reader, String prompt, String[] values) {
|
|
|
|
|
|
|
|
|
|
String input = null;
|
|
|
|
|
|
|
|
|
|
boolean valid = false;
|
|
|
|
|
do {
|
|
|
|
|
System.out.print(prompt);
|
|
|
|
|
try {
|
|
|
|
|
input = reader.readLine();
|
|
|
|
|
for(int i = 0; i < values.length; i++)
|
|
|
|
|
if(input.equalsIgnoreCase(values[i])) {
|
|
|
|
|
valid = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if(!valid)
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid selection."));
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.out.println(Ansi.colorize("Invalid selection."));
|
|
|
|
|
}
|
|
|
|
|
} while(!valid);
|
|
|
|
|
|
|
|
|
|
return input;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper function to read the size of a file chunk.
|
|
|
|
|
*
|
|
|
|
|
* @param file
|
|
|
|
|
* @param chunkAddress
|
|
|
|
|
* @return chunk size
|
|
|
|
|
* @throws IOException
|
|
|
|
|
*/
|
|
|
|
|
private static int readChunkSize(File file, int chunkAddress) throws IOException {
|
|
|
|
|
|
|
|
|
|
RandomAccessFile accessedFile = new RandomAccessFile(file, "r");
|
|
|
|
|
return readChunkSize(accessedFile, chunkAddress);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper function to read the size of a file chunk.
|
|
|
|
|
*
|
|
|
|
|
* @param accessedFile
|
|
|
|
|
* @param chunkAddress
|
|
|
|
|
* @return chunk size
|
|
|
|
|
* @throws IOException
|
|
|
|
|
*/
|
|
|
|
|
private static int readChunkSize(RandomAccessFile file, int chunkAddress) throws IOException {
|
|
|
|
|
|
|
|
|
|
byte[] buffer = new byte[4];
|
|
|
|
|
|
|
|
|
|
file.seek(chunkAddress + 4);
|
|
|
|
|
file.read(buffer);
|
|
|
|
|
|
|
|
|
|
return 8 // size offset
|
|
|
|
|
+ buffer[3] // 16^0
|
|
|
|
|
+ buffer[2] * 256 // 16^2
|
|
|
|
|
+ buffer[1] * 65_536 // 16^4
|
|
|
|
|
+ buffer[0] * 16_777_216; // 16^6
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a map of audio tracks within provided file.
|
|
|
|
@ -697,7 +837,6 @@ public class AudioExtractor {
|
|
|
|
|
public static LinkedHashMap<Integer, Integer> findAudioTracks(RandomAccessFile file) throws IOException {
|
|
|
|
|
|
|
|
|
|
LinkedHashMap<Integer, Integer> tracklist = new LinkedHashMap<Integer, Integer>();
|
|
|
|
|
int key = 0x52494646; // "RIFF" in ASCII representation
|
|
|
|
|
|
|
|
|
|
// Files start on addresses divisible by 0x800
|
|
|
|
|
for(int a = 0; a + 0x800 < file.length(); a += 0x800) {
|
|
|
|
@ -705,7 +844,7 @@ public class AudioExtractor {
|
|
|
|
|
file.seek(a);
|
|
|
|
|
|
|
|
|
|
// File header starts with "RIFF" sequence
|
|
|
|
|
if(file.readInt() == key) {
|
|
|
|
|
if(file.readInt() == ASCII_RIFF) {
|
|
|
|
|
|
|
|
|
|
// Next 4 bytes specify file length beyond first 8 bytes, in little-endian representation
|
|
|
|
|
int length = 8;
|
|
|
|
@ -725,11 +864,6 @@ public class AudioExtractor {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static String logoFormat(String string, boolean italic) {
|
|
|
|
|
return logoFormat(string, italic, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates an ASCII logo in RR6 style using the provided text.
|
|
|
|
|
* Supports alphanumeric text with spaces only.
|
|
|
|
@ -741,6 +875,18 @@ public class AudioExtractor {
|
|
|
|
|
public static String logoFormat(String text) {
|
|
|
|
|
return logoFormat(text, true, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates an ASCII logo in RR6 style using the provided text.
|
|
|
|
|
* Supports alphanumeric text with spaces only.
|
|
|
|
|
*
|
|
|
|
|
* @param string
|
|
|
|
|
* @param italicize logo
|
|
|
|
|
* @return logo formatted text
|
|
|
|
|
*/
|
|
|
|
|
private static String logoFormat(String string, boolean italic) {
|
|
|
|
|
return logoFormat(string, italic, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates an ASCII logo in RR6 style using the provided text.
|
|
|
|
|