diff --git a/RR6AudioExtractor.md b/RR6AudioExtractor.md new file mode 100644 index 0000000..d1b4581 --- /dev/null +++ b/RR6AudioExtractor.md @@ -0,0 +1,20 @@ +Command Line Functions: +- [ ] Identify +- [ ] Extract +- [ ] Convert +- [ ] Patch +- [ ] Pack +- [ ] Print + +Interface Functions: +- [x] Identify +- [x] Extract +- [ ] Convert + +- [ ] Patch + - Incorporate x2st parameters +- [ ] Pack + - Map audio files in directory + - Create new pack file + - Write audio files into pack file with padding +- [x] Print diff --git a/src/goblincave/gitea/nes/Analyzer.java b/src/goblincave/gitea/nes/Analyzer.java index 2f5ac8c..22450b5 100644 --- a/src/goblincave/gitea/nes/Analyzer.java +++ b/src/goblincave/gitea/nes/Analyzer.java @@ -6,6 +6,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.io.Reader; +import java.nio.ByteBuffer; import java.util.LinkedHashMap; import org.apache.commons.csv.CSVFormat; @@ -58,6 +59,7 @@ public class Analyzer { * Indicates the start of an FLAC file. */ final static int ASCII_fLaC = 0x664C_6143; + final static int ASCII_LIST = 0x4C49_5354; /** * Creates a map of audio tracks within provided file. @@ -289,9 +291,9 @@ public class Analyzer { 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"); +// int extra = littleEndianToInt(LEBytes); +// if(extra != ASCII_smpl) +// parseField(file, offset + 0x18, 2, " extra format bytes"); } } @@ -467,6 +469,29 @@ public class Analyzer { } + /** + * Helper function to read the size of an alignment chunk. + * + * @param file + * @param offset + * @return chunk end address + * @throws IOException + */ + static int readListChunk(RandomAccessFile file, int offset) throws IOException { + + System.out.println(formatAddress(offset, file.length()) + ":\tLIST (List) 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. * @@ -547,4 +572,27 @@ public class Analyzer { return value; } + /** + * Helper method that converts an int value into a little-endian byte array. + * + * @param int value + * @return bytes (up to 8 bytes) + */ + static byte[] intToLittleEndian(int value) { + byte[] bytes = new byte[4]; + for(int i = 0; i < bytes.length; i++) + bytes[i] = (byte) ((value >> 8 * i) & 0xFF); + return bytes; + } + + /** + * Helper method to convert a byte array into an integer. + * + * @param bytes + * @return value + */ + static int bytesToInt(byte[] bytes) { + return ByteBuffer.wrap(bytes).getInt(); + } + } diff --git a/src/goblincave/gitea/nes/AudioExtractor.java b/src/goblincave/gitea/nes/AudioExtractor.java index bdf84a3..f750c7f 100644 --- a/src/goblincave/gitea/nes/AudioExtractor.java +++ b/src/goblincave/gitea/nes/AudioExtractor.java @@ -31,6 +31,22 @@ public class AudioExtractor { */ private final static boolean CMD_MODE = true; + public static void test(String[] args) { + + int maxVal = Integer.MAX_VALUE; + System.out.println(maxVal); + + byte[] bytes = Analyzer.intToLittleEndian(maxVal); + System.out.printf("0x%02x%02x%02x%02x\n", bytes[0], bytes[1], bytes[2], bytes[3]); + + maxVal = Analyzer.littleEndianToInt(bytes); + System.out.println(maxVal); + + bytes = Analyzer.intToLittleEndian(maxVal); + System.out.printf("0x%02x%02x%02x%02x\n", bytes[0], bytes[1], bytes[2], bytes[3]); + + } + /** * 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.
@@ -682,6 +698,10 @@ public class AudioExtractor { i = Analyzer.readx2stChunk(file, (int) i) - 4; break; } + case Analyzer.ASCII_LIST: { + i = Analyzer.readListChunk(file, (int) i) - 4; + break; + } } } break; @@ -881,75 +901,147 @@ public class AudioExtractor { try { RandomAccessFile file = new RandomAccessFile(waveFile, "r"); - // Also Find data location + // Must find 3 chunks to successfully patch WAV file: + int fmt_Address = -1; int dataAddress = -1; - // Check if sample chunk already exists int smplAddress = -1; + int fmt_Size = -1; + int dataSize = -1; + int smplSize = -1; + + int sampleWidth = -1; + + boolean[] found = {false, false, false}; 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; + // System.out.println(i + "\t" + scan); - // if both chunks already located, stop seeking - if(smplAddress > -1 && dataAddress > -1) + // Skip to end of chunk when found + if(scan == Analyzer.ASCII_fmt) { + System.out.println("format chunk found"); + found[0] = true; + fmt_Address = i; + fmt_Size = Analyzer.readChunkSize(file, i); + + // get sample width + byte[] bytes = new byte[2]; + file.seek(i + 0x14); + file.read(bytes); + sampleWidth = Analyzer.littleEndianToInt(bytes); + + i += fmt_Size - 4; + System.out.println("Skipping to " + i); + + } else if(scan == Analyzer.ASCII_data) { + System.out.println("data chunk was found"); + found[1] = true; + dataAddress = i; + dataSize = Analyzer.readChunkSize(file, i); + i += dataSize - 4; + + } else if(scan == Analyzer.ASCII_smpl) { + + found[2] = true; + smplAddress = i; + smplSize = Analyzer.readChunkSize(file, i); + i += smplSize - 4; + + } + + // if all chunks already located, stop seeking + if(found[0] && found[1] && found[2]) 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."); + System.out.println("chunks searched"); - // Determine size of smpl chunk - int smplSize; - if(smplAddress > -1) - smplSize = Analyzer.readChunkSize(file, smplAddress); - // smpl chunk may not already exist + if(!found[0]) { + System.out.println("format chunk missing"); + throw new IOException("Format information not found in file."); + } + if(!found[1]) { + System.out.println("data chunk missing"); + throw new IOException("Audio data not found in file."); + } + byte[] smpl = new byte[68]; + // smpl (0x00~3) + smpl[0x00] = 0x73; // s + smpl[0x01] = 0x6D; // m + smpl[0x02] = 0x70; // p + smpl[0x03] = 0x6C; // l + // size (0x04~7) + smpl[0x04] = 0x3C; // 60 bytes + // manufacturer (0x08~B) + // product (0x0C~F) + // sample period (0x10~3) + // MIDI unity note (0x14~7) + smpl[0x14] = 0x3C; // pitch 60 + // MIDI pitch fraction (0x18~B) + // SMPTE Format (0x1C~F) + // SMPTE Offset (0x20~3) + // Number of Sample Loops (0x24~7) + smpl[0x24] = 0x01; // 1 loop + // Sample Loops size (0x28~B) + // loop ID (0x2C~F) + // loop type (0x30~3) + // loop start (0x34~7) + smpl[0x34] = 0x01; // start at sample 0 + // loop end (0x38~B) + byte[] loopEnd = Analyzer.intToLittleEndian((dataSize - 8) / sampleWidth - 1); + smpl[0x38] = loopEnd[0]; // data end + smpl[0x39] = loopEnd[1]; + smpl[0x3A] = loopEnd[2]; + smpl[0x3B] = loopEnd[3]; + // tuning fraction (0x3C~F) + // play count (0x40~3) + + System.out.println("chunks found"); + + if(!found[2]) { + File dir = waveFile.getAbsoluteFile().getParentFile(); + File patched = new File(dir + "/" + FilenameUtils.removeExtension(waveFile.getName()) + "_patched.wav"); + if(!patched.exists()) + patched.createNewFile(); + try ( + FileInputStream in = new FileInputStream(waveFile); + FileOutputStream out = new FileOutputStream(patched); + ) { + + int totalSize = (int) waveFile.length() + smpl.length - 8; + byte[] sizeBytes = Analyzer.intToLittleEndian(totalSize); + + byte[] bytes = new byte[dataAddress]; + int readBytes = in.read(bytes); + + bytes[4] = sizeBytes[0]; + bytes[5] = sizeBytes[1]; + bytes[6] = sizeBytes[2]; + bytes[7] = sizeBytes[3]; + + out.write(bytes, 0, readBytes); // file up to start of data chunk + + out.write(smpl, 0, smpl.length); // sample chunk + + int part2Size = (int) waveFile.length() - dataAddress; + bytes = new byte[part2Size]; + readBytes = in.read(bytes); + out.write(bytes, 0, readBytes); // data chunk and beyond + + } // 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 + } + + System.out.println("patched"); } catch(IOException e) { System.out.println("Failed to patch file."); @@ -980,15 +1072,16 @@ public class AudioExtractor { // 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); + int headerSize = Analyzer.readChunkSize(file, 0); +// 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(); @@ -1072,10 +1165,10 @@ public class AudioExtractor { */ private static Attribute retrieveColor(BufferedReader reader) { - String prompt = "\nCommand parameters: " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT()) - + Ansi.colorize(" [color]", Attribute.BRIGHT_YELLOW_TEXT()) + Ansi.colorize(" [message]", Attribute.BRIGHT_RED_TEXT()) - + "\n" - + "\n" + Ansi.colorize("Colors", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":" + String prompt = // "\nCommand parameters: " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT()) + //+ Ansi.colorize(" [color]", Attribute.BRIGHT_YELLOW_TEXT()) + Ansi.colorize(" [message]", Attribute.BRIGHT_RED_TEXT()) + //+ "\n" + /*+*/ "\n" + Ansi.colorize("Colors", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":" + "\n- " + Ansi.colorize("Default", Attribute.CLEAR()) + "\n- " + Ansi.colorize("Black", Attribute.BLACK_TEXT()) + ", " + Ansi.colorize("Gray", Attribute.BRIGHT_BLACK_TEXT())