/*
 *	Mp3Encoder.java
 *
 *	This file is part of jsresources.org
 */

/*
 * Copyright (c) 2000 by Florian Bomers
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
|<---            this code is formatted to fit into 80 columns             --->|
*/

import java.io.File;
import java.io.IOException;

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



/**	<titleabbrev>Mp3Encoder</titleabbrev>
	<title>Encoding an audio file to mp3</title>

	<formalpara><title>Purpose</title>
	<para>Encodes one or more
	PCM audio files, writes the results as mp3 files.</para>
	</formalpara>

	<formalpara>
	<title>Usage</title>
	<para>
	<cmdsynopsis>
	<command>java Mp3Encoder</command>
	<arg><option>-q <replaceable>quality</replaceable></option></arg>
	<arg><option>-b <replaceable>bitrate</replaceable></option></arg>
	<arg><option>-V</option></arg>
	<arg><option>-v</option></arg>
	<arg><option>-s</option></arg>
	<arg><option>-e</option></arg>
	<arg><option>-t</option></arg>
	<arg rep="repeat" choice="plain"><replaceable class="parameter">pcmfile</replaceable></arg>
	</cmdsynopsis>
	</para>
	</formalpara>

	<formalpara><title>Parameters</title>
	<variablelist>
	<varlistentry>
	<term><option>-q <replaceable>quality</replaceable></option></term>
	<listitem><para>Quality of output mp3 file. In VBR mode, this affects
              the size of the mp3 file. (Default middle)
              One of: lowest, low, middle, high, highest.</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option>-b <replaceable>bitrate</replaceable></option></term>
	<listitem><para>Bitrate in KBit/s. Useless in VBR mode. (Default 128)
              One of: 32 40 48 56 64 80 96 112 128 160 192 224 256 320 (MPEG1)
              Or: 8 16 24 32 40 48 56 64 80 96 112 128 144 160 (MPEG2 and MPEG2.5).</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option>-V</option></term>
	<listitem><para>VBR (variable bit rate) mode.
	Slower, but potentially better
	quality. (Default off)</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option>-v</option></term>
	<listitem><para>Be verbose.</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option>-s</option></term>
	<listitem><para>Be silent.</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option>-e</option></term>
	<listitem><para>Debugging: Dump stack trace of exceptions.</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option>-t</option></term>
	<listitem><para>Debugging: trace execution of converters.</para></listitem>
	</varlistentry>
	<varlistentry>
	<term><option><replaceable class="parameter">pcmfile</replaceable></option></term>
	<listitem><para>the name(s) of PCM input file(s).
	The output file(s) will be named after the input file(s)
	with the extension changed to 
	<filename>.mp3</filename>.</para></listitem>
	</varlistentry>
	</variablelist>
	</formalpara>

	<formalpara><title>Bugs, limitations</title>
	<para>
	To work cleanly, this program requires JDK 1.5.0 or the latest version of Tritonus.
	You have to download and install the
	<ulink url="http://sourceforge.net/projects/lame/">LAME</ulink>
	mp3 encoder and the
	<ulink url="http://www.tritonus.org/plugins.html">Tritonus
	mp3enc plug-in</ulink>.</para>
	</formalpara>
	<para>Too many options...</para>

	<formalpara><title>Source code</title>
	<para>
	<ulink url="Mp3Encoder.java.html">Mp3Encoder.java</ulink>,
	<ulink url="http://sourceforge.net/projects/lame/">LAME</ulink>,
	<ulink url="http://www.tritonus.org/">Tritonus</ulink>
	</para>
	</formalpara>

*/
public class Mp3Encoder {
	private static boolean DEBUG = false;
	private static boolean dumpExceptions=false;
	private static boolean traceConverters=false;
	private static boolean quiet=false;

	private static final AudioFormat.Encoding	MPEG1L3 = new AudioFormat.Encoding("MPEG1L3");
	private static final AudioFileFormat.Type	MP3 = new AudioFileFormat.Type("MP3", "mp3");



	private static AudioInputStream getInStream(String filename)
			throws IOException {
		File	file = new File(filename);
		AudioInputStream	ais = null;
		try {
			ais = AudioSystem.getAudioInputStream(file);
		} catch (Exception e) {
			if (dumpExceptions) {
				e.printStackTrace();
			} else if (!quiet) {
				System.out.println("Error: " + e.getMessage());
			}
		}
		if (ais == null) {
			throw new IOException("Cannot open \"" + filename + "\"");
		}
		return ais;
	}



	public static String stripExtension(String filename) {
		int	ind = filename.lastIndexOf(".");
		if (ind == -1
		        || ind == filename.length()
		        || filename.lastIndexOf(File.separator) > ind) {
			// when dot is at last position,
			// or a slash is after the dot, there isn't an extension
			return filename;
		}
		return filename.substring(0, ind);
	}



