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
parent
51be8d2190
commit
38def65ed7
|
@ -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
|
|
@ -6,6 +6,7 @@ import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
|
||||||
import org.apache.commons.csv.CSVFormat;
|
import org.apache.commons.csv.CSVFormat;
|
||||||
|
@ -58,6 +59,7 @@ public class Analyzer {
|
||||||
* Indicates the start of an FLAC file.
|
* Indicates the start of an FLAC file.
|
||||||
*/
|
*/
|
||||||
final static int ASCII_fLaC = 0x664C_6143;
|
final static int ASCII_fLaC = 0x664C_6143;
|
||||||
|
final static int ASCII_LIST = 0x4C49_5354;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a map of audio tracks within provided file.
|
* Creates a map of audio tracks within provided file.
|
||||||
|
@ -289,9 +291,9 @@ public class Analyzer {
|
||||||
LEBytes = new byte[2];
|
LEBytes = new byte[2];
|
||||||
file.seek(offset + 0x18);
|
file.seek(offset + 0x18);
|
||||||
file.read(LEBytes);
|
file.read(LEBytes);
|
||||||
int extra = littleEndianToInt(LEBytes);
|
// int extra = littleEndianToInt(LEBytes);
|
||||||
if(extra != ASCII_smpl)
|
// if(extra != ASCII_smpl)
|
||||||
parseField(file, offset + 0x18, 2, " extra format bytes");
|
// 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.
|
* Helper function to read the size of an seek chunk.
|
||||||
*
|
*
|
||||||
|
@ -547,4 +572,27 @@ public class Analyzer {
|
||||||
return value;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,22 @@ public class AudioExtractor {
|
||||||
*/
|
*/
|
||||||
private final static boolean CMD_MODE = true;
|
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.
|
* 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>
|
* 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;
|
i = Analyzer.readx2stChunk(file, (int) i) - 4;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Analyzer.ASCII_LIST: {
|
||||||
|
i = Analyzer.readListChunk(file, (int) i) - 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -881,75 +901,147 @@ public class AudioExtractor {
|
||||||
try {
|
try {
|
||||||
RandomAccessFile file = new RandomAccessFile(waveFile, "r");
|
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;
|
int dataAddress = -1;
|
||||||
// Check if sample chunk already exists
|
|
||||||
int smplAddress = -1;
|
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) {
|
for(int i = 0; i < file.length(); i += 4) {
|
||||||
|
|
||||||
file.seek(i);
|
file.seek(i);
|
||||||
int scan = file.readInt();
|
int scan = file.readInt();
|
||||||
|
|
||||||
// match "data" chunk header
|
// System.out.println(i + "\t" + scan);
|
||||||
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
|
// Skip to end of chunk when found
|
||||||
if(smplAddress > -1 && dataAddress > -1)
|
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;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine size of data chunk
|
System.out.println("chunks searched");
|
||||||
int dataSize;
|
|
||||||
if(dataAddress > -1)
|
|
||||||
dataSize = Analyzer.readChunkSize(file, dataAddress);
|
|
||||||
else throw new IOException("Data chunk not in file.");
|
|
||||||
|
|
||||||
// Determine size of smpl chunk
|
if(!found[0]) {
|
||||||
int smplSize;
|
System.out.println("format chunk missing");
|
||||||
if(smplAddress > -1)
|
throw new IOException("Format information not found in file.");
|
||||||
smplSize = Analyzer.readChunkSize(file, smplAddress);
|
}
|
||||||
// smpl chunk may not already exist
|
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
|
// Create new sample chunk
|
||||||
// Insert 68-byte sample chunk with loop before data 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
|
// Set loop points to start and end of data chunk
|
||||||
// Update the new file size in RIFF header
|
// Update the new file size in RIFF header
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("patched");
|
||||||
|
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
System.out.println("Failed to patch file.");
|
System.out.println("Failed to patch file.");
|
||||||
|
@ -980,15 +1072,16 @@ public class AudioExtractor {
|
||||||
// first 4 bytes start with RIFF
|
// first 4 bytes start with RIFF
|
||||||
if(file.readInt() == Analyzer.ASCII_RIFF) {
|
if(file.readInt() == Analyzer.ASCII_RIFF) {
|
||||||
// next 4 bytes contain the size of the file in little-endian representation
|
// next 4 bytes contain the size of the file in little-endian representation
|
||||||
file.seek(4);
|
int headerSize = Analyzer.readChunkSize(file, 0);
|
||||||
byte[] buffer = new byte[4];
|
// file.seek(4);
|
||||||
file.read(buffer); // write size bytes to buffer
|
// byte[] buffer = new byte[4];
|
||||||
|
// file.read(buffer); // write size bytes to buffer
|
||||||
// calculate expected file size
|
//
|
||||||
int headerSize = 8 + buffer[3]
|
// // calculate expected file size
|
||||||
+ buffer[2] * (int) Math.pow(16, 2)
|
// int headerSize = 8 + buffer[3]
|
||||||
+ buffer[1] * (int) Math.pow(16, 4)
|
// + buffer[2] * (int) Math.pow(16, 2)
|
||||||
+ buffer[0] * (int) Math.pow(16, 6);
|
// + buffer[1] * (int) Math.pow(16, 4)
|
||||||
|
// + buffer[0] * (int) Math.pow(16, 6);
|
||||||
|
|
||||||
// file is valid if actual file length matches expected size
|
// file is valid if actual file length matches expected size
|
||||||
valid = headerSize == file.length();
|
valid = headerSize == file.length();
|
||||||
|
@ -1072,10 +1165,10 @@ public class AudioExtractor {
|
||||||
*/
|
*/
|
||||||
private static Attribute retrieveColor(BufferedReader reader) {
|
private static Attribute retrieveColor(BufferedReader reader) {
|
||||||
|
|
||||||
String prompt = "\nCommand parameters: " + Ansi.colorize("Print", Attribute.BRIGHT_BLUE_TEXT())
|
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())
|
//+ Ansi.colorize(" [color]", Attribute.BRIGHT_YELLOW_TEXT()) + Ansi.colorize(" [message]", Attribute.BRIGHT_RED_TEXT())
|
||||||
+ "\n"
|
//+ "\n"
|
||||||
+ "\n" + Ansi.colorize("Colors", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":"
|
/*+*/ "\n" + Ansi.colorize("Colors", Attribute.BRIGHT_YELLOW_TEXT(), Attribute.BOLD()) + ":"
|
||||||
+ "\n- " + Ansi.colorize("Default", Attribute.CLEAR())
|
+ "\n- " + Ansi.colorize("Default", Attribute.CLEAR())
|
||||||
+ "\n- " + Ansi.colorize("Black", Attribute.BLACK_TEXT())
|
+ "\n- " + Ansi.colorize("Black", Attribute.BLACK_TEXT())
|
||||||
+ ", " + Ansi.colorize("Gray", Attribute.BRIGHT_BLACK_TEXT())
|
+ ", " + Ansi.colorize("Gray", Attribute.BRIGHT_BLACK_TEXT())
|
||||||
|
|
Loading…
Reference in New Issue