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