	/* first version. Remains here for documentation how to
	 * get a stream with complete description of the target format.
	 */
	public static AudioInputStream getConvertedStream2(
	    	AudioInputStream sourceStream,
	    	AudioFormat.Encoding targetEncoding)
			throws Exception {
		AudioFormat sourceFormat = sourceStream.getFormat();
		if (!quiet) {
			System.out.println("Input format: " + sourceFormat);
		}
		// build the output format
		AudioFormat targetFormat = new AudioFormat(
		                               targetEncoding,
		                               sourceFormat.getSampleRate(),
		                               AudioSystem.NOT_SPECIFIED,
		                               sourceFormat.getChannels(),
		                               AudioSystem.NOT_SPECIFIED,
		                               AudioSystem.NOT_SPECIFIED,
		                               false); // endianness doesn't matter
		// construct a converted stream
		AudioInputStream targetStream = null;
		if (!AudioSystem.isConversionSupported(targetFormat, sourceFormat)) {
			if (DEBUG && !quiet) {
				System.out.println("Direct conversion not possible.");
				System.out.println("Trying with intermediate PCM format.");
			}
			AudioFormat intermediateFormat = new AudioFormat(
			                                     AudioFormat.Encoding.PCM_SIGNED,
			                                     sourceFormat.getSampleRate(),
			                                     16,
			                                     sourceFormat.getChannels(),
			                                     2 * sourceFormat.getChannels(), // frameSize
			                                     sourceFormat.getSampleRate(),
			                                     false);
			if (AudioSystem.isConversionSupported(intermediateFormat, sourceFormat)) {
				// intermediate conversion is supported
				sourceStream = AudioSystem.getAudioInputStream(intermediateFormat, sourceStream);
			}
		}
		targetStream = AudioSystem.getAudioInputStream(targetFormat, sourceStream);
		if (targetStream == null) {
			throw new Exception("conversion not supported");
		}
		if (!quiet) {
			if (DEBUG) {
				System.out.println("Got converted AudioInputStream: " + targetStream.getClass().getName());
			}
			System.out.println("Output format: " + targetStream.getFormat());
		}
		return targetStream;
	}



	public static AudioInputStream getConvertedStream(
	    	AudioInputStream sourceStream,
	    	AudioFormat.Encoding targetEncoding)
			throws Exception {
		AudioFormat sourceFormat = sourceStream.getFormat();
		if (!quiet) {
			System.out.println("Input format: " + sourceFormat);
		}

		// construct a converted stream
		AudioInputStream targetStream = null;
		if (!AudioSystem.isConversionSupported(targetEncoding, sourceFormat)) {
			if (DEBUG && !quiet) {
				System.out.println("Direct conversion not possible.");
				System.out.println("Trying with intermediate PCM format.");
			}
			AudioFormat intermediateFormat = new AudioFormat(
			                                     AudioFormat.Encoding.PCM_SIGNED,
			                                     sourceFormat.getSampleRate(),
			                                     16,
			                                     sourceFormat.getChannels(),
			                                     2 * sourceFormat.getChannels(), // frameSize
			                                     sourceFormat.getSampleRate(),
			                                     false);
			if (AudioSystem.isConversionSupported(intermediateFormat, sourceFormat)) {
				// intermediate conversion is supported
				sourceStream = AudioSystem.getAudioInputStream(intermediateFormat, sourceStream);
			}
		}
		targetStream = AudioSystem.getAudioInputStream(targetEncoding, sourceStream);
		if (targetStream == null) {
			throw new Exception("conversion not supported");
		}
		if (!quiet) {
			if (DEBUG) {
				System.out.println("Got converted AudioInputStream: " + targetStream.getClass().getName());
			}
			System.out.println("Output format: " + targetStream.getFormat());
		}
		return targetStream;
	}



	public static int writeFile(String inFilename) {
		int writtenBytes = -1;
		try {
			AudioFileFormat.Type targetType = MP3;
			AudioInputStream ais = getInStream(inFilename);
			ais = getConvertedStream(ais, MPEG1L3);

			// construct the target filename
			String outFilename = stripExtension(inFilename) + "." + targetType.getExtension();

			// write the file
			if (!quiet) {
				System.out.println("Writing " + outFilename + "...");
			}
			writtenBytes = AudioSystem.write(ais, targetType, new File(outFilename));
			if (DEBUG && !quiet) {
				System.out.println("Effective parameters of output file:");
				try {
					String version=System.getProperty("tritonus.lame.encoder.version", "");
					if (version!="") {
						System.out.println("  Version      = "+version);
					}
					System.out.println("  Quality      = "+System.getProperty
					                   ("tritonus.lame.effective.quality", "<none>"));
					System.out.println("  Bitrate      = "+System.getProperty
					                   ("tritonus.lame.effective.bitrate", "<none>"));
					System.out.println("  Channel Mode = "+System.getProperty
					                   ("tritonus.lame.effective.chmode", "<none>"));
					System.out.println("  VBR mode     = "+System.getProperty
					                   ("tritonus.lame.effective.vbr", "<none>"));
					System.out.println("  Sample rate  = "+System.getProperty
					                   ("tritonus.lame.effective.samplerate", "<none>"));
					System.out.println("  Encoding     = "+System.getProperty
					                   ("tritonus.lame.effective.encoding", "<none>"));
				} catch (Throwable t1) {}
			}
		} catch (Throwable t) {
			if (dumpExceptions) {
				t.printStackTrace();
			} else if (!quiet) {
				System.out.println("Error: " + t.getMessage());
			}
		}
		return writtenBytes;
	}



