Update AudioExtractor.java
							parent
							
								
									80760bc534
								
							
						
					
					
						commit
						21efae2c2a
					
				| 
						 | 
					@ -38,63 +38,6 @@ public class AudioExtractor {
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	private final static boolean CMD_MODE = true;
 | 
						private final static boolean CMD_MODE = true;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * ANSI code to go up one line, followed by a carriage return.
 | 
					 | 
				
			||||||
	 * Useful for overwriting user input.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static String ANSI_BACKLINE = "\033[F\r";
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * ANSI code to switch to beige colored text.
 | 
					 | 
				
			||||||
	 * Useful for highlighting user input.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static String ANSI_BEIGE = "\u001B[93m";
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "RIFF".
 | 
					 | 
				
			||||||
	 * Indicates the start of a RIFF header in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_RIFF = 0x5249_4646;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "RIFF".
 | 
					 | 
				
			||||||
	 * Indicates the start of a RIFF header in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_WAVE = 0x5741_5645;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "fmt ".
 | 
					 | 
				
			||||||
	 * Indicates the start of a format chunk in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_fmt = 0x666D_7420;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "smpl".
 | 
					 | 
				
			||||||
	 * Indicates the start of a sample chunk in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_smpl = 0x736D_706C;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "data".
 | 
					 | 
				
			||||||
	 * Indicates the start of a data chunk in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_data = 0x6461_7461;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "ALIG".
 | 
					 | 
				
			||||||
	 * Indicates the start of an alignment chunk in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_ALIG = 0x414C_4947;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "x2st".
 | 
					 | 
				
			||||||
	 * Indicates the start of an XMA stream chunk in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_x2st = 0x7832_7374;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "seek".
 | 
					 | 
				
			||||||
	 * Indicates the start of an XMA stream chunk in WAV files.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_seek = 0x7365_656B;
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Hex value for the ASCII sequence "fLaC".
 | 
					 | 
				
			||||||
	 * Indicates the start of an FLAC file.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private final static int ASCII_fLaC = 0x664C_6143;
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * 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>
 | 
				
			||||||
| 
						 | 
					@ -186,6 +129,9 @@ public class AudioExtractor {
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
					case "convert": {
 | 
										case "convert": {
 | 
				
			||||||
 | 
					//						File audioFile = retrieveAudio(reader);
 | 
				
			||||||
 | 
					//						if(audioFile.isFile())
 | 
				
			||||||
 | 
					//							convert(audioFile, encoding);
 | 
				
			||||||
						
 | 
											
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
| 
						 | 
					@ -214,87 +160,83 @@ public class AudioExtractor {
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
		System.exit(0);
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if(args.length > 0) {
 | 
					 | 
				
			||||||
			System.out.println("Arguments\t:");
 | 
					 | 
				
			||||||
			for(int i = 0; i < args.length; i++)
 | 
					 | 
				
			||||||
				System.out.print(Ansi.colorize(args[i] + ' ', Attribute.BRIGHT_GREEN_TEXT()));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if(args.length > 0) {
 | 
					 | 
				
			||||||
			switch(args[0]) {
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				case "help":
 | 
					 | 
				
			||||||
					if(args.length > 1)
 | 
					 | 
				
			||||||
						switch(args[1]) {
 | 
					 | 
				
			||||||
							case "extract":
 | 
					 | 
				
			||||||
								
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							case "pack":
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							case "print":
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					System.out.println("java -jar RR6AudioExtractor.jar [operation] [package] [extract] [option]");
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
				case "extract":
 | 
					 | 
				
			||||||
					// TODO 
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
				case "pack":
 | 
					 | 
				
			||||||
					// TODO
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// 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);
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Examines a given file and reads some of its properties.
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * @param givenFile
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static void identify(File givenFile) {
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// TODO check for characteristics of BIN or WAV files
 | 
				
			||||||
 | 
							String name = givenFile.getName();
 | 
				
			||||||
 | 
							System.out.println("Name:\t" + name);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							long size = givenFile.length();
 | 
				
			||||||
 | 
							System.out.println("Size:\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "") + "\n");
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// file extension
 | 
				
			||||||
 | 
							int extIndex = name.lastIndexOf('.');
 | 
				
			||||||
 | 
							String extension = null;
 | 
				
			||||||
 | 
							if(extIndex > 0 && extIndex < name.length() - 1)
 | 
				
			||||||
 | 
								extension = name.substring(extIndex + 1);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								RandomAccessFile file = new RandomAccessFile(givenFile, "r");
 | 
				
			||||||
 | 
								switch(extension.toLowerCase()) {
 | 
				
			||||||
 | 
									case "bin": {
 | 
				
			||||||
 | 
										LinkedHashMap<Integer, Integer> tracklist = findAudioTracks(file);
 | 
				
			||||||
 | 
										System.out.println(tracklist.size() + " Tracks (" + convertBytes(size / tracklist.size()) + " avg size)\n");
 | 
				
			||||||
 | 
										System.out.println("Format Info");
 | 
				
			||||||
 | 
										readFormatChunk(file, 0x0C);
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									case "wav":
 | 
				
			||||||
 | 
									case "xma":
 | 
				
			||||||
 | 
									default: {
 | 
				
			||||||
 | 
										for(long i = 0; i < size - 4; i += 4) {
 | 
				
			||||||
 | 
											file.seek(i);
 | 
				
			||||||
 | 
											int value = file.readInt();
 | 
				
			||||||
 | 
											switch(value) {
 | 
				
			||||||
 | 
												case ASCII_RIFF: {
 | 
				
			||||||
 | 
													readRIFFChunk(file, (int) i);
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												case ASCII_fmt: {
 | 
				
			||||||
 | 
													i = readFormatChunk(file, (int) i) - 4;
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												case ASCII_smpl: {
 | 
				
			||||||
 | 
													i = readSampleChunk(file, (int) i) - 4;
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												case ASCII_data: {
 | 
				
			||||||
 | 
													i = readDataChunk(file, (int) i) - 4;
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												case ASCII_ALIG: {
 | 
				
			||||||
 | 
													i = readAlignmentChunk(file, (int) i) - 4;
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												case ASCII_seek: {
 | 
				
			||||||
 | 
													i = readx2stChunk(file, (int) i) - 4;
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												case ASCII_x2st: {
 | 
				
			||||||
 | 
													i = readx2stChunk(file, (int) i) - 4;
 | 
				
			||||||
 | 
													break;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								System.out.println("Could not read file.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Extracts audio tracks from the given package file.
 | 
						 * Extracts audio tracks from the given package file.
 | 
				
			||||||
| 
						 | 
					@ -364,18 +306,6 @@ public class AudioExtractor {
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static void printProgressBar(int current, int total) {
 | 
					 | 
				
			||||||
		int done = 50 * current / total;
 | 
					 | 
				
			||||||
		int todo = 50 - done;
 | 
					 | 
				
			||||||
		String doneStr = ""; // String.format("%" + done + "s", '█');
 | 
					 | 
				
			||||||
		for(int i = 0; i < done; i++)
 | 
					 | 
				
			||||||
			doneStr += '█';
 | 
					 | 
				
			||||||
		String todoStr = ""; // String.format("%" + todo + "s", '_');
 | 
					 | 
				
			||||||
		for(int i = 0; i < todo; i++)
 | 
					 | 
				
			||||||
			doneStr += '_';
 | 
					 | 
				
			||||||
		System.out.println(ANSI_BACKLINE + "Progress: |" + doneStr + todoStr + "| (" + current + "/" + total + ")");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Extracts audio files to the extract directory from package files located at the package directory.
 | 
						 * Extracts audio files to the extract directory from package files located at the package directory.
 | 
				
			||||||
	 * Also supports FLAC conversion for BGM files.
 | 
						 * Also supports FLAC conversion for BGM files.
 | 
				
			||||||
| 
						 | 
					@ -383,6 +313,7 @@ public class AudioExtractor {
 | 
				
			||||||
	 * @param packDirectory
 | 
						 * @param packDirectory
 | 
				
			||||||
	 * @param extractDirectory
 | 
						 * @param extractDirectory
 | 
				
			||||||
	 * @param compressBGM
 | 
						 * @param compressBGM
 | 
				
			||||||
 | 
						 * @deprecated delete after adapting FLAC conversion
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) {
 | 
						public static void extract(File packDirectory, File extractDirectory, boolean compressBGM) {
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
| 
						 | 
					@ -515,6 +446,168 @@ public class AudioExtractor {
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Packs WAV files located at the extract directory into BIN files at the package directory.
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * @param packDirectory
 | 
				
			||||||
 | 
						 * @param extractDirectory
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private static void pack(File packDirectory, File extractDirectory) {
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// Identify extracted folders
 | 
				
			||||||
 | 
							File[] packages = new File(extractDirectory.toString()).listFiles(new FilenameFilter() {
 | 
				
			||||||
 | 
								public boolean accept(File dir, String name) {
 | 
				
			||||||
 | 
									return name.toLowerCase().startsWith("pack_") && !name.toLowerCase().endsWith(".bin");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}); 
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
							for(int i = 0; i < packages.length; i++)
 | 
				
			||||||
 | 
								System.out.println(packages[i].getName());
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							for(File packDir : packages) {
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// 1. Compile list of WAV files in extracted directories
 | 
				
			||||||
 | 
								File[] audioTracks = new File(packDir.toString()).listFiles(new FilenameFilter() {
 | 
				
			||||||
 | 
									public boolean accept(File dir, String name) {
 | 
				
			||||||
 | 
										return name.toLowerCase().endsWith(".wav");
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// 2. Check if pack files already exist in package directory
 | 
				
			||||||
 | 
								File pack = new File(packDir.getPath() + ".bin");
 | 
				
			||||||
 | 
								if(pack.exists());
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// 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.
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Patches a WAV file by seeking out its data and smpl chunks, 
 | 
				
			||||||
 | 
						 * replacing any existing smpl chunk with a new smpl chunk,
 | 
				
			||||||
 | 
						 * setting loop points at the start and end of the data chunk.
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * @param waveFile
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private static void patch(File waveFile) {
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								RandomAccessFile file = new RandomAccessFile(waveFile, "r");
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// Also Find data location
 | 
				
			||||||
 | 
								int dataAddress = -1;
 | 
				
			||||||
 | 
								// Check if sample chunk already exists
 | 
				
			||||||
 | 
								int smplAddress = -1;
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								for(int i = 0; i < file.length(); i += 4) {
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									file.seek(i);
 | 
				
			||||||
 | 
									int scan = file.readInt();
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									// match "data" chunk header
 | 
				
			||||||
 | 
									if(scan == ASCII_data)
 | 
				
			||||||
 | 
										dataAddress = i;
 | 
				
			||||||
 | 
									// match "smpl" chunk header
 | 
				
			||||||
 | 
									else if(scan == ASCII_smpl)
 | 
				
			||||||
 | 
										smplAddress = i;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									// if both chunks already located, stop seeking
 | 
				
			||||||
 | 
									if(smplAddress > -1 && dataAddress > -1)
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// Determine size of data chunk
 | 
				
			||||||
 | 
								int dataSize;
 | 
				
			||||||
 | 
								if(dataAddress > -1)
 | 
				
			||||||
 | 
									dataSize = readChunkSize(file, dataAddress);
 | 
				
			||||||
 | 
								else throw new IOException("Data chunk not in file.");
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// Determine size of smpl chunk
 | 
				
			||||||
 | 
								int smplSize;
 | 
				
			||||||
 | 
								if(smplAddress > -1)
 | 
				
			||||||
 | 
									smplSize = readChunkSize(file, smplAddress);	
 | 
				
			||||||
 | 
								// smpl chunk may not already exist
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								// 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
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
							} catch(IOException e) {
 | 
				
			||||||
 | 
								System.out.println("Failed to patch file.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Helper function to show current progress.
 | 
				
			||||||
 | 
						 * Should only be used in CMD Mode.
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * @param current
 | 
				
			||||||
 | 
						 * @param total
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static void printProgressBar(int current, int total) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							int width = 50; // progress bar total width
 | 
				
			||||||
 | 
							double progress = (double) current / total; // 0 = none, 1 = complete
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							int progressWidth = (int) Math.floor(progress * width);
 | 
				
			||||||
 | 
							double progressRemainder = (progress * width) % 1.0;
 | 
				
			||||||
 | 
							int x = (int) Math.floor(progressRemainder * 8);
 | 
				
			||||||
 | 
							char[] c = { ' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉' };
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							String bar = "";
 | 
				
			||||||
 | 
							for(int i = 0; i < width; i++) {
 | 
				
			||||||
 | 
								if(i < progressWidth)
 | 
				
			||||||
 | 
									bar += '█';
 | 
				
			||||||
 | 
								else if(i == progressWidth)
 | 
				
			||||||
 | 
									bar += c[x];
 | 
				
			||||||
 | 
								else bar += ' ';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							System.out.println(ANSI_BACKLINE + "Progress: |" + bar + "| (" + current + "/" + total + ")");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Helper method to prompt user to select a package file, or folder containing one or more packages.
 | 
						 * Helper method to prompt user to select a package file, or folder containing one or more packages.
 | 
				
			||||||
	 * 
 | 
						 * 
 | 
				
			||||||
| 
						 | 
					@ -637,84 +730,6 @@ public class AudioExtractor {
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Examines a given file and reads some of its properties.
 | 
					 | 
				
			||||||
	 * 
 | 
					 | 
				
			||||||
	 * @param givenFile
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public static void identify(File givenFile) {
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// TODO check for characteristics of BIN or WAV files
 | 
					 | 
				
			||||||
		String name = givenFile.getName();
 | 
					 | 
				
			||||||
		System.out.println("Name:\t" + name);
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		long size = givenFile.length();
 | 
					 | 
				
			||||||
		System.out.println("Size:\t" + size + " Bytes" + (size > 1024 ? " (" + convertBytes(size) + ")" : "") + "\n");
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// file extension
 | 
					 | 
				
			||||||
		int extIndex = name.lastIndexOf('.');
 | 
					 | 
				
			||||||
		String extension = null;
 | 
					 | 
				
			||||||
		if(extIndex > 0 && extIndex < name.length() - 1)
 | 
					 | 
				
			||||||
			extension = name.substring(extIndex + 1);
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			RandomAccessFile file = new RandomAccessFile(givenFile, "r");
 | 
					 | 
				
			||||||
			switch(extension.toLowerCase()) {
 | 
					 | 
				
			||||||
				case "bin": {
 | 
					 | 
				
			||||||
					LinkedHashMap<Integer, Integer> tracklist = findAudioTracks(file);
 | 
					 | 
				
			||||||
					System.out.println(tracklist.size() + " Tracks (" + convertBytes(size / tracklist.size()) + " avg size)\n");
 | 
					 | 
				
			||||||
					System.out.println("Format Info");
 | 
					 | 
				
			||||||
					readFormatChunk(file, 0x0C);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				case "wav":
 | 
					 | 
				
			||||||
				case "xma":
 | 
					 | 
				
			||||||
				default: {
 | 
					 | 
				
			||||||
					for(long i = 0; i < size - 4; i += 4) {
 | 
					 | 
				
			||||||
						file.seek(i);
 | 
					 | 
				
			||||||
						int value = file.readInt();
 | 
					 | 
				
			||||||
						switch(value) {
 | 
					 | 
				
			||||||
							case ASCII_RIFF: {
 | 
					 | 
				
			||||||
								readRIFFChunk(file, (int) i);
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
							case ASCII_fmt: {
 | 
					 | 
				
			||||||
								i = readFormatChunk(file, (int) i) - 4;
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
							case ASCII_smpl: {
 | 
					 | 
				
			||||||
								i = readSampleChunk(file, (int) i) - 4;
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
							case ASCII_data: {
 | 
					 | 
				
			||||||
								i = readDataChunk(file, (int) i) - 4;
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
							case ASCII_ALIG: {
 | 
					 | 
				
			||||||
								i = readAlignmentChunk(file, (int) i) - 4;
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
							case ASCII_seek: {
 | 
					 | 
				
			||||||
								i = readx2stChunk(file, (int) i) - 4;
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
							case ASCII_x2st: {
 | 
					 | 
				
			||||||
								i = readx2stChunk(file, (int) i) - 4;
 | 
					 | 
				
			||||||
								break;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (IOException e) {
 | 
					 | 
				
			||||||
			System.out.println("Could not read file.");
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// check if RIFF header
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Helper function to read RIFF header information.
 | 
						 * Helper function to read RIFF header information.
 | 
				
			||||||
	 * 
 | 
						 * 
 | 
				
			||||||
| 
						 | 
					@ -1046,138 +1061,7 @@ public class AudioExtractor {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Packs WAV files located at the extract directory into BIN files at the package directory.
 | 
					 | 
				
			||||||
	 * 
 | 
					 | 
				
			||||||
	 * @param packDirectory
 | 
					 | 
				
			||||||
	 * @param extractDirectory
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private static void pack(File packDirectory, File extractDirectory) {
 | 
					 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		// Identify extracted folders
 | 
					 | 
				
			||||||
		File[] packages = new File(extractDirectory.toString()).listFiles(new FilenameFilter() {
 | 
					 | 
				
			||||||
			public boolean accept(File dir, String name) {
 | 
					 | 
				
			||||||
				return name.toLowerCase().startsWith("pack_") && !name.toLowerCase().endsWith(".bin");
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}); 
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
		for(int i = 0; i < packages.length; i++)
 | 
					 | 
				
			||||||
			System.out.println(packages[i].getName());
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		for(File packDir : packages) {
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// 1. Compile list of WAV files in extracted directories
 | 
					 | 
				
			||||||
			File[] audioTracks = new File(packDir.toString()).listFiles(new FilenameFilter() {
 | 
					 | 
				
			||||||
				public boolean accept(File dir, String name) {
 | 
					 | 
				
			||||||
					return name.toLowerCase().endsWith(".wav");
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// 2. Check if pack files already exist in package directory
 | 
					 | 
				
			||||||
			File pack = new File(packDir.getPath() + ".bin");
 | 
					 | 
				
			||||||
			if(pack.exists());
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// 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.
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Patches a WAV file by seeking out its data and smpl chunks, 
 | 
					 | 
				
			||||||
	 * replacing any existing smpl chunk with a new smpl chunk,
 | 
					 | 
				
			||||||
	 * setting loop points at the start and end of the data chunk.
 | 
					 | 
				
			||||||
	 * 
 | 
					 | 
				
			||||||
	 * @param waveFile
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private static void patch(File waveFile) {
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			RandomAccessFile file = new RandomAccessFile(waveFile, "r");
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// Also Find data location
 | 
					 | 
				
			||||||
			int dataAddress = -1;
 | 
					 | 
				
			||||||
			// Check if sample chunk already exists
 | 
					 | 
				
			||||||
			int smplAddress = -1;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			for(int i = 0; i < file.length(); i += 4) {
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				file.seek(i);
 | 
					 | 
				
			||||||
				int scan = file.readInt();
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				// match "data" chunk header
 | 
					 | 
				
			||||||
				if(scan == ASCII_data)
 | 
					 | 
				
			||||||
					dataAddress = i;
 | 
					 | 
				
			||||||
				// match "smpl" chunk header
 | 
					 | 
				
			||||||
				else if(scan == ASCII_smpl)
 | 
					 | 
				
			||||||
					smplAddress = i;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				// if both chunks already located, stop seeking
 | 
					 | 
				
			||||||
				if(smplAddress > -1 && dataAddress > -1)
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// Determine size of data chunk
 | 
					 | 
				
			||||||
			int dataSize;
 | 
					 | 
				
			||||||
			if(dataAddress > -1)
 | 
					 | 
				
			||||||
				dataSize = readChunkSize(file, dataAddress);
 | 
					 | 
				
			||||||
			else throw new IOException("Data chunk not in file.");
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// Determine size of smpl chunk
 | 
					 | 
				
			||||||
			int smplSize;
 | 
					 | 
				
			||||||
			if(smplAddress > -1)
 | 
					 | 
				
			||||||
				smplSize = readChunkSize(file, smplAddress);	
 | 
					 | 
				
			||||||
			// smpl chunk may not already exist
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			// 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
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
		} catch(IOException e) {
 | 
					 | 
				
			||||||
			System.out.println("Failed to patch file.");
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Helper function to prompt user to select a WAV file.
 | 
						 * Helper function to prompt user to select a WAV file.
 | 
				
			||||||
| 
						 | 
					@ -2443,4 +2327,63 @@ public class AudioExtractor {
 | 
				
			||||||
		return value;
 | 
							return value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * ANSI code to go up one line, followed by a carriage return.
 | 
				
			||||||
 | 
						 * Useful for overwriting user input.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static String ANSI_BACKLINE = "\033[F\r";
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * ANSI code to switch to beige colored text.
 | 
				
			||||||
 | 
						 * Useful for highlighting user input.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static String ANSI_BEIGE = "\u001B[93m";
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "RIFF".
 | 
				
			||||||
 | 
						 * Indicates the start of a RIFF header in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_RIFF = 0x5249_4646;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "RIFF".
 | 
				
			||||||
 | 
						 * Indicates the start of a RIFF header in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_WAVE = 0x5741_5645;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "fmt ".
 | 
				
			||||||
 | 
						 * Indicates the start of a format chunk in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_fmt = 0x666D_7420;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "smpl".
 | 
				
			||||||
 | 
						 * Indicates the start of a sample chunk in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_smpl = 0x736D_706C;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "data".
 | 
				
			||||||
 | 
						 * Indicates the start of a data chunk in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_data = 0x6461_7461;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "ALIG".
 | 
				
			||||||
 | 
						 * Indicates the start of an alignment chunk in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_ALIG = 0x414C_4947;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "x2st".
 | 
				
			||||||
 | 
						 * Indicates the start of an XMA stream chunk in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_x2st = 0x7832_7374;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "seek".
 | 
				
			||||||
 | 
						 * Indicates the start of an XMA stream chunk in WAV files.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_seek = 0x7365_656B;
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Hex value for the ASCII sequence "fLaC".
 | 
				
			||||||
 | 
						 * Indicates the start of an FLAC file.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private final static int ASCII_fLaC = 0x664C_6143;
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue