From 5cb99ba836524a68d9dee51eb3690480f159135d Mon Sep 17 00:00:00 2001 From: Nes370 Date: Mon, 15 Jul 2024 11:52:50 -0700 Subject: [PATCH] Initial commit --- .classpath | 8 + .project | 17 ++ .settings/org.eclipse.core.resources.prefs | 2 + .settings/org.eclipse.jdt.core.prefs | 14 + .../gitea/nes/AudioExtractor$1.class | Bin 0 -> 820 bytes .../gitea/nes/AudioExtractor$2.class | Bin 0 -> 820 bytes bin/goblincave/gitea/nes/AudioExtractor.class | Bin 0 -> 8832 bytes src/goblincave/gitea/nes/AudioExtractor.java | 268 ++++++++++++++++++ 8 files changed, 309 insertions(+) create mode 100644 .classpath create mode 100644 .project create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 bin/goblincave/gitea/nes/AudioExtractor$1.class create mode 100644 bin/goblincave/gitea/nes/AudioExtractor$2.class create mode 100644 bin/goblincave/gitea/nes/AudioExtractor.class create mode 100644 src/goblincave/gitea/nes/AudioExtractor.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..5f705dd --- /dev/null +++ b/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..bb77eb5 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + RR6AudioExtractor + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..71f736f --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -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 diff --git a/bin/goblincave/gitea/nes/AudioExtractor$1.class b/bin/goblincave/gitea/nes/AudioExtractor$1.class new file mode 100644 index 0000000000000000000000000000000000000000..f007380b2d4f330e3df695ecfaf13ce8f9931261 GIT binary patch literal 820 zcma)4O>fgc5Ph4tad1sb(+|EOrA?cZsunk-AS6^370HxCQ7iSfwpYnU?r!DXwEQS8 zT#CejAHa`7%-Ti}!J(FBXJ>cj?VC6L@$=hv057rYqruP{=_Hpb<(DEFNh^4ygo)lv zGpP?g*@CB57n?79cno(Y{E|mGSEJ}dG7+iu(PCIDPo<9D%3LUZF34L^FtlDtCGBg5 zW`A(P;O*;71ZZMDgpW4EVl0(7nw}@3_{fu7Fmz&_^8ADs(#7?mXUEboY{xeuI;3}b znu-g{u+fkI<~FRNlK-(Sq#6weX91S6>SJZ5yXrQ?8tyX8TOI2wQS5U=6j%C#e_`6V zPyOvBQaQMXAs*o|LtCiKoJu=pSgo1NzD>$xl7w2ArAbguMkmJ~!E3?{!LgnesS}wp zmS^X*=kiI|1C{35P=oJ<9qY`;7Kw2K`9kzDVDLpXc!utO6|84HJac0W-zi1nV-K~l=z{tqo1>qtK}IvD=oalF@{Ii4&Q}Cq%IlEUR(UwVZQ6%b0v51H5e)}R zw7TL$0&9{FcCXRt+`Gnu&lEMN;x|HGB6O>)8iDfh1W!xUfT*ogg@s RJG2WZ%h7Y+bFv0{zW_6+y%_)i literal 0 HcmV?d00001 diff --git a/bin/goblincave/gitea/nes/AudioExtractor$2.class b/bin/goblincave/gitea/nes/AudioExtractor$2.class new file mode 100644 index 0000000000000000000000000000000000000000..d5456f9e47a3f28d8c576645e3ac7f4029308726 GIT binary patch literal 820 zcma)4O>fgc5Ph4tad1sb(+@rifi`VYs#@v^DF_KwMMZMUp{Pi`ZR}ODmAi|)o0K2L zg-bvj_yPPV)LGjIA~@92?Ck8$ynXY=KYxAy0pJC8ebg8_Lp{i(O87)XLumz%lrYij zaVquUC!6!c>U?wGhsSW|f=_sqaW#zI4K74teKZ)>ic_hhH!>57UkdV8W7WsXOn2pNh&9}0n72CCS0X>)hA6Idd;h{T zai99zyW$fE_b|jGtTQx)O3eppM+~bKli9aPnN*Tc3$ru{%E{>D_#=2lm?1dT<2-R9 zQ^xY_oc3Hk345rLOdD$OSlE$HeQc2!H;^wx7d-}Fl!Iqz|5w3k*8Oug*6^)TBu>VS z5j3Xei7@s^8;drmKe9Rc*=l4obBb=!E+Ws!&+U9g@TIs8X>AmTQ{1L~SSDZrixkmt zutcjXJ|eI>`Cxa7R_opr4?a^=ql(`M-wK3wnN=ZBJ|5#qff^9Cr&QtLSy8!xP4XT( S*v1a+0?Kmq-1nTUhR!vlxxL5$ literal 0 HcmV?d00001 diff --git a/bin/goblincave/gitea/nes/AudioExtractor.class b/bin/goblincave/gitea/nes/AudioExtractor.class new file mode 100644 index 0000000000000000000000000000000000000000..a37d8fb6368fd1be35d85eb969f606f8e0e15868 GIT binary patch literal 8832 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 literal 0 HcmV?d00001 diff --git a/src/goblincave/gitea/nes/AudioExtractor.java b/src/goblincave/gitea/nes/AudioExtractor.java new file mode 100644 index 0000000..5bba95d --- /dev/null +++ b/src/goblincave/gitea/nes/AudioExtractor.java @@ -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.
+ * + * You can simply run this program in the same directory as pack_*.bin files + * to launch with default arguments:
extract currentDir currentDir wav
+ * + * @param args - 4 optional arguments
    + *
  1. "extract" or "pack" mode
  2. + *
  3. package directory containing pack_*.bin files
  4. + *
  5. extract directory for unpacked audio files
  6. + *
  7. extract BGM in "WAV" or "FLAC" format
+ * @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 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 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 findAudioTracks(RandomAccessFile file) throws IOException { + + LinkedHashMap tracklist = new LinkedHashMap(); + 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; + + } + +}