Compare commits

..

2 Commits

Author SHA1 Message Date
Nes370 d05b5ca4b6 Added CSV files for track metadata 2024-07-22 09:50:53 -07:00
Nes370 08aa16493c Update AudioExtractor.java 2024-07-20 10:35:58 -07:00
4 changed files with 1698 additions and 369 deletions

21
pack_bgm.csv Normal file
View File

@ -0,0 +1,21 @@
Track Title Artist Album
1 Explorers Hiroshi Okubo Green Album
2 Valley of the Mind SamplingMasters MEGA Green Album
3 Nitro Mantra SamplingMasters AYA Green Album
4 I Want You SamplingMasters AYA Green Album
5 Road Mauler Rio Hamamoto Green Album
6 Floodlight sanodg White Album
7 Drift Psychosis Hiroshi Okubo White Album
8 Sue?o Del Mar Asuka Sakai White Album
9 Trail of Light J99 White Album
10 Ultra Cruise Tetsukazu Nakanishi White Album
11 Highway Fusion SamplingMasters MEGA Black Album
12 Acid Eutron #001 Acid Eutron Black Album
13 Radiance sanodg Black Album
14 Photon Field Koji Nakagawa Black Album
15 Galactic Life "Kohta ""SOLIDSTATE"" Takahashi" Black Album
16 Run PAC-MAN Run! Akitaka Tohyama Black Album
17
18
19
20
1 Track Title Artist Album
2 1 Explorers Hiroshi Okubo Green Album
3 2 Valley of the Mind SamplingMasters MEGA Green Album
4 3 Nitro Mantra SamplingMasters AYA Green Album
5 4 I Want You SamplingMasters AYA Green Album
6 5 Road Mauler Rio Hamamoto Green Album
7 6 Floodlight sanodg White Album
8 7 Drift Psychosis Hiroshi Okubo White Album
9 8 Sue?o Del Mar Asuka Sakai White Album
10 9 Trail of Light J99 White Album
11 10 Ultra Cruise Tetsukazu Nakanishi White Album
12 11 Highway Fusion SamplingMasters MEGA Black Album
13 12 Acid Eutron #001 Acid Eutron Black Album
14 13 Radiance sanodg Black Album
15 14 Photon Field Koji Nakagawa Black Album
16 15 Galactic Life Kohta "SOLIDSTATE" Takahashi Black Album
17 16 Run PAC-MAN Run! Akitaka Tohyama Black Album
18 17
19 18
20 19
21 20

0
pack_cheer.csv Normal file
View File

1162
pack_dj_menu.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,19 +29,38 @@ import com.diogonunes.jcolor.Attribute;
*/ */
public class AudioExtractor { 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. * Activate alternate behavior for command line terminal.
* Specifically prints things in a narrower width, and overwrites previous lines to add color. * 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. * 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 { public static void main(String[] args) throws URISyntaxException, InterruptedException {
if(CMD) { // Print Audio Extractor logo
if(CMD_MODE) {
// NARROW LOGO // NARROW LOGO
String adjustment = "█████████ ████████████████████████████████████████████████████ ████████████████████████";
System.out.println(Ansi.colorize(logoFormat("Ridge", true, 8)/*.substring(0, 484) + adjustment*/, Attribute.BRIGHT_RED_TEXT())); System.out.println(Ansi.colorize(logoFormat("Ridge", true, 8)/*.substring(0, 484) + adjustment*/, Attribute.BRIGHT_RED_TEXT()));
Thread.sleep(500); 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())); System.out.println(Ansi.colorize(logoFormat("Racer 6", true, 9), Attribute.BRIGHT_RED_TEXT()));
Thread.sleep(500); Thread.sleep(500);
System.out.println(Ansi.colorize(logoFormat("Audio", true, 8), Attribute.YELLOW_TEXT())); System.out.println(Ansi.colorize(logoFormat("Audio", true, 8), Attribute.YELLOW_TEXT()));
Thread.sleep(500); Thread.sleep(500);
System.out.println(Ansi.colorize(logoFormat("Xtractr", true, 8), Attribute.YELLOW_TEXT())); System.out.println(Ansi.colorize(logoFormat("Xtractr", true, 8), Attribute.YELLOW_TEXT()));
Thread.sleep(500); Thread.sleep(500);
} else { } else {
// WIDE LOGO // WIDE LOGO
System.out.println(Ansi.colorize(logoFormat("Ridge Racer 6"), Attribute.BRIGHT_RED_TEXT())); System.out.println(Ansi.colorize(logoFormat("Ridge Racer 6"), Attribute.BRIGHT_RED_TEXT()));
@ -80,11 +105,12 @@ public class AudioExtractor {
Thread.sleep(500); Thread.sleep(500);
} }
// Print meta info
System.out.println("Program:\tRidge Racer 6 Audio Extractor " + Ansi.colorize("v1.0", Attribute.BRIGHT_GREEN_TEXT()) + " by " 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))); + 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())); 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)); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
// determine which operation to execute // determine which operation to execute
@ -116,7 +142,7 @@ public class AudioExtractor {
break; break;
case "print": // print case "print": // print
Attribute color = retrieveColor(reader); 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)); System.out.println(Ansi.colorize(logoFormat(message), color));
break; break;
case "exit": // exit case "exit": // exit
@ -204,358 +230,15 @@ public class AudioExtractor {
else extract(packDirectory, extractDirectory, compressBGM); 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 packDirectory
* @param prompt * @param extractDirectory
* @param values * @param compressBGM
* @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;
}
/**
* 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) { public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) {
// TODO Delete // 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. * 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 { public static LinkedHashMap<Integer, Integer> findAudioTracks(RandomAccessFile file) throws IOException {
LinkedHashMap<Integer, Integer> tracklist = new LinkedHashMap<Integer, Integer>(); LinkedHashMap<Integer, Integer> tracklist = new LinkedHashMap<Integer, Integer>();
int key = 0x52494646; // "RIFF" in ASCII representation
// Files start on addresses divisible by 0x800 // Files start on addresses divisible by 0x800
for(int a = 0; a + 0x800 < file.length(); a += 0x800) { for(int a = 0; a + 0x800 < file.length(); a += 0x800) {
@ -705,7 +844,7 @@ public class AudioExtractor {
file.seek(a); file.seek(a);
// File header starts with "RIFF" sequence // 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 // Next 4 bytes specify file length beyond first 8 bytes, in little-endian representation
int length = 8; 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. * Creates an ASCII logo in RR6 style using the provided text.
* Supports alphanumeric text with spaces only. * Supports alphanumeric text with spaces only.
@ -741,6 +875,18 @@ public class AudioExtractor {
public static String logoFormat(String text) { public static String logoFormat(String text) {
return logoFormat(text, true, 10); 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. * Creates an ASCII logo in RR6 style using the provided text.