Converting .bin audio files to .wav in Java

Let's say you have some old-school CD games that use Red Book audio. You know, the kind of CD game you can pop into your regular CD player and listen to the music like a giant nerd. Most Sega CD and TurboGrafx-16 CD games work like this. Some assortment of Saturn and PlayStation games are like this too.

If you want to play these games on an emulator you probably ripped them into a bin/cue format with a .cue file like this:


FILE "whatever1.bin" BINARY
  TRACK 01 AUDIO
    INDEX 01 00:00:00
FILE "whatever2.bin" BINARY
  TRACK 02 MODE1/2352
    INDEX 00 00:00:00
    INDEX 01 00:03:00
FILE "whatever3.bin" BINARY
  TRACK 03 AUDIO
    INDEX 00 00:00:00
    INDEX 01 00:02:00
[and so on]

Now let's say you want to copy the soundtrack from this game to your phone or some other modern music listening device. Of course you could rip the audio from the CD but maybe there's some very believable reason you can't...

Maybe you misplaced or damaged the original CD? Maybe you ripped them as bin/cue years ago and no longer have a CD drive in your computer since they're kind of useless now? I guess those stories check out, right?

Look, I don't care how you got these CD images in the first place. I'm not here to judge you. The bottom line is you have some old CD games in bin/cue format and want to convert the audio tracks to something you can use.

I originally searched for a solution and found "helpful" advice like:

It didn't take long to realize I could write this in Java since there are built-in libraries to read and write audio files. It turns out it's super easy. Excluding comments and brackets, it's only about 50 lines of code. This is going to be a simple class that accepts two parameters - path to a cue sheet and where to write the .wav files. If you want to make it fancier or prettier go ahead and do that.


/*
BinToWav

Copyright (c) 2021 Hugues Johnson

Permission is hereby granted, free of charge, to any person obtaining a copy of 
this software and associated documentation files(the "Software"), to deal in 
the Software without restriction, including without limitation the rights to 
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do 
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
THE SOFTWARE.
*/

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

//this is not meant to be enterprise-y code - it's a quick utility class for a specific purpose
//this was written and tested in 45 minutes - most of that time spent debating whether I wanted to do any error handling at all
public class BinToWav{
	private final static String SEPARATOR=File.separator;
	//these settings assume the .bin is a redbook audio file
	private final static float SAMPLE_RATE=44100;
	private final static int SAMPLE_BITS=16;
	private final static int CHANNELS=2;
	private final static boolean SIGNED=true;
	private final static boolean BIG_ENDIAN=false;
	private static final AudioFormat FORMAT=new AudioFormat(SAMPLE_RATE,SAMPLE_BITS,CHANNELS,SIGNED,BIG_ENDIAN);

	//this expects two arguments
	//arg[0] = path to a .cue file
	//arg[1] = path to write wav files
	public static void main(String[] args){
		BufferedReader cueReader=null;
		AudioInputStream audioIn=null;
		try{
			if(args.length<2){
				throw(new Exception("Expecting two arguments - [0] cue path [1] output directoty path"));
			}
			File cueFile=new File(args[0]);
			//some minimal validation
			if(!cueFile.exists()){
				throw(new Exception(args[0]+" doesn't appear to be a file that exists"));
			}
			cueReader=new BufferedReader(new InputStreamReader(new FileInputStream(cueFile)));
			//cueDir is used to build the path to each .bin file entry
			String cueDir=cueFile.getPath();
			cueDir=cueDir.substring(0,cueDir.lastIndexOf(SEPARATOR)+1);
			String outputDir=args[1];
			//more minimal validation
			if(!outputDir.endsWith(SEPARATOR)){outputDir=outputDir+SEPARATOR;}
			if(!(new File(outputDir)).exists()){
				throw(new Exception(args[1]+" doesn't appear to be a directory that exists"));
			}
			String currentLine=null;
			while((currentLine=cueReader.readLine())!=null){
				if(currentLine.toUpperCase().startsWith("FILE")){
					//yes, this will error out if the .cue ends in a FILE line
					//since it's therefore not a valid .cue sheet in the first place this is the least of your problems if you encounter it
					String nextLine=cueReader.readLine();
					if(nextLine.toUpperCase().endsWith("AUDIO")){
						//hooray, we have an audio file
						//next build the path to it
						String fileName=currentLine.substring(currentLine.indexOf("\"")+1,currentLine.lastIndexOf("\""));
						String binPath=cueDir+fileName;
						//now get all the bytes - I'll reserve commentary on the weirdness of not having an overload for getAllBytes that takes a string
						byte[] bytes=Files.readAllBytes(Paths.get(binPath));
						audioIn=new AudioInputStream((new ByteArrayInputStream(bytes)),FORMAT,bytes.length/FORMAT.getFrameSize());
						//build the output file name
						int indexOf=fileName.lastIndexOf('.');
						if(indexOf>0){
							fileName=fileName.substring(0,indexOf);
						}
						String outPath=outputDir+fileName+".wav";
						AudioSystem.write(audioIn,AudioFileFormat.Type.WAVE,new File(outPath));
						audioIn.close();
					}
				}
			}
		}catch(Exception x){
			x.printStackTrace();
		}finally{
			//don't care about errors closing open files
			if(cueReader!=null){try{cueReader.close();}catch(Exception x){}};
			if(audioIn!=null){try{audioIn.close();}catch(Exception x){}};
		}
	}
}

Now let's see if one of the files converted correctly:



Raw data in .bin file vs .wav file



Alright, that looks good.

I'm not planning to post this to Github today. Someday I'll start a project of various random things like this I've done. Maybe I have by the time you're reading this. Follow the link to my Github page in the navigation bar to find out.

Update, this is is Github now at: https://github.com/huguesjohnson/DubbelLib/tree/main/java/src/com/huguesjohnson/dubbel/converters.

Now if you're planning to call this from some kind of batch script you'll want to add a couple commands like:


for f in *.wav; do ffmpeg -i "$f" -c:a libmp3lame -q:a 2 "${f/%wav/mp3}"; done
rm *.wav

The first one uses ffmpeg, which hopefully you have installed, to convert all the .wav files to .mp3. You can use .ogg instead if you're one of those types. The purpose of the second command I hope is obvious.

If you're feeling really fancy you can add id3tool to the mix.

After mp3 conversion everything still looks fine:



Raw data in .bin file vs .wav file vs .mp3 file



So this was fun and easier than expected. Look for a follow-up article called "Admitting you have a video game soundtrack hoarding problem" soon.



Related