Initial commit
commit
5cb99ba836
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" path="src"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-12"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/javaFlacEncoder"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Apache Commons IO"/>
|
||||||
|
<classpathentry kind="output" path="bin"/>
|
||||||
|
</classpath>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>RR6AudioExtractor</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
|
@ -0,0 +1,2 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
encoding/<project>=UTF-8
|
|
@ -0,0 +1,14 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.targetPlatform=12
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||||
|
org.eclipse.jdt.core.compiler.compliance=12
|
||||||
|
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||||
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||||
|
org.eclipse.jdt.core.compiler.release=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.source=12
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,268 @@
|
||||||
|
package goblincave.gitea.nes;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FilenameFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
|
||||||
|
import javaFlacEncoder.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ridge Racer 6 Audio Extractor
|
||||||
|
* @author Nes
|
||||||
|
* @version 1.0, 2024-07-14
|
||||||
|
*/
|
||||||
|
public class AudioExtractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
*
|
||||||
|
* You can simply run this program in the same directory as <b>pack_*.bin</b> files
|
||||||
|
* to launch with default arguments: <pre>extract currentDir currentDir wav</pre>
|
||||||
|
*
|
||||||
|
* @param args - 4 optional arguments<ol>
|
||||||
|
* <li>"extract" or "pack" mode</li>
|
||||||
|
* <li>package directory containing <b>pack_*.bin</b> files</li>
|
||||||
|
* <li>extract directory for unpacked audio files</li>
|
||||||
|
* <li>extract BGM in "WAV" or "FLAC" format</li></ol>
|
||||||
|
* @throws URISyntaxException
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws URISyntaxException {
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void pack(File packDirectory, File extractDirectory) {
|
||||||
|
|
||||||
|
// 1. Compile list of WAV files in extracted directories
|
||||||
|
// 2. Check if pack files already exist in package directory
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) {
|
||||||
|
|
||||||
|
// TODO Delete
|
||||||
|
System.out.println("packDirectory:\t" + packDirectory);
|
||||||
|
System.out.println("extractDirectory:\t" + extractDirectory);
|
||||||
|
|
||||||
|
// Identify target binary files
|
||||||
|
File[] packages = new File(packDirectory.toString()).listFiles(new FilenameFilter() {
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
return name.toLowerCase().endsWith(".bin");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify and extract audio within binary files
|
||||||
|
for(int i = 0; i < packages.length; i++) {
|
||||||
|
|
||||||
|
RandomAccessFile source;
|
||||||
|
LinkedHashMap<Integer, Integer> tracklist = null;
|
||||||
|
try {
|
||||||
|
source = new RandomAccessFile(packages[i], "r");
|
||||||
|
tracklist = findAudioTracks(source);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
System.out.println("Binary file:\t" + packages[i] + "\n was not found. File skipped.");
|
||||||
|
continue;
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("Binary file:\t" + packages[i] + "\n could not be read. File skipped.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO Delete
|
||||||
|
System.out.println("packages[" + i + "]:\t" + packages[i]);
|
||||||
|
|
||||||
|
File dir = new File(extractDirectory.getPath() + "/" + FilenameUtils.removeExtension(packages[i].getName()));
|
||||||
|
if(!dir.exists())
|
||||||
|
dir.mkdir();
|
||||||
|
|
||||||
|
// Extract and write WAV files in directory
|
||||||
|
Set<Integer> keys = tracklist.keySet();
|
||||||
|
int track = 1;
|
||||||
|
|
||||||
|
for(Integer key : keys) {
|
||||||
|
|
||||||
|
String name = String.format("track%04d", track);
|
||||||
|
{ // Extract WAV files
|
||||||
|
File wav = null;
|
||||||
|
try {
|
||||||
|
|
||||||
|
// create file for storing WAV data
|
||||||
|
wav = new File(dir.getPath() + "/" + name + ".wav");
|
||||||
|
if(!wav.exists())
|
||||||
|
wav.createNewFile();
|
||||||
|
|
||||||
|
//TODO Delete
|
||||||
|
System.out.println("Saving track " + String.format("%d (@0x%08X)", track, key) + " to " + wav + String.format(" (%d bytes)", tracklist.get(key)));
|
||||||
|
|
||||||
|
// write selection to file
|
||||||
|
try (
|
||||||
|
FileInputStream inStream = new FileInputStream(packages[i]);
|
||||||
|
FileOutputStream outStream = new FileOutputStream(wav)
|
||||||
|
) {
|
||||||
|
inStream.skipNBytes(key.intValue());
|
||||||
|
byte[] trackBytes = new byte[tracklist.get(key)];
|
||||||
|
int readBytes = inStream.read(trackBytes);
|
||||||
|
outStream.write(trackBytes, 0, readBytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("An error occurred when attempting to write " + name + " to file:\t" + wav);
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track++;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(compressBGM && packages[i].getName().equals("pack_bgm.bin")) {
|
||||||
|
|
||||||
|
// find WAV files
|
||||||
|
File[] WAV_Files = dir.listFiles(new FilenameFilter() {
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
return name.toLowerCase().endsWith(".wav");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// convert WAV files to FLAC
|
||||||
|
FLAC_FileEncoder ffe = new FLAC_FileEncoder();
|
||||||
|
|
||||||
|
for(File wav : WAV_Files) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
File flac = new File(dir.getPath() + "/" + FilenameUtils.removeExtension(wav.getName()) + ".flac");
|
||||||
|
if(!flac.exists())
|
||||||
|
flac.createNewFile();
|
||||||
|
|
||||||
|
//TODO Delete
|
||||||
|
System.out.println("Compressing WAV to FLAC:\t" + flac);
|
||||||
|
|
||||||
|
ffe.encode(wav, flac);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("An error occurred when attempting to write file.");
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.print("Deleting WAV files");
|
||||||
|
// delete WAV files
|
||||||
|
for(File wav : WAV_Files) {
|
||||||
|
boolean deleted = false;
|
||||||
|
while(!deleted) {
|
||||||
|
try {
|
||||||
|
Files.delete(wav.toPath());
|
||||||
|
deleted = true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
System.out.print('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a map of audio tracks within provided file.
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @return Tracklist containing address-length pairs
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static LinkedHashMap<Integer, Integer> findAudioTracks(RandomAccessFile file) throws IOException {
|
||||||
|
|
||||||
|
LinkedHashMap<Integer, Integer> tracklist = new LinkedHashMap<Integer, Integer>();
|
||||||
|
int key = 0x52494646; // "RIFF" in ASCII representation
|
||||||
|
|
||||||
|
// Files start on addresses divisible by 0x800
|
||||||
|
for(int a = 0; a + 0x800 < file.length(); a += 0x800) {
|
||||||
|
|
||||||
|
file.seek(a);
|
||||||
|
|
||||||
|
// File header starts with "RIFF" sequence
|
||||||
|
if(file.readInt() == key) {
|
||||||
|
|
||||||
|
// Next 4 bytes specify file length beyond first 8 bytes, in little-endian representation
|
||||||
|
int length = 8;
|
||||||
|
// Calculate remaining length of file
|
||||||
|
for(int d = 0; d < 4; d++) {
|
||||||
|
file.seek(a + 4 + d);
|
||||||
|
length += file.readUnsignedByte() * ((int) Math.pow(16, 2 * d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record file position in map
|
||||||
|
tracklist.put(a, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracklist;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue