Patch function update

Completed original intended scope of patch function, however adding sample chunk proved ineffective. More information must be encoded in x2st chunk, which requires some analysis due to lack of documentation.
main
Nes370 2024-07-28 22:26:20 -07:00
parent 51be8d2190
commit 38def65ed7
3 changed files with 226 additions and 65 deletions

20
RR6AudioExtractor.md Normal file
View File

@ -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

View File

@ -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();
}
}

View File

@ -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.<br>
@ -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())