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.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in New Issue