diff --git a/src/goblincave/gitea/nes/Analyzer.java b/src/goblincave/gitea/nes/Analyzer.java new file mode 100644 index 0000000..2f5ac8c --- /dev/null +++ b/src/goblincave/gitea/nes/Analyzer.java @@ -0,0 +1,550 @@ +package goblincave.gitea.nes; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.util.LinkedHashMap; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; + +public class Analyzer { + + /** + * Hex value for the ASCII sequence "RIFF". + * Indicates the start of a RIFF header in WAV files. + */ + final static int ASCII_RIFF = 0x5249_4646; + /** + * Hex value for the ASCII sequence "RIFF". + * Indicates the start of a RIFF header in WAV files. + */ + final static int ASCII_WAVE = 0x5741_5645; + /** + * Hex value for the ASCII sequence "fmt ". + * Indicates the start of a format chunk in WAV files. + */ + final static int ASCII_fmt = 0x666D_7420; + /** + * Hex value for the ASCII sequence "smpl". + * Indicates the start of a sample chunk in WAV files. + */ + final static int ASCII_smpl = 0x736D_706C; + /** + * Hex value for the ASCII sequence "data". + * Indicates the start of a data chunk in WAV files. + */ + final static int ASCII_data = 0x6461_7461; + /** + * Hex value for the ASCII sequence "ALIG". + * Indicates the start of an alignment chunk in WAV files. + */ + 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. + */ + 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. + */ + final static int ASCII_seek = 0x7365_656B; + /** + * Hex value for the ASCII sequence "fLaC". + * Indicates the start of an FLAC file. + */ + final static int ASCII_fLaC = 0x664C_6143; + + /** + * Creates a map of audio tracks within provided file. + * + * @param file + * @return Tracklist containing address-length pairs + * @throws IOException + */ + static LinkedHashMap findAudioTracks(RandomAccessFile file) throws IOException { + + LinkedHashMap tracklist = new LinkedHashMap(); + + // Files start on addresses divisible by 0x800 + for(int a = 0; a + 0x800 < file.length(); a += 0x800) { + + file.seek(a); + + // File header starts with "RIFF" sequence + if(file.readInt() == ASCII_RIFF) { + + // Next 4 bytes specify file length beyond first 8 bytes, in little-endian representation + int length = 8; + // Calculate remaining length of file + for(int d = 0; d < 4; d++) { + file.seek(a + 4 + d); + length += file.readUnsignedByte() * ((int) Math.pow(16, 2 * d)); + } + + // Record file position in map + tracklist.put(a, length); + } + + } + + return tracklist; + + } + + /** + * Helper method to read a little-endian byte field and print its contents. + * + * @param file + * @param address + * @param size + * @param message + * @return value + * @throws IOException + */ + static int parseField(RandomAccessFile file, int address, int size, String message) throws IOException { + + byte[] bytes = new byte[size]; + file.seek(address); + file.read(bytes); + int value = littleEndianToInt(bytes); + + System.out.println(formatAddress(address, file.length()) + ":\t" + value + message); + + return value; + + } + + + + static String formatAddress(long address, long size) { + + int digits = (int) Math.ceil(Math.log(size) / Math.log(16)); + + return String.format("%0" + digits + "X", address); + + } + + /** + * Formats a file address with leading zeroes. + * + * @param address + * @return padded address + */ + static String formatAddress(long address) { + + return String.format("%08X", address); + + } + + /** + * Helper function to read RIFF header information. + * + * @param file + * @param offset + * @return file end address + * @throws IOException + */ + static int readRIFFChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tRIFF (Resource Interchange File Format) file"); + + int size = -1; + if(offset + 8 < file.length()) { + size = readChunkSize(file, offset); + System.out.println(formatAddress(offset + 4, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + if(offset + 12 < file.length()) { + file.seek(offset + 8); + if(file.readInt() == ASCII_WAVE) + System.out.println(formatAddress(offset + 8, file.length()) + ":\tWAVE (Waveform Audio File Format) type"); + offset += 8; + } + + System.out.println(); + return offset + size; + + } + + /** + * Helper function to read the size of a file chunk. + * + * @param file + * @param chunkAddress + * @return chunk size + * @throws IOException + */ + 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 + */ + 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[0] & 0xFF) // 256^0 + + (buffer[1] & 0xFF) * 256 // 256^1 + + (buffer[2] & 0xFF) * 65_536 // 256^2 + + (buffer[3] & 0xFF) * 16_777_216; // 256^3 + + } + + /** + * Helper method to read format chunk information. + * + * @param file + * @param offset from start of file to start of format chunk + * @return offset from start of file to end of format chunk + * @throws IOException + */ + static int readFormatChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tfmt (Format) chunk"); + + int size = 8; + if(offset + 8 < file.length()) { + size = readChunkSize(file, (int) offset); + System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + if(offset + size < file.length()) { + + byte[] LEBytes = new byte[2]; + file.seek(offset + 0x08); + file.read(LEBytes); + int encoding = littleEndianToInt(LEBytes); + System.out.print(formatAddress(offset + 0x08, file.length()) + ":\t"); + if(encoding == 0x0001) + System.out.print("PCM (Pulse-Code Modulation)"); + else if(encoding == 0x0002) + System.out.print("ADPCM (Adaptive Differential Pulse-Code Modulation)"); + else if(encoding == 0x0165) + System.out.print("XMA (Xbox Media Audio)"); + else if(encoding == 0x0166) + System.out.print("XMA2 (Xbox Media Audio 2)"); + else { + boolean found = false; + InputStream filestream = AudioExtractor.class.getResourceAsStream("/encoding.csv"); + InputStreamReader streamReader = new InputStreamReader(filestream); + Reader in = streamReader; + String[] headers = { "Decimal value", "Hexadecimal value", "Description" }; + Iterable records = CSVFormat.EXCEL.builder().setHeader(headers).setSkipHeaderRecord(true).build().parse(in); + for (CSVRecord record : records) { + String dec = record.get("Decimal value"); + String desc = record.get("Description"); + if(encoding == Integer.parseInt(dec)) { + System.out.print(desc); + found = true; + break; + } + } + if(!found) + System.out.print("Unknown (" + encoding + ")"); + } + System.out.println(" encoding"); + + if(encoding == 0x0165) { + LEBytes = new byte[2]; + file.seek(offset + 0x10); + file.read(LEBytes); + int streams = littleEndianToInt(LEBytes); + System.out.println(formatAddress(offset + 0x10, file.length()) + ":\t" + streams + (streams == 1 ? " stream" : " streams")); + parseField(file, offset + 0x12, 1, " loop flag"); + // unknown 5 bytes ? + parseField(file, offset + 0x18, 4, " Hz sampling rate"); + parseField(file, offset + 0x1C, 4, " loop start"); + parseField(file, offset + 0x20, 4, " loop end"); + parseField(file, offset + 0x24, 1, " loop subframe"); + // unknown 3 bytes ? + +// int channels = 0; +// for(int i = 0; i < streams; i++) +// channels += parseField(file, offset + 0x08 + 0x0C + 0x14 * i + 0x11, 1, " channel"); +// System.out.println(channels + " channels"); + } else { + parseField(file, offset + 0x0A, 2, " channels"); + parseField(file, offset + 0x0C, 4, " Hz sampling rate"); + parseField(file, offset + 0x10, 4, " average bytes per second"); + parseField(file, offset + 0x14, 2, "-byte sample block size"); + parseField(file, offset + 0x16, 2, " significant bits per sample"); + LEBytes = new byte[2]; + file.seek(offset + 0x18); + file.read(LEBytes); + int extra = littleEndianToInt(LEBytes); + if(extra != ASCII_smpl) + parseField(file, offset + 0x18, 2, " extra format bytes"); + } + + } + + System.out.println(); + return offset + size; + + } + + static int readDataChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tdata (Data) chunk"); + + int size = 8; + if(offset + 8 < file.length()) { + size = readChunkSize(file, (int) offset); + System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + System.out.println(); + return offset + size; + + } + + + + static int readSampleChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tsmpl (Sample) chunk"); + + int size = 8; + if(offset + 8 < file.length()) { + size = readChunkSize(file, (int) offset); + System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + // 0x08 4 bytes manufacturer code + byte[] bytes = new byte[4]; + file.seek(offset + 0x08); + file.read(bytes); + int relevantBytes = bytes[0]; + String code; + if(relevantBytes == 3) + code = String.format("%02XH %02XH %02XH", bytes[1], bytes[2], bytes[3]); + else code = String.format("%02XH", bytes[3]); + System.out.print(formatAddress(offset + 0x08, file.length()) + ":\t"); + boolean found = false; + InputStream filestream = AudioExtractor.class.getResourceAsStream("/manufacturer.csv"); + InputStreamReader streamReader = new InputStreamReader(filestream); + Reader in = streamReader; + String[] headers = { "SysEx ID Number", "Company Name" }; + Iterable records = CSVFormat.EXCEL.builder().setHeader(headers).setSkipHeaderRecord(true).build().parse(in); + for (CSVRecord record : records) { + String ID = record.get("SysEx ID Number"); + String manufacturer = record.get("Company Name"); + if(code.equalsIgnoreCase(ID)) { + System.out.print(manufacturer + " (" + code + ")"); + found = true; + break; + } + } + if(!found) + System.out.print("Unknown (" + code + ")"); + System.out.println(" manufacturer"); + + parseField(file, offset + 0x0C, 4, " product code"); + parseField(file, offset + 0x10, 4, "-nanosecond sample period"); + parseField(file, offset + 0x14, 4, " MIDI unity note"); + parseField(file, offset + 0x18, 4, " MIDI pitch fraction"); + parseField(file, offset + 0x1C, 4, " SMPTE format"); + parseField(file, offset + 0x20, 4, " SMPTE offset"); + int loops = parseField(file, offset + 0x24, 4, " sample loops"); + parseField(file, offset + 0x28, 4, " sample data"); + + for(int i = 0; i < loops; i++) { + int loopOffset = offset + 0x2C + i * 0x18; + parseField(file, loopOffset, 4, " loop ID"); + + bytes = new byte[4]; + file.seek(loopOffset + 0x04); + file.read(bytes); + int loopType = littleEndianToInt(bytes); + System.out.print(formatAddress(loopOffset + 0x04, file.length()) + ":\t"); + String type; + switch(loopType) { + case 0: + type = "Forward"; + break; + case 1: + type = "Alternating"; + break; + case 2: + type = "Backward"; + break; + default: + type = "Unknown"; + break; + } + type += " (" + loopType + ") loop type"; + System.out.println(type); + + file.seek(loopOffset + 0x08); + file.read(bytes); + int loopStart = littleEndianToInt(bytes); + System.out.println(formatAddress(loopOffset + 0x08, file.length()) + ":\tLoop start @ sample " + loopStart); + + file.seek(loopOffset + 0x0C); + file.read(bytes); + int loopEnd = littleEndianToInt(bytes); + System.out.println(formatAddress(loopOffset + 0x08, file.length()) + ":\tLoop end @ sample " + loopEnd); + + parseField(file, loopOffset + 0x10, 4, " loop fraction"); + + file.seek(loopOffset + 0x14); + file.read(bytes); + int repeat = littleEndianToInt(bytes); + if(repeat == 0) + System.out.println(formatAddress(loopOffset + 0x14, file.length()) + ":\tRepeat indefinitely"); + else System.out.println(formatAddress(loopOffset + 0x14, file.length()) + ":\t" + repeat + "repeat count"); + + } + + System.out.println(); + return offset + size; + + } + + + + /** + * Helper function to read the size of a x2st chunk. + * + * @param file + * @param offset + * @return chunk end address + * @throws IOException + */ + static int readx2stChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tx2st chunk"); + + int size = 8; + if(offset + 8 < file.length()) { + size = readChunkSize(file, (int) offset); + System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + System.out.println(); + return offset + size; + + } + + /** + * Helper function to read the size of an alignment chunk. + * + * @param file + * @param offset + * @return chunk end address + * @throws IOException + */ + static int readAlignmentChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tALIG (Alignment) chunk"); + + int size = 8; + if(offset + 8 < file.length()) { + size = readChunkSize(file, (int) offset); + System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + System.out.println(); + return offset + size; + + } + + /** + * Helper function to read the size of an seek chunk. + * + * @param file + * @param offset + * @return chunk end address + * @throws IOException + */ + static int readSeekChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tseek (Seek) chunk"); + + int size = 8; + if(offset + 8 < file.length()) { + size = readChunkSize(file, (int) offset); + System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); + } + + System.out.println(); + return offset + size; + + } + + /** + * Get the name of a WAVE encoding from its value. + * + * @param encoding + * @return + */ + static String identifyEncoding(int encoding) { + // TODO Auto-generated method stub + System.out.println("identifyEncoding(" + encoding + ") is not yet implemented."); + return null; + } + + /** + * Helper method to convert a number of bytes to a more readable form. + * + * @param bytes + * @return legible form + */ + static String convertBytes(long bytes) { + + final String[] units = new String[] {"B", "KB", "MB", "GB", "TB"}; + int i = (int) (Math.log10(bytes) / Math.log10(1024)); + double value = bytes / Math.pow(1024, i); + return String.format("%.2f %s", value, units[i]); + + } + + /** + * Helper method that converts a little-endian byte array into a long value. + * + * @param bytes (up to 8 bytes) + * @return value + */ + static long littleEndianToLong(byte[] bytes) { + if(bytes.length > 8) + throw new IllegalArgumentException("Byte array must be 8 bytes or shorter."); + long value = 0; + for(int i = 0; i < bytes.length; i++) + value |= (long) (bytes[i] & 0xFF) << (8 * i); + return value; + } + + /** + * Helper method that converts a little-endian byte array into a long value. + * + * @param bytes (up to 8 bytes) + * @return value + */ + static int littleEndianToInt(byte[] bytes) { + if(bytes.length > 8) + throw new IllegalArgumentException("Byte array must be 4 bytes or shorter."); + int value = 0; + for(int i = 0; i < bytes.length; i++) + value |= (int) (bytes[i] & 0xFF) << (8 * i); + return value; + } + +} diff --git a/src/goblincave/gitea/nes/AudioExtractor.java b/src/goblincave/gitea/nes/AudioExtractor.java index a8209ec..bdf84a3 100644 --- a/src/goblincave/gitea/nes/AudioExtractor.java +++ b/src/goblincave/gitea/nes/AudioExtractor.java @@ -7,19 +7,12 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; -import java.io.Reader; import java.net.URISyntaxException; -import java.nio.file.Files; import java.util.LinkedHashMap; import java.util.Set; -import javaFlacEncoder.FLAC_FileEncoder; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.FilenameUtils; import com.diogonunes.jcolor.Ansi; @@ -55,33 +48,52 @@ public class AudioExtractor { */ public static void main(String[] args) throws URISyntaxException, InterruptedException { - if(args.length == 0) { + if(args.length > 0) { + switch(args[0].toLowerCase()) { + case "identify": { + File file = new File(args[0]); + identify(file); + break; + } + case "extract": { + File packageFile = new File(args[0]); + extract(packageFile); + break; + } + case "convert": { + if(args.length < 2) + throw new IllegalArgumentException("Missing one or more required parameters."); + + } + default: throw new IllegalArgumentException(args[0] + " is not a valid command."); + } + } else { // Print Audio Extractor logo if(CMD_MODE) { // NARROW LOGO - System.out.println(Ansi.colorize(logoFormat("Ridge", true, 8)/*.substring(0, 484) + adjustment*/, Attribute.BRIGHT_RED_TEXT())); + System.out.println(Ansi.colorize(Logo.format("Ridge", true, 8), Attribute.BRIGHT_RED_TEXT())); Thread.sleep(500); 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(Logo.format("Racer 6", true, 9), Attribute.BRIGHT_RED_TEXT())); Thread.sleep(500); - System.out.println(Ansi.colorize(logoFormat("Audio", true, 8), Attribute.YELLOW_TEXT())); + System.out.println(Ansi.colorize(Logo.format("Audio", true, 8), Attribute.YELLOW_TEXT())); Thread.sleep(500); - System.out.println(Ansi.colorize(logoFormat("Xtractr", true, 8), Attribute.YELLOW_TEXT())); + System.out.println(Ansi.colorize(Logo.format("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())); + System.out.println(Ansi.colorize(Logo.format("Ridge Racer 6"), Attribute.BRIGHT_RED_TEXT())); Thread.sleep(500); - System.out.println(Ansi.colorize(logoFormat("Audio Extractor", true, 8), Attribute.YELLOW_TEXT())); + System.out.println(Ansi.colorize(Logo.format("Audio Extractor", true, 8), Attribute.YELLOW_TEXT())); Thread.sleep(500); } @@ -132,7 +144,9 @@ public class AudioExtractor { case "convert": { File audioFile = retrieveAudio(reader); // Confirms file has WAV/XMA/FLAC extension + header - + //state which encoding file uses + //user selects target format + String format = retrieveFormat(reader); // if(audioFile.isFile()) { // String extension = FilenameUtils.getExtension(audioFile.getName()); // // Ensure file has the right encoding (differentiate between WAV/XMA) @@ -147,8 +161,8 @@ public class AudioExtractor { // e.printStackTrace(); // } - convert(audioFile, extension); - } + convert(audioFile, format); +// } } @@ -162,7 +176,7 @@ public class AudioExtractor { case "print": // print Attribute color = retrieveColor(reader); String message = retrieveInput(reader, "Please enter your logo message: " + ANSI_BEIGE); - System.out.println(Ansi.colorize(logoFormat(message), color)); + print(color, message); break; case "exit": // exit System.out.println("Program closed."); @@ -177,598 +191,9 @@ public class AudioExtractor { } } - - } - - /** - * Converts an audio file to the target format. - * - * @param audioFile - * @param format - */ - private static void convert(File audioFile, String format) { - - - } - /** - * Helper function to prompt user to select a convertible audio file. - * - * @param reader - * @return audio file - */ - private static File retrieveAudio(BufferedReader reader) { - - File file = null; - - String input = null; - boolean valid = false; - do { - System.out.print("Please enter the path of a file\t" + ANSI_BEIGE); - try { - input = reader.readLine(); - file = new File(input); - if(file.exists()) { - if(file.isFile()) { - - String name = file.getName().toLowerCase(); - String extension = FilenameUtils.getExtension(name); - - if(extension.equals("wav") || extension.equals("xma")) { - // check for RIFF header - byte[] bytes = new byte[4]; - RandomAccessFile raf = new RandomAccessFile(file, "r"); - raf.seek(0x0); - if(raf.readInt() == ASCII_RIFF) { - raf.seek(0x14); - raf.read(bytes); - int encoding = littleEndianToInt(bytes); - if(encoding == 0x0001) - valid = true; - else if(encoding == 0x0165) - valid = true; - else { - System.out.println("Unsupported encoding" + identifyEncoding(encoding) + "."); - } - } - else System.out.println("Not a valid RIFF file."); - - } else if (name.endsWith(".flac")) { - // check for FLAC header - byte[] bytes = new byte[4]; - RandomAccessFile raf = new RandomAccessFile(file, "r"); - raf.seek(0); - if(raf.readInt() == ASCII_fLaC) - valid = true; - else System.out.println("Not a valid FLAC file."); - - } else System.out.println(Ansi.colorize("File does not have a supported file extension.")); - - } - } else System.out.println(Ansi.colorize("File does not exist.")); - } catch (IOException e) { - System.out.println(Ansi.colorize("Invalid file.")); - } - } while(!valid); - - return file; - } - /** - * Get the name of a WAVE encoding from its value. - * - * @param encoding - * @return - */ - private static String identifyEncoding(int encoding) { - // TODO Auto-generated method stub - return null; - } - - /** - * 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 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. - * - * @param packageFile - * @throws FileNotFoundException - */ - private static void extract(File packageFile) { - - System.out.println("Name:\t" + packageFile.getName()); - System.out.println("Size:\t" + packageFile.length() + " (" + convertBytes(packageFile.length()) + ")\n"); - - try { - RandomAccessFile file = new RandomAccessFile(packageFile, "r"); - LinkedHashMap tracklist = findAudioTracks(file); - - File dir = new File(packageFile.getAbsoluteFile().getParentFile().toPath() + "/" - + FilenameUtils.removeExtension(packageFile.getName())); - if(!dir.exists()) - dir.mkdir(); - - System.out.println("Extracting " + tracklist.size() + " tracks..."); - - int digits = (int) Math.ceil(Math.log10(tracklist.size() + 1)); - int extracted = 0; - Set keys = tracklist.keySet(); - for(Integer address : keys) { - int length = tracklist.get(address); - - if(CMD_MODE) - printProgressBar(extracted, keys.size()); - - String extension; - byte[] bytes = new byte[2]; - file.seek(address + 0x14); - file.read(bytes); - int encoding = littleEndianToInt(bytes); - switch(encoding) { - case 0x0165: extension = "xma"; break; - case 0x0001: extension = "wav"; break; - default: extension = "bin"; break; - } - - File trackFile = new File(dir.toPath() + "/track" + String.format("%0" + digits + "d", extracted + 1) + "." + extension); - - try ( - FileInputStream in = new FileInputStream(packageFile); - FileOutputStream out = new FileOutputStream(trackFile); - ) { - in.skipNBytes(address.intValue()); - bytes = new byte[length]; - int readBytes = in.read(bytes); - out.write(bytes, 0, readBytes); - } catch (IOException e) { - e.printStackTrace(); - } - extracted++; - } - - if(CMD_MODE) - printProgressBar(extracted, keys.size()); - - } catch(Exception e) { - e.printStackTrace(); - } - - System.out.println("Finished.\n"); - - } - - /** - * Extracts audio files to the extract directory from package files located at the package directory. - * Also supports FLAC conversion for BGM files. - * - * @param packDirectory - * @param extractDirectory - * @param compressBGM - * @deprecated delete after adapting FLAC conversion - */ - public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) { - - // TODO Delete - System.out.println("packDirectory:\t" + packDirectory); - System.out.println("extractDirectory:\t" + extractDirectory); - - // Identify target binary files - File[] packages = new File(packDirectory.toString()).listFiles(new FilenameFilter() { - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".bin"); - } - }); - - // Identify and extract audio within binary files - for(int i = 0; i < packages.length; i++) { - - RandomAccessFile source; - LinkedHashMap tracklist = null; - try { - source = new RandomAccessFile(packages[i], "r"); - tracklist = findAudioTracks(source); - } catch (FileNotFoundException e) { - System.out.println("Binary file:\t" + packages[i] + "\n was not found. File skipped."); - continue; - } catch (IOException e) { - System.out.println("Binary file:\t" + packages[i] + "\n could not be read. File skipped."); - continue; - } - - //TODO Delete - System.out.println("packages[" + i + "]:\t" + packages[i]); - - File dir = new File(extractDirectory.getPath() + "/" + FilenameUtils.removeExtension(packages[i].getName())); - if(!dir.exists()) - dir.mkdir(); - - // Extract and write WAV files in directory - Set keys = tracklist.keySet(); - int track = 1; - - for(Integer key : keys) { - - String name = String.format("track%04d", track); - { // Extract WAV files - File wav = null; - try { - - // create file for storing WAV data - wav = new File(dir.getPath() + "/" + name + ".wav"); - if(!wav.exists()) - wav.createNewFile(); - - //TODO Delete - System.out.println("Saving track " + String.format("%d (@0x%08X)", track, key) + " to " + wav + String.format(" (%d bytes)", tracklist.get(key))); - - // write selection to file - try ( - FileInputStream inStream = new FileInputStream(packages[i]); - FileOutputStream outStream = new FileOutputStream(wav) - ) { - inStream.skipNBytes(key.intValue()); - byte[] trackBytes = new byte[tracklist.get(key)]; - int readBytes = inStream.read(trackBytes); - outStream.write(trackBytes, 0, readBytes); - } catch (IOException e) { - e.printStackTrace(); - System.exit(3); - } - - } catch (IOException e) { - System.out.println("An error occurred when attempting to write " + name + " to file:\t" + wav); - e.printStackTrace(); - System.exit(3); - } - } - - track++; - - } - - if(compressBGM && packages[i].getName().equals("pack_bgm.bin")) { - - // find WAV files - File[] WAV_Files = dir.listFiles(new FilenameFilter() { - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".wav"); - } - }); - - // convert WAV files to FLAC - FLAC_FileEncoder ffe = new FLAC_FileEncoder(); - - for(File wav : WAV_Files) { - - try { - File flac = new File(dir.getPath() + "/" + FilenameUtils.removeExtension(wav.getName()) + ".flac"); - if(!flac.exists()) - flac.createNewFile(); - - //TODO Delete - System.out.println("Compressing WAV to FLAC:\t" + flac); - - ffe.encode(wav, flac); - } catch (IOException e) { - System.out.println("An error occurred when attempting to write file."); - e.printStackTrace(); - System.exit(3); - } - - } - - System.out.print("Deleting WAV files"); - // delete WAV files - for(File wav : WAV_Files) { - boolean deleted = false; - while(!deleted) { - try { - Files.delete(wav.toPath()); - deleted = true; - } catch(Exception e) { - System.out.print('.'); - } - } - } - - } - - } - - } - - /** - * 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. - * - * @param reader - * @return file - */ - private static File retrievePackage(BufferedReader reader) { - - String prompt = "Please enter the path of a package: " + ANSI_BEIGE; - - File packageFile = null; - boolean valid = false; - do { - - // Retrieve a file or folder - packageFile = retrieveFileOrFolder(reader, prompt); - - if(packageFile.isFile()) { - // File must have .bin extension - String name = packageFile.getName(); - int extIndex = name.lastIndexOf('.'); - String extension = null; - if(extIndex > 0 && extIndex < name.length() - 1) - extension = name.substring(extIndex + 1); - if(extension != null && extension.equalsIgnoreCase("bin")) - valid = true; - } else { // packageFile is folder - ; // Folder should have a .bin file - File[] contents = packageFile.listFiles(); - for(File file : contents) { - String name = file.getName(); - int extIndex = name.lastIndexOf('.'); - String extension = null; - if(extIndex > 0 && extIndex < name.length() - 1) - extension = name.substring(extIndex + 1); - if(extension != null && extension.equalsIgnoreCase("bin")) { - valid = true; - break; - } - } - } - - if(!valid) { - if(packageFile.isFile()) - System.out.println("File does not have a .BIN file extension."); - else System.out.println("Directory does not contain a .BIN file."); - } - - } while(!valid); - - return packageFile; - - } - - - /** * Helper function to prompt user to select a program task. * @@ -779,7 +204,7 @@ public class AudioExtractor { private static String retrieveOperation(BufferedReader reader, String input) { String operation = null; - String[] values = {"identify", "extract", "pack", "patch", "print", "exit"}; + String[] values = {"identify", "extract", "convert", "patch", "pack", "print", "exit"}; if(input != null) for(int i = 0; i < values.length; i++) @@ -836,388 +261,297 @@ public class AudioExtractor { } /** - * Helper function to read RIFF header information. - * - * @param file - * @param offset - * @throws IOException - */ - public static void readRIFFChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tRIFF (Resource Interchange File Format) file"); - - if(offset + 8 < file.length()) { - int riffSize = readChunkSize(file, offset); - System.out.println(formatAddress(offset + 4, file.length()) + ":\t" + riffSize + " Bytes" + (riffSize > 1024 ? " (" + convertBytes(riffSize) + ")" : "")); - } - - if(offset + 12 < file.length()) { - file.seek(offset + 8); - if(file.readInt() == ASCII_WAVE) - System.out.println(formatAddress(offset + 8, file.length()) + ":\tWAVE (Waveform Audio File Format) type"); - offset += 8; - } - - System.out.println(); - - } - - - /** - * Helper method to read format chunk information. - * - * @param file - * @param offset from start of file to start of format chunk - * @return offset from start of file to end of format chunk - * @throws IOException - */ - public static int readFormatChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tfmt (Format) chunk"); - - int size = 8; - if(offset + 8 < file.length()) { - size = readChunkSize(file, (int) offset); - System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); - } - - if(offset + size < file.length()) { - - byte[] LEBytes = new byte[2]; - file.seek(offset + 0x08); - file.read(LEBytes); - int encoding = littleEndianToInt(LEBytes); - System.out.print(formatAddress(offset + 0x08, file.length()) + ":\t"); - if(encoding == 0x0001) - System.out.print("PCM (Pulse-Code Modulation)"); - else if(encoding == 0x0002) - System.out.print("ADPCM (Adaptive Differential Pulse-Code Modulation)"); - else if(encoding == 0x0165) - System.out.print("XMA (Xbox Media Audio)"); - else if(encoding == 0x0166) - System.out.print("XMA2 (Xbox Media Audio 2)"); - else { - boolean found = false; - InputStream filestream = AudioExtractor.class.getResourceAsStream("/encoding.csv"); - InputStreamReader streamReader = new InputStreamReader(filestream); - Reader in = streamReader; - String[] headers = { "Decimal value", "Hexadecimal value", "Description" }; - Iterable records = CSVFormat.EXCEL.builder().setHeader(headers).setSkipHeaderRecord(true).build().parse(in); - for (CSVRecord record : records) { - String dec = record.get("Decimal value"); - String desc = record.get("Description"); - if(encoding == Integer.parseInt(dec)) { - System.out.print(desc); - found = true; - break; - } - } - if(!found) - System.out.print("Unknown (" + encoding + ")"); - } - System.out.println(" encoding"); - - if(encoding == 0x0165) { - LEBytes = new byte[2]; - file.seek(offset + 0x10); - file.read(LEBytes); - int streams = littleEndianToInt(LEBytes); - System.out.println(formatAddress(offset + 0x10, file.length()) + ":\t" + streams + (streams == 1 ? " stream" : " streams")); - parseField(file, offset + 0x12, 1, " loop flag"); - // unknown 5 bytes ? - parseField(file, offset + 0x18, 4, " Hz sampling rate"); - parseField(file, offset + 0x1C, 4, " loop start"); - parseField(file, offset + 0x20, 4, " loop end"); - parseField(file, offset + 0x24, 1, " loop subframe"); - // unknown 3 bytes ? - -// int channels = 0; -// for(int i = 0; i < streams; i++) -// channels += parseField(file, offset + 0x08 + 0x0C + 0x14 * i + 0x11, 1, " channel"); -// System.out.println(channels + " channels"); - } else { - parseField(file, offset + 0x0A, 2, " channels"); - parseField(file, offset + 0x0C, 4, " Hz sampling rate"); - parseField(file, offset + 0x10, 4, " average bytes per second"); - parseField(file, offset + 0x14, 2, "-byte sample block size"); - parseField(file, offset + 0x16, 2, " significant bits per sample"); - LEBytes = new byte[2]; - file.seek(offset + 0x18); - file.read(LEBytes); - int extra = littleEndianToInt(LEBytes); - if(extra != ASCII_smpl) - parseField(file, offset + 0x18, 2, " extra format bytes"); - } - - } - - System.out.println(); - return offset + size; - - } - - private static int readDataChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tdata (Data) chunk"); - - int size = 8; - if(offset + 8 < file.length()) { - size = readChunkSize(file, (int) offset); - System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); - } - - System.out.println(); - return offset + size; - - } - - private static int readSampleChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tsmpl (Sample) chunk"); - - int size = 8; - if(offset + 8 < file.length()) { - size = readChunkSize(file, (int) offset); - System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); - } - - // 0x08 4 bytes manufacturer code - byte[] bytes = new byte[4]; - file.seek(offset + 0x08); - file.read(bytes); - int relevantBytes = bytes[0]; - String code; - if(relevantBytes == 3) - code = String.format("%02XH %02XH %02XH", bytes[1], bytes[2], bytes[3]); - else code = String.format("%02XH", bytes[3]); - System.out.print(formatAddress(offset + 0x08, file.length()) + ":\t"); - boolean found = false; - InputStream filestream = AudioExtractor.class.getResourceAsStream("/manufacturer.csv"); - InputStreamReader streamReader = new InputStreamReader(filestream); - Reader in = streamReader; - String[] headers = { "SysEx ID Number", "Company Name" }; - Iterable records = CSVFormat.EXCEL.builder().setHeader(headers).setSkipHeaderRecord(true).build().parse(in); - for (CSVRecord record : records) { - String ID = record.get("SysEx ID Number"); - String manufacturer = record.get("Company Name"); - if(code.equalsIgnoreCase(ID)) { - System.out.print(manufacturer + " (" + code + ")"); - found = true; - break; - } - } - if(!found) - System.out.print("Unknown (" + code + ")"); - System.out.println(" manufacturer"); - - parseField(file, offset + 0x0C, 4, " product code"); - parseField(file, offset + 0x10, 4, "-nanosecond sample period"); - parseField(file, offset + 0x14, 4, " MIDI unity note"); - parseField(file, offset + 0x18, 4, " MIDI pitch fraction"); - parseField(file, offset + 0x1C, 4, " SMPTE format"); - parseField(file, offset + 0x20, 4, " SMPTE offset"); - int loops = parseField(file, offset + 0x24, 4, " sample loops"); - parseField(file, offset + 0x28, 4, " sample data"); - - for(int i = 0; i < loops; i++) { - int loopOffset = offset + 0x2C + i * 0x18; - parseField(file, loopOffset, 4, " loop ID"); - - bytes = new byte[4]; - file.seek(loopOffset + 0x04); - file.read(bytes); - int loopType = littleEndianToInt(bytes); - System.out.print(formatAddress(loopOffset + 0x04, file.length()) + ":\t"); - String type; - switch(loopType) { - case 0: - type = "Forward"; - break; - case 1: - type = "Alternating"; - break; - case 2: - type = "Backward"; - break; - default: - type = "Unknown"; - break; - } - type += " (" + loopType + ") loop type"; - System.out.println(type); - - file.seek(loopOffset + 0x08); - file.read(bytes); - int loopStart = littleEndianToInt(bytes); - System.out.println(formatAddress(loopOffset + 0x08, file.length()) + ":\tLoop start @ sample " + loopStart); - - file.seek(loopOffset + 0x0C); - file.read(bytes); - int loopEnd = littleEndianToInt(bytes); - System.out.println(formatAddress(loopOffset + 0x08, file.length()) + ":\tLoop end @ sample " + loopEnd); - - parseField(file, loopOffset + 0x10, 4, " loop fraction"); - - file.seek(loopOffset + 0x14); - file.read(bytes); - int repeat = littleEndianToInt(bytes); - if(repeat == 0) - System.out.println(formatAddress(loopOffset + 0x14, file.length()) + ":\tRepeat indefinitely"); - else System.out.println(formatAddress(loopOffset + 0x14, file.length()) + ":\t" + repeat + "repeat count"); - - } - - System.out.println(); - return offset + size; - - } - - public static int readSeekChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tseek (Seek) chunk"); - - int size = 8; - if(offset + 8 < file.length()) { - size = readChunkSize(file, (int) offset); - System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); - } - - return offset + size; - - } - - /** - * Helper method to read a little-endian byte field and print its contents. - * - * @param file - * @param address - * @param size - * @param message - * @return value - * @throws IOException - */ - public static int parseField(RandomAccessFile file, int address, int size, String message) throws IOException { - - byte[] bytes = new byte[size]; - file.seek(address); - file.read(bytes); - int value = littleEndianToInt(bytes); - - System.out.println(formatAddress(address, file.length()) + ":\t" + value + message); - - return value; - - } - - private static int readx2stChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tx2st chunk"); - - int size = 8; - if(offset + 8 < file.length()) { - size = readChunkSize(file, (int) offset); - System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); - } - - System.out.println(); - return offset + size; - - } - - private static int readAlignmentChunk(RandomAccessFile file, int offset) throws IOException { - - System.out.println(formatAddress(offset, file.length()) + ":\tALIG (Alignment) chunk"); - - int size = 8; - if(offset + 8 < file.length()) { - size = readChunkSize(file, (int) offset); - System.out.println(formatAddress(offset + 0x04, file.length()) + ":\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "")); - } - - System.out.println(); - return offset + size; - - } - - private static String formatAddress(long address, long size) { - - int digits = (int) Math.ceil(Math.log(size) / Math.log(16)); - - return String.format("%0" + digits + "X", address); - -// if(size <= 0xFFFF) -// return String.format("%04X", address); -// else return String.format("%08X", address); - - } - - /** - * Formats a file address with leading zeroes. - * - * @param address - * @return padded address - */ - private static String formatAddress(long address) { - -// if(address > 0xFFFF) - return String.format("%08X", address); -// else return String.format("%04X", address); - - } - - - - - /** - * Helper function to prompt user to select a WAV file. + * Prompts the user for input and validates that the input matches one of the provided values. * * @param reader - * @return WAV file + * @param prompt + * @param values + * @return input */ - private static File retrieveWave(BufferedReader reader) { + private static String retrieveInput(BufferedReader reader, String prompt, String[] values) { - String prompt = "Please provide a WAV file: " + ANSI_BEIGE; + String input = null; - File waveFile = 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; + + } + + /** + * Extracts audio tracks from the given package file. + * + * @param packageFile + * @throws FileNotFoundException + */ + public static void extract(File packageFile) { + + System.out.println("Name:\t" + packageFile.getName()); + System.out.println("Size:\t" + packageFile.length() + " (" + Analyzer.convertBytes(packageFile.length()) + ")\n"); + + try { + RandomAccessFile file = new RandomAccessFile(packageFile, "r"); + LinkedHashMap tracklist = Analyzer.findAudioTracks(file); + + File dir = new File(packageFile.getAbsoluteFile().getParentFile().toPath() + "/" + + FilenameUtils.removeExtension(packageFile.getName())); + if(!dir.exists()) + dir.mkdir(); + + System.out.println("Extracting " + tracklist.size() + " tracks..."); + + int digits = (int) Math.ceil(Math.log10(tracklist.size() + 1)); + int extracted = 0; + Set keys = tracklist.keySet(); + for(Integer address : keys) { + int length = tracklist.get(address); + + if(CMD_MODE) + printProgressBar(extracted, keys.size()); + + String extension; + byte[] bytes = new byte[2]; + file.seek(address + 0x14); + file.read(bytes); + int encoding = Analyzer.littleEndianToInt(bytes); + switch(encoding) { + case 0x0165: extension = "xma"; break; + case 0x0001: extension = "wav"; break; + default: extension = "bin"; break; + } + + File trackFile = new File(dir.toPath() + "/track" + String.format("%0" + digits + "d", extracted + 1) + "." + extension); + + try ( + FileInputStream in = new FileInputStream(packageFile); + FileOutputStream out = new FileOutputStream(trackFile); + ) { + in.skipNBytes(address.intValue()); + bytes = new byte[length]; + int readBytes = in.read(bytes); + out.write(bytes, 0, readBytes); + } catch (IOException e) { + e.printStackTrace(); + } + extracted++; + } + + if(CMD_MODE) + printProgressBar(extracted, keys.size()); + + } catch(Exception e) { + e.printStackTrace(); + } + + System.out.println("Finished.\n"); + + } + +// /** +// * Extracts audio files to the extract directory from package files located at the package directory. +// * Also supports FLAC conversion for BGM files. +// * +// * @param packDirectory +// * @param extractDirectory +// * @param compressBGM +// * @deprecated delete after adapting FLAC conversion +// */ +// public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) { +// +// System.out.println("packDirectory:\t" + packDirectory); +// System.out.println("extractDirectory:\t" + extractDirectory); +// +// // Identify target binary files +// File[] packages = new File(packDirectory.toString()).listFiles(new FilenameFilter() { +// public boolean accept(File dir, String name) { +// return name.toLowerCase().endsWith(".bin"); +// } +// }); +// +// // Identify and extract audio within binary files +// for(int i = 0; i < packages.length; i++) { +// +// RandomAccessFile source; +// LinkedHashMap tracklist = null; +// try { +// source = new RandomAccessFile(packages[i], "r"); +// tracklist = Analyzer.findAudioTracks(source); +// } catch (FileNotFoundException e) { +// System.out.println("Binary file:\t" + packages[i] + "\n was not found. File skipped."); +// continue; +// } catch (IOException e) { +// System.out.println("Binary file:\t" + packages[i] + "\n could not be read. File skipped."); +// continue; +// } +// +// System.out.println("packages[" + i + "]:\t" + packages[i]); +// +// File dir = new File(extractDirectory.getPath() + "/" + FilenameUtils.removeExtension(packages[i].getName())); +// if(!dir.exists()) +// dir.mkdir(); +// +// // Extract and write WAV files in directory +// Set keys = tracklist.keySet(); +// int track = 1; +// +// for(Integer key : keys) { +// +// String name = String.format("track%04d", track); +// { // Extract WAV files +// File wav = null; +// try { +// +// // create file for storing WAV data +// wav = new File(dir.getPath() + "/" + name + ".wav"); +// if(!wav.exists()) +// wav.createNewFile(); +// +// System.out.println("Saving track " + String.format("%d (@0x%08X)", track, key) + " to " + wav + String.format(" (%d bytes)", tracklist.get(key))); +// +// // write selection to file +// try ( +// FileInputStream inStream = new FileInputStream(packages[i]); +// FileOutputStream outStream = new FileOutputStream(wav) +// ) { +// inStream.skipNBytes(key.intValue()); +// byte[] trackBytes = new byte[tracklist.get(key)]; +// int readBytes = inStream.read(trackBytes); +// outStream.write(trackBytes, 0, readBytes); +// } catch (IOException e) { +// e.printStackTrace(); +// System.exit(3); +// } +// +// } catch (IOException e) { +// System.out.println("An error occurred when attempting to write " + name + " to file:\t" + wav); +// e.printStackTrace(); +// System.exit(3); +// } +// } +// +// track++; +// +// } +// +// if(compressBGM && packages[i].getName().equals("pack_bgm.bin")) { +// +// // find WAV files +// File[] WAV_Files = dir.listFiles(new FilenameFilter() { +// public boolean accept(File dir, String name) { +// return name.toLowerCase().endsWith(".wav"); +// } +// }); +// +// // convert WAV files to FLAC +// FLAC_FileEncoder ffe = new FLAC_FileEncoder(); +// +// for(File wav : WAV_Files) { +// +// try { +// File flac = new File(dir.getPath() + "/" + FilenameUtils.removeExtension(wav.getName()) + ".flac"); +// if(!flac.exists()) +// flac.createNewFile(); +// +// System.out.println("Compressing WAV to FLAC:\t" + flac); +// +// ffe.encode(wav, flac); +// } catch (IOException e) { +// System.out.println("An error occurred when attempting to write file."); +// e.printStackTrace(); +// System.exit(3); +// } +// +// } +// +// System.out.print("Deleting WAV files"); +// // delete WAV files +// for(File wav : WAV_Files) { +// boolean deleted = false; +// while(!deleted) { +// try { +// Files.delete(wav.toPath()); +// deleted = true; +// } catch(Exception e) { +// System.out.print('.'); +// } +// } +// } +// +// } +// +// } +// +// } + + /** + * Helper method to prompt user to select a package file, or folder containing one or more packages. + * + * @param reader + * @return file + */ + private static File retrievePackage(BufferedReader reader) { + + String prompt = "Please enter the path of a package: " + ANSI_BEIGE; + + File packageFile = null; boolean valid = false; do { - // Retrieve an audio file - waveFile = retrieveFile(reader, prompt, ".wav"); + // Retrieve a file or folder + packageFile = retrieveFileOrFolder(reader, prompt); - 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."); + if(packageFile.isFile()) { + // File must have .bin extension + String name = packageFile.getName(); + int extIndex = name.lastIndexOf('.'); + String extension = null; + if(extIndex > 0 && extIndex < name.length() - 1) + extension = name.substring(extIndex + 1); + if(extension != null && extension.equalsIgnoreCase("bin")) + valid = true; + } else { // packageFile is folder + ; // Folder should have a .bin file + File[] contents = packageFile.listFiles(); + for(File file : contents) { + String name = file.getName(); + int extIndex = name.lastIndexOf('.'); + String extension = null; + if(extIndex > 0 && extIndex < name.length() - 1) + extension = name.substring(extIndex + 1); + if(extension != null && extension.equalsIgnoreCase("bin")) { + valid = true; + break; + } + } + } + + if(!valid) { + if(packageFile.isFile()) + System.out.println("File does not have a .BIN file extension."); + else System.out.println("Directory does not contain a .BIN file."); } } while(!valid); - return waveFile; + return packageFile; } - + /** * Helper function to prompt user to select a file or folder. * @@ -1251,6 +585,114 @@ public class AudioExtractor { } + /** + * Helper function to show current progress. + * Should only be used in CMD Mode. + * + * @param current + * @param total + */ + private 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 + ")"); + } + + /** + * Examines a given file and reads some of its properties. + * + * @param givenFile + */ + public static void identify(File givenFile) { + + String name = givenFile.getName(); + System.out.println("Name:\t" + name); + + long size = givenFile.length(); + System.out.println("Size:\t" + size + " Bytes" + (size > 1024 ? " (" + Analyzer.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 tracklist = Analyzer.findAudioTracks(file); + System.out.println(tracklist.size() + " Tracks (" + Analyzer.convertBytes(size / tracklist.size()) + " avg size)\n"); + System.out.println("Format Info"); + Analyzer.readFormatChunk(file, 0x0C); + break; + } + case "flac": { + + 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 Analyzer.ASCII_RIFF: { + Analyzer.readRIFFChunk(file, (int) i); + break; + } + case Analyzer.ASCII_fmt: { + i = Analyzer.readFormatChunk(file, (int) i) - 4; + break; + } + case Analyzer.ASCII_smpl: { + i = Analyzer.readSampleChunk(file, (int) i) - 4; + break; + } + case Analyzer.ASCII_data: { + i = Analyzer.readDataChunk(file, (int) i) - 4; + break; + } + case Analyzer.ASCII_ALIG: { + i = Analyzer.readAlignmentChunk(file, (int) i) - 4; + break; + } + case Analyzer.ASCII_seek: { + i = Analyzer.readx2stChunk(file, (int) i) - 4; + break; + } + case Analyzer.ASCII_x2st: { + i = Analyzer.readx2stChunk(file, (int) i) - 4; + break; + } + } + } + break; + } + } + } catch (IOException e) { + System.out.println("Could not read file."); + } + + } + /** * Helper function to prompt user to select a file. * @@ -1316,7 +758,312 @@ public class AudioExtractor { return file; } + + /** + * Converts an audio file to the target format. + * + * @param audioFile + * @param format + */ + public static void convert(File audioFile, String format) { + + System.out.println("convert(" + audioFile.toPath() + ", " + format + ") not yet implemented."); + + } + /** + * Helper function to prompt user to select a convertible audio file. + * + * @param reader + * @return audio file + */ + private static File retrieveAudio(BufferedReader reader) { + + File file = null; + + String input = null; + boolean valid = false; + do { + System.out.print("Please enter the path of an audio file:\t" + ANSI_BEIGE); + try { + input = reader.readLine(); + file = new File(input); + if(file.exists()) { + if(file.isFile()) { + + String name = file.getName().toLowerCase(); + String extension = FilenameUtils.getExtension(name); + + if(extension.equals("wav") || extension.equals("xma")) { + // check for RIFF header + byte[] bytes = new byte[4]; + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + raf.seek(0x0); + if(raf.readInt() == Analyzer.ASCII_RIFF) { + raf.seek(0x14); + raf.read(bytes); + int encoding = Analyzer.littleEndianToInt(bytes); + if(encoding == 0x0001) + valid = true; + else if(encoding == 0x0165) + valid = true; + else { + System.out.println("Unsupported encoding" + Analyzer.identifyEncoding(encoding) + "."); + } + } + else System.out.println("Not a valid RIFF file."); + } + + } else if (name.endsWith(".flac")) { + // check for FLAC header + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + raf.seek(0); + if(raf.readInt() == Analyzer.ASCII_fLaC) + valid = true; + else System.out.println("Not a valid FLAC file."); + } + + } else System.out.println(Ansi.colorize("File does not have a supported file extension.")); + + } + } else System.out.println(Ansi.colorize("File does not exist.")); + } catch (IOException e) { + System.out.println(Ansi.colorize("Invalid file.")); + } + } while(!valid); + + return file; + + } + + /** + * Prompts user to select an audio format that can be converted to. + * + * @param reader + * @return + */ + private static String retrieveFormat(BufferedReader reader) { + + String prompt = "\nSupported audio formats: " + + "\n- " + Ansi.colorize("WAV", Attribute.BRIGHT_BLUE_TEXT()) + "\tPCM audio (uncompressed)" + + "\n- " + Ansi.colorize("FLAC", Attribute.TEXT_COLOR(0xFF, 0x68, 0x1F)) + "\tPCM audio (lossless compression)" + + "\n- " + Ansi.colorize("XMA", Attribute.BRIGHT_GREEN_TEXT()) + "\tXbox Media Audio (lossy compression)" + + "\n" + + "\n" + Ansi.colorize("Please select an audio format:") + ANSI_BEIGE + " "; + + String[] values = {"WAV", "XMA", "FLAC"}; + + String input = retrieveInput(reader, prompt, values); + + Attribute color = null; + switch(input.toLowerCase()) { + case "wav": color = Attribute.BRIGHT_BLUE_TEXT(); break; + case "flac": color = Attribute.TEXT_COLOR(0xFF, 0x68, 0x1F); break; + case "xma": color = Attribute.BRIGHT_GREEN_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 input; + + } + + /** + * 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 + */ + public 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 == Analyzer.ASCII_data) + dataAddress = i; + // match "smpl" chunk header + else if(scan == Analyzer.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 = Analyzer.readChunkSize(file, dataAddress); + else throw new IOException("Data chunk not in file."); + + // Determine size of smpl chunk + int smplSize; + if(smplAddress > -1) + smplSize = Analyzer.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. + * + * @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() == Analyzer.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; + + } + + /** + * Packs WAV files located at the extract directory into BIN files at the package directory. + * + * @param packDirectory + * @param extractDirectory + */ + public 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. + + } + + /** + * Prints a RR6 ASCII style logo with the given color and message. + * + * @param color + * @param message + */ + public static void print(Attribute color, String message) { + + System.out.println(Ansi.colorize(Logo.format(message), color)); + + } + /** * Helper function to prompt user to select a color. * @@ -1411,1027 +1158,6 @@ public class AudioExtractor { 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); -// System.out.println("DEBUG: " + (buffer[0] & 0xFF) + " " + (buffer[1] & 0xFF) + " " + (buffer[2] & 0xFF) + " " + (buffer[3] & 0xFF) + ""); - return 8 // size offset - + (buffer[0] & 0xFF) // 256^0 - + (buffer[1] & 0xFF) * 256 // 256^1 - + (buffer[2] & 0xFF) * 65_536 // 256^2 - + (buffer[3] & 0xFF) * 16_777_216; // 256^3 - - } - - /** - * Helper method to convert a number of bytes to a more readable form. - * - * @param bytes - * @return legible form - */ - private static String convertBytes(long bytes) { - - final String[] units = new String[] {"B", "KB", "MB", "GB", "TB"}; - int i = (int) (Math.log10(bytes) / Math.log10(1024)); - double value = bytes / Math.pow(1024, i); - return String.format("%.2f %s", value, units[i]); - - } - - /** - * Creates a map of audio tracks within provided file. - * - * @param file - * @return Tracklist containing address-length pairs - * @throws IOException - */ - private static LinkedHashMap findAudioTracks(RandomAccessFile file) throws IOException { - - LinkedHashMap tracklist = new LinkedHashMap(); - - // Files start on addresses divisible by 0x800 - for(int a = 0; a + 0x800 < file.length(); a += 0x800) { - - file.seek(a); - - // File header starts with "RIFF" sequence - if(file.readInt() == ASCII_RIFF) { - - // Next 4 bytes specify file length beyond first 8 bytes, in little-endian representation - int length = 8; - // Calculate remaining length of file - for(int d = 0; d < 4; d++) { - file.seek(a + 4 + d); - length += file.readUnsignedByte() * ((int) Math.pow(16, 2 * d)); - } - - // Record file position in map - tracklist.put(a, length); - } - - } - - return tracklist; - - } - - /** - * Creates an ASCII logo in RR6 style using the provided text. - * Supports alphanumeric text with spaces only. - * - * @param text - * @param italicize logo - * @return logo formatted text - */ - 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. - * Supports alphanumeric text with spaces only. - * - * @param text - * @param height - * @param italicize logo - * @return logo formatted text - */ - private static String logoFormat(String text, boolean italic, int height) { - - // Initialize logo lines - String[] logo = new String[10]; - for(int i = 0; i < logo.length; i++) - logo[i] = new String(); - - boolean spaced = false; - for(int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - - // Add space from previous character - if(i > 0) { - if(c != ' ' && c!= ':' && !spaced) { - for(int line = 0; line < logo.length - 1; line++) - logo[line] += " "; - logo[9] += "███"; - } - spaced = false; - } - - // Add character to logo - switch(c) { - - // Space - case ':': - case ' ': - for(int line = 0; line < logo.length; line++) - logo[line] += " "; - spaced = true; - break; - - // Numbers - case '0': - if(logo[0].length() > 0) - for(int x = 0; x < logo.length; x++) - logo[x] = logo[x].substring(0, logo[x].length() - 1); - logo[0] += " ██████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ ████████ "; - logo[3] += "████████ ████████ "; - logo[4] += "████████ ████████ "; - logo[5] += "████████ ████████ "; - logo[6] += "████████ ████████ "; - logo[7] += "████████ ████████ "; - logo[8] += "████████████████████████ "; - logo[9] += " ██████████████████████ "; - spaced = true; - break; - case '1': - logo[0] += " ███████████████ "; - logo[1] += "████████████████ "; - logo[2] += " ████████ "; - logo[3] += " ████████ "; - logo[4] += " ████████ "; - logo[5] += " ████████ "; - logo[6] += " ████████ "; - logo[7] += " ████████ "; - logo[8] += " ████████ "; - logo[9] += " ████████ "; - spaced = true; - break; - case '2': - logo[0] += " ███████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ ████████ "; - logo[3] += " ████████ "; - logo[4] += " ██████████████████████ "; - logo[5] += "████████████████████████ "; - logo[6] += "████████ "; - logo[7] += "████████ "; - logo[8] += "████████████████████████ "; - logo[9] += "███████████████████████ "; - spaced = true; - break; - case '3': - logo[0] += " ██████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ ████████ "; - logo[3] += " ████████ "; - logo[4] += " ███████████████ "; - logo[5] += " ██████████████ "; - logo[6] += " ████████ "; - logo[7] += "████████ ████████ "; - logo[8] += "████████████████████████ "; - logo[9] += " ██████████████████████ "; - spaced = true; - break; - case '4': - logo[0] += " █████████████ "; - logo[1] += " ███████████████ "; - logo[2] += " ████ ██████ "; - logo[3] += " ██████ ██████ "; - logo[4] += " ███████████████████████ "; - logo[5] += "████████████████████████ "; - logo[6] += " ██████ "; - logo[7] += " ██████ "; - logo[8] += " ██████ "; - logo[9] += " ██████ "; - spaced = true; - break; - case '5': - logo[0] += " ███████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ "; - logo[3] += "████████ "; - logo[4] += "████████████████████████ "; - logo[5] += "████████████████████████ "; - logo[6] += " ████████ "; - logo[7] += " ████████ "; - logo[8] += "████████████████████████ "; - logo[9] += "███████████████████████ "; - spaced = true; - break; - case '6': - logo[0] += " ███████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ "; - logo[3] += "████████ "; - logo[4] += "████████████████████████ "; - logo[5] += "████████████████████████ "; - logo[6] += "████████ ████████ "; - logo[7] += "████████ ████████ "; - logo[8] += "████████████████████████ "; - logo[9] += "███████████████████████ "; - spaced = true; - break; - case '7': - logo[0] += " ███████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ ████████ "; - logo[3] += " ████████ "; - logo[4] += " ████████ "; - logo[5] += " ████████ "; - logo[6] += " ████████ "; - logo[7] += " ████████ "; - logo[8] += " ████████ "; - logo[9] += " ████████ "; - spaced = true; - break; - case '8': - logo[0] += " ██████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ ████████ "; - logo[3] += "████████ ████████ "; - logo[4] += " █████████████████████ "; - logo[5] += " █████████████████████ "; - logo[6] += "████████ ████████ "; - logo[7] += "████████ ████████ "; - logo[8] += "████████████████████████ "; - logo[9] += " ██████████████████████ "; - spaced = true; - break; - case '9': - logo[0] += " ██████████████████████ "; - logo[1] += "████████████████████████ "; - logo[2] += "████████ ████████ "; - logo[3] += "████████ ████████ "; - logo[4] += " ███████████████████████ "; - logo[5] += " █████████████████████ "; - logo[6] += " ████████ "; - logo[7] += " ████████ "; - logo[8] += " ████████ "; - logo[9] += " ████████ "; - spaced = true; - break; - - // Capital letters - case 'A': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████████████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'B': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "███████████ "; - logo[6] += "████ ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'C': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ "; - logo[4] += "████ "; - logo[5] += "████ "; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'D': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'E': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ "; - logo[4] += "████ "; - logo[5] += "████████████"; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'F': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ "; - logo[4] += "████ "; - logo[5] += "████████████"; - logo[6] += "████ "; - logo[7] += "████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'G': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ "; - logo[4] += "████ "; - logo[5] += "████ ██████"; - logo[6] += "████ ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'H': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████████████"; - logo[6] += "████ ████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'I': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████"; - logo[3] += "████"; - logo[4] += "████"; - logo[5] += "████"; - logo[6] += "████"; - logo[7] += "████"; - logo[8] += " "; - logo[9] += "████"; - break; - case 'J': - logo[0] += " "; - logo[1] += " "; - logo[2] += " ██████"; - logo[3] += " ████"; - logo[4] += " ████"; - logo[5] += " ████"; - logo[6] += " ████"; - logo[7] += "████████"; - logo[8] += " "; - logo[9] += "████████"; - break; - case 'K': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████ "; - logo[3] += "████ ████ "; - logo[4] += "████ ████ "; - logo[5] += "████ ████ "; - logo[6] += "████████ "; - logo[7] += "████ █████ "; - logo[8] += " █████"; - logo[9] += "███████ ███"; - spaced = true; - break; - case 'L': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ "; - logo[3] += "████ "; - logo[4] += "████ "; - logo[5] += "████ "; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'M': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████"; - logo[3] += "██████ ██████"; - logo[4] += "████ ████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "██████████████"; - break; - case 'N': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████ "; - logo[3] += "██████ ████ "; - logo[4] += "███████ ████ "; - logo[5] += "████ ███████ "; - logo[6] += "████ ██████ "; - logo[7] += "████ ████ "; - logo[8] += " ████ "; - logo[9] += "█████████ ███"; - spaced = true; - break; - case 'O': - if(logo[0].length() > 0) - for(int x = 0; x < logo.length; x++) - logo[x] = logo[x].substring(0, logo[x].length() - 1); - logo[0] += " "; - logo[1] += " "; - logo[2] += " ██████████ "; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += " ██████████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'P': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████████████"; - logo[7] += "████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'Q': - logo[0] += " "; - logo[1] += " "; - logo[2] += " ███████████ "; - logo[3] += "████ ████ "; - logo[4] += "████ ████ "; - logo[5] += "████ ████ "; - logo[6] += "████ ████ "; - logo[7] += "████████████ "; - logo[8] += " ████ "; - logo[9] += "█████████ ███"; - spaced = true; - break; - case 'R': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████ "; - logo[3] += "████ ████ "; - logo[4] += "████ ████ "; - logo[5] += "████ ████ "; - logo[6] += "███████████ "; - logo[7] += "█████ █████ "; - logo[8] += " █████"; - logo[9] += "█████████ ███"; - spaced = true; - break; - case 'S': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += "████ ████"; - logo[4] += " ████ "; - logo[5] += " ████ "; - logo[6] += "████ ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'T': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += " ████ "; - logo[4] += " ████ "; - logo[5] += " ████ "; - logo[6] += " ████ "; - logo[7] += " ████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'U': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'V': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += " ████████ "; - logo[7] += " ████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'W': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████"; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████ ████"; - logo[6] += "██████ ██████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "██████████████"; - break; - case 'X': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████ "; - logo[3] += "████ ████ "; - logo[4] += " ████ ████ "; - logo[5] += " ████ "; - logo[6] += " ████ ████ "; - logo[7] += "████ ████ "; - logo[8] += " █████"; - logo[9] += "█████████ ███"; - spaced = true; - break; - case 'Y': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████ ████"; - logo[3] += "████ ████"; - logo[4] += " ████ ████ "; - logo[5] += " ████ "; - logo[6] += " ████ "; - logo[7] += " ████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'Z': - logo[0] += " "; - logo[1] += " "; - logo[2] += "████████████"; - logo[3] += " ████"; - logo[4] += " ████ "; - logo[5] += " ████ "; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - - // Lowercase letters - case 'a': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ███████████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████████████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'b': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += "████ ████"; - logo[5] += "██████████ "; - logo[6] += "████ ████"; - logo[7] += "███████████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'c': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ███████████"; - logo[4] += "████ "; - logo[5] += "████ "; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'd': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += "███████████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'e': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ███████████"; - logo[4] += "████ "; - logo[5] += "████████████"; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'f': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ███████████"; - logo[4] += "████ "; - logo[5] += "████████████"; - logo[6] += "████ "; - logo[7] += "████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'g': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ███████████"; - logo[4] += "████ "; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += "███████████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'h': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████████████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'i': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████"; - logo[4] += "████"; - logo[5] += "████"; - logo[6] += "████"; - logo[7] += "████"; - logo[8] += " "; - logo[9] += "████"; - break; - case 'j': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ██████"; - logo[4] += " ████"; - logo[5] += " ████"; - logo[6] += " ████"; - logo[7] += "████████"; - logo[8] += " "; - logo[9] += "████████"; - break; - case 'k': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += "████ ████ "; - logo[5] += "████████ "; - logo[6] += "████ ████ "; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'l': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ "; - logo[4] += "████ "; - logo[5] += "████ "; - logo[6] += "████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'm': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ █████"; - logo[4] += "█████ ██████"; - logo[5] += "████ ██ ████"; - logo[6] += "████ ████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'n': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += "█████ ████"; - logo[5] += "████ ██ ████"; - logo[6] += "████ █████"; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'o': - if(logo[0].length() > 0) - for(int x = 0; x < logo.length; x++) - logo[x] = logo[x].substring(0, logo[x].length() - 1); - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ██████████ "; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += " ██████████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'p': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "██████████ "; - logo[7] += "████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'q': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += " ███████████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += " ██████████"; - logo[7] += " ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'r': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "██████████ "; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 's': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += "████ "; - logo[5] += " ████ "; - logo[6] += " ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 't': - if(logo[0].length() > 0) - for(int x = 0; x < logo.length; x++) - logo[x] = logo[x].substring(0, logo[x].length() - 1); - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += " ████ "; - logo[5] += " ████ "; - logo[6] += " ████ "; - logo[7] += " ████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'u': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += "████ ████"; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'v': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += "████ ████"; - logo[5] += "████ ████"; - logo[6] += " ████ ████"; - logo[7] += " █████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'w': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "███ ███ ███"; - logo[4] += "███ ███ ███"; - logo[5] += "███ ███ ███"; - logo[6] += " ██ ████ ███"; - logo[7] += " ████ ████ "; - logo[8] += " "; - logo[9] += "███████████████"; - break; - case 'x': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += " ████ ████ "; - logo[5] += " ████ "; - logo[6] += " ████ ████ "; - logo[7] += "████ ████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'y': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████ ████"; - logo[4] += " ████ ████ "; - logo[5] += " ████ "; - logo[6] += " ████ "; - logo[7] += " ████ "; - logo[8] += " "; - logo[9] += "████████████"; - break; - case 'z': - logo[0] += " "; - logo[1] += " "; - logo[2] += " "; - logo[3] += "████████████"; - logo[4] += " ████ "; - logo[5] += " ████ "; - logo[6] += " ████ "; - logo[7] += "████████████"; - logo[8] += " "; - logo[9] += "████████████"; - break; - default: - System.out.println("Unable to convert character '" + c + "'"); - break; - } - } - - // Italicize logo - if(italic) { - for(int i = 0; i < logo.length; i++) { - // i = 0 -> 9 - for(int x = logo.length - (i + 1); x > 0; x--) - // x = 10 - (i + 1) - logo[i] = " " + logo[i]; // line 0 gets 10 spaces, line 1 gets 9 spaces... line 9 gets 0 spaces - } - } - - // Compile logo into single text element - StringBuilder logoText = new StringBuilder(); - for(int i = logo.length - height; i < logo.length; i++) { - logoText.append(logo[i] + "\n"); - } - - return logoText.toString(); - - } - - /** - * Helper method that converts a little-endian byte array into a long value. - * - * @param bytes (up to 8 bytes) - * @return value - */ - public static long littleEndianToLong(byte[] bytes) { - if(bytes.length > 8) - throw new IllegalArgumentException("Byte array must be 8 bytes or shorter."); - long value = 0; - for(int i = 0; i < bytes.length; i++) - value |= (long) (bytes[i] & 0xFF) << (8 * i); - return value; - } - - /** - * Helper method that converts a little-endian byte array into a long value. - * - * @param bytes (up to 8 bytes) - * @return value - */ - public static int littleEndianToInt(byte[] bytes) { - if(bytes.length > 8) - throw new IllegalArgumentException("Byte array must be 4 bytes or shorter."); - int value = 0; - for(int i = 0; i < bytes.length; i++) - value |= (int) (bytes[i] & 0xFF) << (8 * i); - return value; - } - /** * ANSI code to go up one line, followed by a carriage return. @@ -2444,51 +1170,5 @@ public class AudioExtractor { */ 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; - } diff --git a/src/goblincave/gitea/nes/Logo.java b/src/goblincave/gitea/nes/Logo.java new file mode 100644 index 0000000..72b2a3d --- /dev/null +++ b/src/goblincave/gitea/nes/Logo.java @@ -0,0 +1,871 @@ +package goblincave.gitea.nes; + +public class Logo { + + /** + * Creates an ASCII logo in RR6 style using the provided text. + * Supports alphanumeric text with spaces only. + * + * @param text + * @return logo formatted text + */ + public static String format(String text) { + return format(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 + */ + public static String format(String string, boolean italic) { + return format(string, italic, 10); + } + + /** + * Creates an ASCII logo in RR6 style using the provided text. + * Supports alphanumeric text with spaces only. + * + * @param text + * @param height + * @param italicize logo + * @return logo formatted text + */ + public static String format(String text, boolean italic, int height) { + + // Initialize logo lines + String[] logo = new String[10]; + for(int i = 0; i < logo.length; i++) + logo[i] = new String(); + + boolean spaced = false; + for(int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + + // Add space from previous character + if(i > 0) { + if(c != ' ' && c!= ':' && !spaced) { + for(int line = 0; line < logo.length - 1; line++) + logo[line] += " "; + logo[9] += "███"; + } + spaced = false; + } + + // Add character to logo + switch(c) { + + // Space + case ':': + case ' ': + for(int line = 0; line < logo.length; line++) + logo[line] += " "; + spaced = true; + break; + + // Numbers + case '0': + if(logo[0].length() > 0) + for(int x = 0; x < logo.length; x++) + logo[x] = logo[x].substring(0, logo[x].length() - 1); + logo[0] += " ██████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ ████████ "; + logo[3] += "████████ ████████ "; + logo[4] += "████████ ████████ "; + logo[5] += "████████ ████████ "; + logo[6] += "████████ ████████ "; + logo[7] += "████████ ████████ "; + logo[8] += "████████████████████████ "; + logo[9] += " ██████████████████████ "; + spaced = true; + break; + case '1': + logo[0] += " ███████████████ "; + logo[1] += "████████████████ "; + logo[2] += " ████████ "; + logo[3] += " ████████ "; + logo[4] += " ████████ "; + logo[5] += " ████████ "; + logo[6] += " ████████ "; + logo[7] += " ████████ "; + logo[8] += " ████████ "; + logo[9] += " ████████ "; + spaced = true; + break; + case '2': + logo[0] += " ███████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ ████████ "; + logo[3] += " ████████ "; + logo[4] += " ██████████████████████ "; + logo[5] += "████████████████████████ "; + logo[6] += "████████ "; + logo[7] += "████████ "; + logo[8] += "████████████████████████ "; + logo[9] += "███████████████████████ "; + spaced = true; + break; + case '3': + logo[0] += " ██████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ ████████ "; + logo[3] += " ████████ "; + logo[4] += " ███████████████ "; + logo[5] += " ██████████████ "; + logo[6] += " ████████ "; + logo[7] += "████████ ████████ "; + logo[8] += "████████████████████████ "; + logo[9] += " ██████████████████████ "; + spaced = true; + break; + case '4': + logo[0] += " █████████████ "; + logo[1] += " ███████████████ "; + logo[2] += " ████ ██████ "; + logo[3] += " ██████ ██████ "; + logo[4] += " ███████████████████████ "; + logo[5] += "████████████████████████ "; + logo[6] += " ██████ "; + logo[7] += " ██████ "; + logo[8] += " ██████ "; + logo[9] += " ██████ "; + spaced = true; + break; + case '5': + logo[0] += " ███████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ "; + logo[3] += "████████ "; + logo[4] += "████████████████████████ "; + logo[5] += "████████████████████████ "; + logo[6] += " ████████ "; + logo[7] += " ████████ "; + logo[8] += "████████████████████████ "; + logo[9] += "███████████████████████ "; + spaced = true; + break; + case '6': + logo[0] += " ███████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ "; + logo[3] += "████████ "; + logo[4] += "████████████████████████ "; + logo[5] += "████████████████████████ "; + logo[6] += "████████ ████████ "; + logo[7] += "████████ ████████ "; + logo[8] += "████████████████████████ "; + logo[9] += "███████████████████████ "; + spaced = true; + break; + case '7': + logo[0] += " ███████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ ████████ "; + logo[3] += " ████████ "; + logo[4] += " ████████ "; + logo[5] += " ████████ "; + logo[6] += " ████████ "; + logo[7] += " ████████ "; + logo[8] += " ████████ "; + logo[9] += " ████████ "; + spaced = true; + break; + case '8': + logo[0] += " ██████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ ████████ "; + logo[3] += "████████ ████████ "; + logo[4] += " █████████████████████ "; + logo[5] += " █████████████████████ "; + logo[6] += "████████ ████████ "; + logo[7] += "████████ ████████ "; + logo[8] += "████████████████████████ "; + logo[9] += " ██████████████████████ "; + spaced = true; + break; + case '9': + logo[0] += " ██████████████████████ "; + logo[1] += "████████████████████████ "; + logo[2] += "████████ ████████ "; + logo[3] += "████████ ████████ "; + logo[4] += " ███████████████████████ "; + logo[5] += " █████████████████████ "; + logo[6] += " ████████ "; + logo[7] += " ████████ "; + logo[8] += " ████████ "; + logo[9] += " ████████ "; + spaced = true; + break; + + // Capital letters + case 'A': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████████████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'B': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "███████████ "; + logo[6] += "████ ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'C': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ "; + logo[4] += "████ "; + logo[5] += "████ "; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'D': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'E': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ "; + logo[4] += "████ "; + logo[5] += "████████████"; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'F': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ "; + logo[4] += "████ "; + logo[5] += "████████████"; + logo[6] += "████ "; + logo[7] += "████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'G': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ "; + logo[4] += "████ "; + logo[5] += "████ ██████"; + logo[6] += "████ ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'H': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████████████"; + logo[6] += "████ ████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'I': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████"; + logo[3] += "████"; + logo[4] += "████"; + logo[5] += "████"; + logo[6] += "████"; + logo[7] += "████"; + logo[8] += " "; + logo[9] += "████"; + break; + case 'J': + logo[0] += " "; + logo[1] += " "; + logo[2] += " ██████"; + logo[3] += " ████"; + logo[4] += " ████"; + logo[5] += " ████"; + logo[6] += " ████"; + logo[7] += "████████"; + logo[8] += " "; + logo[9] += "████████"; + break; + case 'K': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████ "; + logo[3] += "████ ████ "; + logo[4] += "████ ████ "; + logo[5] += "████ ████ "; + logo[6] += "████████ "; + logo[7] += "████ █████ "; + logo[8] += " █████"; + logo[9] += "███████ ███"; + spaced = true; + break; + case 'L': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ "; + logo[3] += "████ "; + logo[4] += "████ "; + logo[5] += "████ "; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'M': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████"; + logo[3] += "██████ ██████"; + logo[4] += "████ ████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "██████████████"; + break; + case 'N': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████ "; + logo[3] += "██████ ████ "; + logo[4] += "███████ ████ "; + logo[5] += "████ ███████ "; + logo[6] += "████ ██████ "; + logo[7] += "████ ████ "; + logo[8] += " ████ "; + logo[9] += "█████████ ███"; + spaced = true; + break; + case 'O': + if(logo[0].length() > 0) + for(int x = 0; x < logo.length; x++) + logo[x] = logo[x].substring(0, logo[x].length() - 1); + logo[0] += " "; + logo[1] += " "; + logo[2] += " ██████████ "; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += " ██████████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'P': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████████████"; + logo[7] += "████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'Q': + logo[0] += " "; + logo[1] += " "; + logo[2] += " ███████████ "; + logo[3] += "████ ████ "; + logo[4] += "████ ████ "; + logo[5] += "████ ████ "; + logo[6] += "████ ████ "; + logo[7] += "████████████ "; + logo[8] += " ████ "; + logo[9] += "█████████ ███"; + spaced = true; + break; + case 'R': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████ "; + logo[3] += "████ ████ "; + logo[4] += "████ ████ "; + logo[5] += "████ ████ "; + logo[6] += "███████████ "; + logo[7] += "█████ █████ "; + logo[8] += " █████"; + logo[9] += "█████████ ███"; + spaced = true; + break; + case 'S': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += "████ ████"; + logo[4] += " ████ "; + logo[5] += " ████ "; + logo[6] += "████ ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'T': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += " ████ "; + logo[4] += " ████ "; + logo[5] += " ████ "; + logo[6] += " ████ "; + logo[7] += " ████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'U': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'V': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += " ████████ "; + logo[7] += " ████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'W': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████"; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████ ████"; + logo[6] += "██████ ██████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "██████████████"; + break; + case 'X': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████ "; + logo[3] += "████ ████ "; + logo[4] += " ████ ████ "; + logo[5] += " ████ "; + logo[6] += " ████ ████ "; + logo[7] += "████ ████ "; + logo[8] += " █████"; + logo[9] += "█████████ ███"; + spaced = true; + break; + case 'Y': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████ ████"; + logo[3] += "████ ████"; + logo[4] += " ████ ████ "; + logo[5] += " ████ "; + logo[6] += " ████ "; + logo[7] += " ████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'Z': + logo[0] += " "; + logo[1] += " "; + logo[2] += "████████████"; + logo[3] += " ████"; + logo[4] += " ████ "; + logo[5] += " ████ "; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + + // Lowercase letters + case 'a': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ███████████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████████████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'b': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += "████ ████"; + logo[5] += "██████████ "; + logo[6] += "████ ████"; + logo[7] += "███████████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'c': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ███████████"; + logo[4] += "████ "; + logo[5] += "████ "; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'd': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += "███████████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'e': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ███████████"; + logo[4] += "████ "; + logo[5] += "████████████"; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'f': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ███████████"; + logo[4] += "████ "; + logo[5] += "████████████"; + logo[6] += "████ "; + logo[7] += "████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'g': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ███████████"; + logo[4] += "████ "; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += "███████████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'h': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████████████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'i': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████"; + logo[4] += "████"; + logo[5] += "████"; + logo[6] += "████"; + logo[7] += "████"; + logo[8] += " "; + logo[9] += "████"; + break; + case 'j': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ██████"; + logo[4] += " ████"; + logo[5] += " ████"; + logo[6] += " ████"; + logo[7] += "████████"; + logo[8] += " "; + logo[9] += "████████"; + break; + case 'k': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += "████ ████ "; + logo[5] += "████████ "; + logo[6] += "████ ████ "; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'l': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ "; + logo[4] += "████ "; + logo[5] += "████ "; + logo[6] += "████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'm': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ █████"; + logo[4] += "█████ ██████"; + logo[5] += "████ ██ ████"; + logo[6] += "████ ████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'n': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += "█████ ████"; + logo[5] += "████ ██ ████"; + logo[6] += "████ █████"; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'o': + if(logo[0].length() > 0) + for(int x = 0; x < logo.length; x++) + logo[x] = logo[x].substring(0, logo[x].length() - 1); + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ██████████ "; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += " ██████████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'p': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "██████████ "; + logo[7] += "████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'q': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += " ███████████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += " ██████████"; + logo[7] += " ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'r': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "██████████ "; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 's': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += "████ "; + logo[5] += " ████ "; + logo[6] += " ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 't': + if(logo[0].length() > 0) + for(int x = 0; x < logo.length; x++) + logo[x] = logo[x].substring(0, logo[x].length() - 1); + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += " ████ "; + logo[5] += " ████ "; + logo[6] += " ████ "; + logo[7] += " ████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'u': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += "████ ████"; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'v': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += "████ ████"; + logo[5] += "████ ████"; + logo[6] += " ████ ████"; + logo[7] += " █████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'w': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "███ ███ ███"; + logo[4] += "███ ███ ███"; + logo[5] += "███ ███ ███"; + logo[6] += " ██ ████ ███"; + logo[7] += " ████ ████ "; + logo[8] += " "; + logo[9] += "███████████████"; + break; + case 'x': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += " ████ ████ "; + logo[5] += " ████ "; + logo[6] += " ████ ████ "; + logo[7] += "████ ████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'y': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████ ████"; + logo[4] += " ████ ████ "; + logo[5] += " ████ "; + logo[6] += " ████ "; + logo[7] += " ████ "; + logo[8] += " "; + logo[9] += "████████████"; + break; + case 'z': + logo[0] += " "; + logo[1] += " "; + logo[2] += " "; + logo[3] += "████████████"; + logo[4] += " ████ "; + logo[5] += " ████ "; + logo[6] += " ████ "; + logo[7] += "████████████"; + logo[8] += " "; + logo[9] += "████████████"; + break; + default: + System.out.println("Unable to convert character '" + c + "'"); + break; + } + } + + // Italicize logo + if(italic) { + for(int i = 0; i < logo.length; i++) { + // i = 0 -> 9 + for(int x = logo.length - (i + 1); x > 0; x--) + // x = 10 - (i + 1) + logo[i] = " " + logo[i]; // line 0 gets 10 spaces, line 1 gets 9 spaces... line 9 gets 0 spaces + } + } + + // Compile logo into single text element + StringBuilder logoText = new StringBuilder(); + for(int i = logo.length - height; i < logo.length; i++) { + logoText.append(logo[i] + "\n"); + } + + return logoText.toString(); + + } + +}