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