	// returns the first index in args where the files start
	public static int parseArgs(String[] args) {
		if (args.length == 0) {
			usage();
		}
		// parse options
		try {
			for (int i = 0; i < args.length; i++) {
				String arg = args[i];
				if (arg.equals("--help")) {
					usage();
				}
				if (arg.length() > 3 || arg.length() < 2 || !arg.startsWith("-")) {
					return i;
				}
				char cArg = arg.charAt(1);
				// options without parameter
				if (cArg == 'v') {
					DEBUG=true;
					continue;
				} else if (cArg == 'e') {
					dumpExceptions=true;
					continue;
				} else if (cArg == 't') {
					org.tritonus.share.TDebug.TraceAudioConverter=true;
					continue;
				} else if (cArg == 's') {
					quiet=true;
					continue;
				} else if (cArg == 'V') {
					try {
						System.setProperty("tritonus.lame.vbr", "true");
					} catch (Throwable t1) {}
					continue;
				} else if (cArg == 'h') {
					usage();
				}
				// options with parameter
				if (args.length < i + 2) {
					throw new Exception("Missing parameter or unrecognized option "+arg+".");
				}
				String param = args[i + 1];
				i++;
				switch (cArg) {
				case 'q':
					try {
						System.setProperty("tritonus.lame.quality", param);
					} catch (Throwable t2) {}
					break;
				case 'b':
					try {
						System.setProperty("tritonus.lame.bitrate", param);
					} catch (Throwable t3) {}
					break;
				default:
					throw new Exception("Unrecognized option "+arg+".");
				}
			}
			throw new Exception("No input file(s) are given.");
		} catch (Exception e) {
			System.err.println(e.getMessage());
			System.exit(1);
		}
		return 0; // statement not reached
	}



	public static void main(String[] args) {
		//try {
		//	System.out.println("Librarypath=" + System.getProperty("java.library.path", ""));
		//} catch (Throwable t) {}

		int firstFileIndex = parseArgs(args);
		int inputFiles = 0;
		int success = 0;
		long totalTime = System.currentTimeMillis();
		for (int i = firstFileIndex; i < args.length; i++) {
			long time = System.currentTimeMillis();
			int bytes = writeFile(args[i]);
			time = System.currentTimeMillis()-time;
			inputFiles++;
			if (bytes >= 0) {
				if (bytes > 0) {
					success++;
				}
				if (!quiet) {
					System.out.println("Wrote " + bytes + " bytes in "
					                   + (time / 60000) + "m " + ((time/1000) % 60) + "s "
					                   + (time % 1000) + "ms ("
					                   + (time/1000) + "s).");
				}
			}
		}
		totalTime = System.currentTimeMillis() - totalTime;
		if ((DEBUG && quiet) || !quiet) {
			// this IS displayed in silent DEBUG mode
			System.out.println("From " + inputFiles + " input file" + (inputFiles == 1 ? "" : "s") + ", "
			                   + success + " file" + (success == 1 ? " was" : "s were") + " converted successfully in "
			                   + (totalTime / 60000) + "m " + ((totalTime/1000) % 60) + "s  ("
			                   + (totalTime/1000) + "s).");
		}
		System.exit(0);
	}



	/**	Display a message of how to call this program.
	 */
	public static void usage() {
		System.out.println("Mp3Encoder - convert audio files to mp3 (layer III of MPEG 1, MPEG 2 or MPEG 2.5");
		System.out.println("java Mp3Encoder <options> <source file> [<source file>...]");
		System.out.println("The output file(s) will be named like the source file(s) but");
		System.out.println("with mp3 file extension.");
		System.out.println("");
		System.out.println("You need LAME 3.88 or later. Get it from http://sourceforge.net/projects/lame/");
		System.out.println("");
		System.out.println("<options> may be a combination of the following:");
		System.out.println("-q <quality>  Quality of output mp3 file. In VBR mode, this affects");
		System.out.println("              the size of the mp3 file. (Default middle)");
		System.out.println("              One of: lowest, low, middle, high, highest");
		System.out.println("-b <bitrate>  Bitrate in KBit/s. Useless in VBR mode. (Default 128)");
		System.out.println("              One of: 32 40 48 56 64 80 96 112 128 160 192 224 256 320 (MPEG1)");
		System.out.println("              Or: 8 16 24 32 40 48 56 64 80 96 112 128 144 160 (MPEG2 and MPEG2.5");
		System.out.println("-V            VBR (variable bit rate) mode. Slower, but potentially better");
		System.out.println("              quality. (Default off)");
		System.out.println("-v            Be verbose.");
		System.out.println("-s            Be silent.");
		System.out.println("-e            Debugging: Dump stack trace of exceptions.");
		System.out.println("-t            Debugging: trace execution of converters.");
		System.out.println("-h | --help   Show this message.");
		System.exit(1);
	}

}



/*** Mp3Encoder.java ***/