/*
 * MpegAudioFileReader.
 *
 * 10/10/05 : size computation bug fixed in parseID3v2Frames.
 *            RIFF/MP3 header support added.
 *            FLAC and MAC headers throw UnsupportedAudioFileException now.
 *            "mp3.id3tag.publisher" (TPUB/TPB) added.
 *            "mp3.id3tag.orchestra" (TPE2/TP2) added.
 *            "mp3.id3tag.length" (TLEN/TLE) added.
 *
 * 08/15/05 : parseID3v2Frames improved.
 *
 * 12/31/04 : mp3spi.weak system property added to skip controls.
 *
 * 11/29/04 : ID3v2.2, v2.3 & v2.4 support improved.
 *            "mp3.id3tag.composer" (TCOM/TCM) added
 *            "mp3.id3tag.grouping" (TIT1/TT1) added
 *            "mp3.id3tag.disc" (TPA/TPOS) added
 *            "mp3.id3tag.encoded" (TEN/TENC) added
 *            "mp3.id3tag.v2.version" added
 *
 * 11/28/04 : String encoding bug fix in chopSubstring method.
 *
 * JavaZOOM : mp3spi@javazoom.net
 *        http://www.javazoom.net
 *
 *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */

// this file is included here because I needed to make a slight modification,
// namely taking out the cast to FileInputStream in the getAudioFileFormat 
// method. This needed to be removed because it was breaking tag parsing 
// when loading an mp3 file in an applet. I figure it's less hassle to 
// just use a modified copy in the main package than change it in 
// the mp3spi project and export a new JAR.
// This also enables me to easily add more tag parsing later.
package ddf.minim.javasound;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessControlException;
import java.util.HashMap;
import java.util.Map;

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

import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.Header;
import javazoom.spi.mpeg.sampled.file.IcyListener;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileFormat;
import javazoom.spi.mpeg.sampled.file.MpegAudioFormat;
import javazoom.spi.mpeg.sampled.file.MpegEncoding;
import javazoom.spi.mpeg.sampled.file.MpegFileFormatType;
import javazoom.spi.mpeg.sampled.file.tag.IcyInputStream;
import javazoom.spi.mpeg.sampled.file.tag.MP3Tag;

import org.tritonus.share.TDebug;
import org.tritonus.share.sampled.file.TAudioFileReader;

/**
 * This class implements AudioFileReader for MP3 SPI.
 */
class MpegAudioFileReader extends TAudioFileReader
{
	public static final String					VERSION					= "MP3SPI 1.9.4";
	
	// private final int SYNC = 0xFFE00000;
	private String									weak						= null;
	
	private final AudioFormat.Encoding[][]	sm_aEncodings			= {
			{ MpegEncoding.MPEG2L1, MpegEncoding.MPEG2L2, MpegEncoding.MPEG2L3 },
			{ MpegEncoding.MPEG1L1, MpegEncoding.MPEG1L2, MpegEncoding.MPEG1L3 },
			{ MpegEncoding.MPEG2DOT5L1, MpegEncoding.MPEG2DOT5L2,
			MpegEncoding.MPEG2DOT5L3 },									
	};
	
	public static final int						INITAL_READ_LENGTH	= 128000;
	private static final int					MARK_LIMIT				= INITAL_READ_LENGTH + 1;

	private static final String[]				id3v1genres				= { "Blues",
			"Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
			"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop",
			"R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative",
			"Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno",
			"Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance",
			"Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip",
			"Gospel", "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space",
			"Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic",
			"Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
			"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
			"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
			"Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
			"Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
			"Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
			"National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
			"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock",
			"Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock",
			"Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
			"Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony",
			"Booty Brass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club",
			"Tango", "Samba", "Folklore", "Ballad", "Power Ballad",
			"Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo",
			"A Capela", "Euro-House", "Dance Hall", "Goa", "Drum & Bass",
			"Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk",
			"Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal",
			"Black Metal", "Crossover", "Contemporary Christian",
			"Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime",
			"JPop", "SynthPop"												
	};
	
	private Map codeToPropName;
  
  protected JSMinim system;

	MpegAudioFileReader(JSMinim sys)
	{
		super(MARK_LIMIT, true);
    system = sys;
		if (TDebug.TraceAudioFileReader)
			TDebug.out(VERSION);
		try
		{
			weak = System.getProperty("mp3spi.weak");
		}
		catch (AccessControlException e)
		{
		}
		
		codeToPropName = new HashMap();
		// if we wanna parse a new tag, we just add it here.
		// ID3v2.2
		codeToPropName.put("TAL", "album");
		codeToPropName.put("TT2", "title");
		codeToPropName.put("TYE", "date");
		codeToPropName.put("TP1", "author");
		codeToPropName.put("TCR", "copyright");
		codeToPropName.put("COM", "comment");
		codeToPropName.put("TCO", "mp3.id3tag.genre");
		codeToPropName.put("TRK", "mp3.id3tag.track");
		codeToPropName.put("TPA", "mp3.id3tag.disc");
		codeToPropName.put("TCM", "mp3.id3tag.composer");
		codeToPropName.put("TT1", "mp3.id3tag.grouping");
		codeToPropName.put("TEN", "mp3.id3tag.encoded");
		codeToPropName.put("TPB", "mp3.id3tag.publisher");
		codeToPropName.put("TP2", "mp3.id3tag.orchestra");
		codeToPropName.put("TLE", "mp3.id3tag.length");
		// ID3v2.3 & ID3v2.4
		codeToPropName.put("TALB", "album");
		codeToPropName.put("TIT2", "title");
		codeToPropName.put("TYER", "date");
		codeToPropName.put("TDRC", "date");
		codeToPropName.put("TPE1", "author");
		codeToPropName.put("TCOP", "copyright");
		codeToPropName.put("WCOP", "copyright");
		codeToPropName.put("COMM", "comment");
		codeToPropName.put("TCON", "mp3.id3tag.genre");
		codeToPropName.put("TRCK", "mp3.id3tag.track");
		codeToPropName.put("TPOS", "mp3.id3tag.disc");
		codeToPropName.put("TCOM", "mp3.id3tag.composer");
		codeToPropName.put("TIT1", "mp3.id3tag.grouping");
		codeToPropName.put("TENC", "mp3.id3tag.encoded");
		codeToPropName.put("TPUB", "mp3.id3tag.publisher");
		codeToPropName.put("TPE2", "mp3.id3tag.orchestra");
		codeToPropName.put("TLEN", "mp3.id3tag.length");
	}

	/**
	 * Returns AudioFileFormat from File.
	 */
	public AudioFileFormat getAudioFileFormat(File file)
			throws UnsupportedAudioFileException, IOException
	{
		return super.getAudioFileFormat(file);
	}

	/**
	 * Returns AudioFileFormat from URL.
	 */
	public AudioFileFormat getAudioFileFormat(URL url)
			throws UnsupportedAudioFileException, IOException
	{
		if (TDebug.TraceAudioFileReader)
		{
			TDebug.out("MpegAudioFileReader.getAudioFileFormat(URL): begin");
		}
		long lFileLengthInBytes = AudioSystem.NOT_SPECIFIED;
		URLConnection conn = url.openConnection();
		// Tell shoucast server (if any) that SPI support shoutcast stream.
		conn.setRequestProperty("Icy-Metadata", "1");
		InputStream inputStream = conn.getInputStream();
		AudioFileFormat audioFileFormat = null;
		try
		{
			audioFileFormat = getAudioFileFormat(inputStream, lFileLengthInBytes);
		}
		finally
		{
			inputStream.close();
		}
		if (TDebug.TraceAudioFileReader)
		{
			TDebug.out("MpegAudioFileReader.getAudioFileFormat(URL): end");
		}
		return audioFileFormat;
	}

	/**
	 * Returns AudioFileFormat from inputstream and medialength.
	 */
	public AudioFileFormat getAudioFileFormat(InputStream inputStream, long mediaLength) 
     throws UnsupportedAudioFileException, IOException
	{
		system.debug("MpegAudioFileReader.getAudioFileFormat(InputStream inputStream, long mediaLength): begin");
		HashMap aff_properties = new HashMap();
		HashMap af_properties = new HashMap();
		int mLength = (int)mediaLength;
		int size = inputStream.available();
		PushbackInputStream pis = new PushbackInputStream(inputStream, MARK_LIMIT);
		byte head[] = new byte[22];
		pis.read(head);
		system.debug("InputStream : " + inputStream + " =>" + new String(head));

		// Check for WAV, AU, and AIFF, Ogg Vorbis, Flac, MAC file formats.
		// Next check for Shoutcast (supported) and OGG (unsupported) streams.
		if ((head[0] == 'R') && (head[1] == 'I') && (head[2] == 'F')
				&& (head[3] == 'F') && (head[8] == 'W') && (head[9] == 'A')
				&& (head[10] == 'V') && (head[11] == 'E'))
		{
			system.debug("RIFF/WAV stream found");
			int isPCM = ((head[21] << 8) & 0x0000FF00) | ((head[20]) & 0x00000FF);
			if (weak == null)
			{
				if (isPCM == 1)
					throw new UnsupportedAudioFileException("WAV PCM stream found");
			}

		}
		else if ((head[0] == '.') && (head[1] == 's') && (head[2] == 'n')
				&& (head[3] == 'd'))
		{
		  system.debug("AU stream found");
			if (weak == null)
				throw new UnsupportedAudioFileException("AU stream found");
		}
		else if ((head[0] == 'F') && (head[1] == 'O') && (head[2] == 'R')
				&& (head[3] == 'M') && (head[8] == 'A') && (head[9] == 'I')
				&& (head[10] == 'F') && (head[11] == 'F'))
		{
			system.debug("AIFF stream found");
			if (weak == null)
				throw new UnsupportedAudioFileException("AIFF stream found");
		}
		else if (((head[0] == 'M') | (head[0] == 'm'))
				&& ((head[1] == 'A') | (head[1] == 'a'))
				&& ((head[2] == 'C') | (head[2] == 'c')))
		{
			system.debug("APE stream found");
			if (weak == null)
				throw new UnsupportedAudioFileException("APE stream found");
		}
		else if (((head[0] == 'F') | (head[0] == 'f'))
				&& ((head[1] == 'L') | (head[1] == 'l'))
				&& ((head[2] == 'A') | (head[2] == 'a'))
				&& ((head[3] == 'C') | (head[3] == 'c')))
		{
			system.debug("FLAC stream found");
			if (weak == null)
				throw new UnsupportedAudioFileException("FLAC stream found");
		}
		// Shoutcast stream ?
		else if (((head[0] == 'I') | (head[0] == 'i'))
				&& ((head[1] == 'C') | (head[1] == 'c'))
				&& ((head[2] == 'Y') | (head[2] == 'y')))
		{
			pis.unread(head);
			// Load shoutcast meta data.
			loadShoutcastInfo(pis, aff_properties);
		}
		// Ogg stream ?
		else if (((head[0] == 'O') | (head[0] == 'o'))
				&& ((head[1] == 'G') | (head[1] == 'g'))
				&& ((head[2] == 'G') | (head[2] == 'g')))
		{
			system.debug("Ogg stream found");
			if (weak == null)
				throw new UnsupportedAudioFileException("Ogg stream found");
		}
		// No, so pushback.
		else
		{
			pis.unread(head);
		}
		// MPEG header info.
		int nVersion = AudioSystem.NOT_SPECIFIED;
		int nLayer = AudioSystem.NOT_SPECIFIED;
		// int nSFIndex = AudioSystem.NOT_SPECIFIED;
		int nMode = AudioSystem.NOT_SPECIFIED;
		int FrameSize = AudioSystem.NOT_SPECIFIED;
		// int nFrameSize = AudioSystem.NOT_SPECIFIED;
		int nFrequency = AudioSystem.NOT_SPECIFIED;
		int nTotalFrames = AudioSystem.NOT_SPECIFIED;
		float FrameRate = AudioSystem.NOT_SPECIFIED;
		int BitRate = AudioSystem.NOT_SPECIFIED;
		int nChannels = AudioSystem.NOT_SPECIFIED;
		int nHeader = AudioSystem.NOT_SPECIFIED;
		int nTotalMS = AudioSystem.NOT_SPECIFIED;
		boolean nVBR = false;
		AudioFormat.Encoding encoding = null;
		try
		{
			Bitstream m_bitstream = new Bitstream(pis);
			aff_properties.put("mp3.header.pos",
										new Integer(m_bitstream.header_pos()));
			Header m_header = m_bitstream.readFrame();
			// nVersion = 0 => MPEG2-LSF (Including MPEG2.5), nVersion = 1 => MPEG1
			nVersion = m_header.version();
			if (nVersion == 2)
				aff_properties.put("mp3.version.mpeg", Float.toString(2.5f));
			else
				aff_properties.put("mp3.version.mpeg",
											Integer.toString(2 - nVersion));
			// nLayer = 1,2,3
			nLayer = m_header.layer();
			aff_properties.put("mp3.version.layer", Integer.toString(nLayer));
			// nSFIndex = m_header.sample_frequency();
			nMode = m_header.mode();
			aff_properties.put("mp3.mode", new Integer(nMode));
			nChannels = nMode == 3 ? 1 : 2;
			aff_properties.put("mp3.channels", new Integer(nChannels));
			nVBR = m_header.vbr();
			af_properties.put("vbr", new Boolean(nVBR));
			aff_properties.put("mp3.vbr", new Boolean(nVBR));
			aff_properties.put("mp3.vbr.scale", new Integer(m_header.vbr_scale()));
			FrameSize = m_header.calculate_framesize();
			aff_properties.put("mp3.framesize.bytes", new Integer(FrameSize));
			if (FrameSize < 0)
      {
				throw new UnsupportedAudioFileException("Invalid FrameSize : " + FrameSize);
      }
			nFrequency = m_header.frequency();
			aff_properties.put("mp3.frequency.hz", new Integer(nFrequency));
			FrameRate = (float)((1.0 / (m_header.ms_per_frame())) * 1000.0);
			aff_properties.put("mp3.framerate.fps", new Float(FrameRate));
			if (FrameRate < 0)
      {
				throw new UnsupportedAudioFileException("Invalid FrameRate : " + FrameRate);
      }
			if (mLength != AudioSystem.NOT_SPECIFIED)
			{
				aff_properties.put("mp3.length.bytes", new Integer(mLength));
				nTotalFrames = m_header.max_number_of_frames(mLength);
				aff_properties.put("mp3.length.frames", new Integer(nTotalFrames));
			}
			BitRate = m_header.bitrate();
			af_properties.put("bitrate", new Integer(BitRate));
			aff_properties.put("mp3.bitrate.nominal.bps", new Integer(BitRate));
			nHeader = m_header.getSyncHeader();
			encoding = sm_aEncodings[nVersion][nLayer - 1];
			aff_properties.put("mp3.version.encoding", encoding.toString());
			if (mLength != AudioSystem.NOT_SPECIFIED)
			{
				nTotalMS = Math.round(m_header.total_ms(mLength));
				aff_properties.put("duration", new Long((long)nTotalMS * 1000L));
			}
			aff_properties.put("mp3.copyright", new Boolean(m_header.copyright()));
			aff_properties.put("mp3.original", new Boolean(m_header.original()));
			aff_properties.put("mp3.crc", new Boolean(m_header.checksums()));
			aff_properties.put("mp3.padding", new Boolean(m_header.padding()));
			InputStream id3v2 = m_bitstream.getRawID3v2();
			if (id3v2 != null)
			{
				aff_properties.put("mp3.id3tag.v2", id3v2);
				parseID3v2Frames(id3v2, aff_properties);
			}
			if (TDebug.TraceAudioFileReader)
				TDebug.out(m_header.toString());
		}
		catch (Exception e)
		{
			system.debug("not a MPEG stream:" + e.getMessage());
			throw new UnsupportedAudioFileException("not a MPEG stream:"
					+ e.getMessage());
		}
		// Deeper checks ?
		int cVersion = (nHeader >> 19) & 0x3;
		if (cVersion == 1)
		{
			system.debug("not a MPEG stream: wrong version");
			throw new UnsupportedAudioFileException(
																	"not a MPEG stream: wrong version");
		}
		int cSFIndex = (nHeader >> 10) & 0x3;
		if (cSFIndex == 3)
		{
			system.debug("not a MPEG stream: wrong sampling rate");
			throw new UnsupportedAudioFileException(
																	"not a MPEG stream: wrong sampling rate");
		}
		// Look up for ID3v1 tag
		if ((size == mediaLength) && (mediaLength != AudioSystem.NOT_SPECIFIED))
		{
			// FileInputStream fis = (FileInputStream) inputStream;
			byte[] id3v1 = new byte[128];
			int toSkip = inputStream.available() - id3v1.length;
			if (toSkip > 0)
			{
				inputStream.skip(inputStream.available() - id3v1.length);
			}
			inputStream.read(id3v1, 0, id3v1.length);
			if ((id3v1[0] == 'T') && (id3v1[1] == 'A') && (id3v1[2] == 'G'))
			{
				parseID3v1Frames(id3v1, aff_properties);
			}
		}
		AudioFormat format = new MpegAudioFormat(encoding, (float)nFrequency,
																AudioSystem.NOT_SPECIFIED // SampleSizeInBits
																									// -
																									// The
																									// size
																									// of a
																									// sample
																, nChannels // Channels - The
																				// number of
																				// channels
																, -1 // The number of bytes in
																		// each frame
																, FrameRate // FrameRate - The
																				// number of frames
																				// played or
																				// recorded per
																				// second
																, true, af_properties);
		return new MpegAudioFileFormat(MpegFileFormatType.MP3, format,
													nTotalFrames, mLength, aff_properties);
	}

	/**
	 * Returns AudioInputStream from file.
	 */
	public AudioInputStream getAudioInputStream(File file)
			throws UnsupportedAudioFileException, IOException
	{
		if (TDebug.TraceAudioFileReader)
			TDebug.out("getAudioInputStream(File file)");
		InputStream inputStream = new FileInputStream(file);
		try
		{
			return getAudioInputStream(inputStream);
		}
		catch (UnsupportedAudioFileException e)
		{
			if (inputStream != null)
				inputStream.close();
			throw e;
		}
		catch (IOException e)
		{
			if (inputStream != null)
				inputStream.close();
			throw e;
		}
	}

	/**
	 * Returns AudioInputStream from url.
	 */
	public AudioInputStream getAudioInputStream(URL url)
			throws UnsupportedAudioFileException, IOException
	{
		system.debug("MpegAudioFileReader.getAudioInputStream(URL): begin");
		long lFileLengthInBytes = AudioSystem.NOT_SPECIFIED;
		URLConnection conn = url.openConnection();
		// Tell shoucast server (if any) that SPI support shoutcast stream.
		boolean isShout = false;
		int toRead = 4;
		byte[] head = new byte[toRead];
		conn.setRequestProperty("Icy-Metadata", "1");
		BufferedInputStream bInputStream = new BufferedInputStream(
																						conn.getInputStream());
		bInputStream.mark(toRead);
		int read = bInputStream.read(head, 0, toRead);
		if ((read > 2)
				&& (((head[0] == 'I') | (head[0] == 'i'))
						&& ((head[1] == 'C') | (head[1] == 'c')) && ((head[2] == 'Y') | (head[2] == 'y'))))
			isShout = true;
		bInputStream.reset();
		InputStream inputStream = null;
		// Is is a shoutcast server ?
		if (isShout == true)
		{
			// Yes
			IcyInputStream icyStream = new IcyInputStream(bInputStream);
			icyStream.addTagParseListener(IcyListener.getInstance());
			inputStream = icyStream;
		}
		else
		{
			// No, is Icecast 2 ?
			String metaint = conn.getHeaderField("icy-metaint");
			if (metaint != null)
			{
				// Yes, it might be icecast 2 mp3 stream.
				IcyInputStream icyStream = new IcyInputStream(bInputStream, metaint);
				icyStream.addTagParseListener(IcyListener.getInstance());
				inputStream = icyStream;
			}
			else
			{
				// No
				inputStream = bInputStream;
			}
		}
		AudioInputStream audioInputStream = null;
		try
		{
			audioInputStream = getAudioInputStream(inputStream, lFileLengthInBytes);
		}
		catch (UnsupportedAudioFileException e)
		{
			inputStream.close();
			throw e;
		}
		catch (IOException e)
		{
			inputStream.close();
			throw e;
		}
		system.debug("MpegAudioFileReader.getAudioInputStream(URL): end");
		return audioInputStream;
	}

	/**
	 * Return the AudioInputStream from the given InputStream.
	 */
	public AudioInputStream getAudioInputStream(InputStream inputStream)
			throws UnsupportedAudioFileException, IOException
	{
		system.debug("MpegAudioFileReader.getAudioInputStream(InputStream inputStream)");
		if (!inputStream.markSupported())
			inputStream = new BufferedInputStream(inputStream);
		return super.getAudioInputStream(inputStream);
	}

	/**
	 * Parser ID3v1 frames
	 * 
	 * @param frames
	 * @param props
	 */
	protected void parseID3v1Frames(byte[] frames, HashMap props)
	{
		if (TDebug.TraceAudioFileReader)
			TDebug.out("Parsing ID3v1");
		String tag = null;
		try
		{
			tag = new String(frames, 0, frames.length, "ISO-8859-1");
		}
		catch (UnsupportedEncodingException e)
		{
			tag = new String(frames, 0, frames.length);
			if (TDebug.TraceAudioFileReader)
				TDebug.out("Cannot use ISO-8859-1");
		}
		if (TDebug.TraceAudioFileReader)
    {
			TDebug.out("ID3v1 frame dump='" + tag + "'");
    }
		int start = 3;
		String titlev1 = chopSubstring(tag, start, start += 30);
		String titlev2 = (String)props.get("title");
		if (((titlev2 == null) || (titlev2.length() == 0)) && (titlev1 != null))
    {
			props.put("title", titlev1);
    }
		String artistv1 = chopSubstring(tag, start, start += 30);
		String artistv2 = (String)props.get("author");
		if (((artistv2 == null) || (artistv2.length() == 0)) && (artistv1 != null))
    {  
			props.put("author", artistv1);
    }
		String albumv1 = chopSubstring(tag, start, start += 30);
		String albumv2 = (String)props.get("album");
		if (((albumv2 == null) || (albumv2.length() == 0)) && (albumv1 != null))
    {
			props.put("album", albumv1);
    }
		String yearv1 = chopSubstring(tag, start, start += 4);
		String yearv2 = (String)props.get("year");
		if (((yearv2 == null) || (yearv2.length() == 0)) && (yearv1 != null))
    {
			props.put("date", yearv1);
    }
		String commentv1 = chopSubstring(tag, start, start += 28);
		String commentv2 = (String)props.get("comment");
		if (((commentv2 == null) || (commentv2.length() == 0)) && (commentv1 != null))
    {
			props.put("comment", commentv1);
    }
		String trackv1 = "" + ((int)(frames[126] & 0xff));
		String trackv2 = (String)props.get("mp3.id3tag.track");
		if (((trackv2 == null) || (trackv2.length() == 0)) && (trackv1 != null))
		{
      props.put("mp3.id3tag.track", trackv1);
    }
		int genrev1 = (int)(frames[127] & 0xff);
		if ((genrev1 >= 0) && (genrev1 < id3v1genres.length))
		{
			String genrev2 = (String)props.get("mp3.id3tag.genre");
			if (((genrev2 == null) || (genrev2.length() == 0)))
			{
        props.put("mp3.id3tag.genre", id3v1genres[genrev1]);
      }
		}
		if (TDebug.TraceAudioFileReader)
		{
      TDebug.out("ID3v1 parsed");
    }
	}

	/**
	 * Extract
	 * 
	 * @param s
	 * @param start
	 * @param end
	 * @return
	 */
	private String chopSubstring(String s, int start, int end)
	{
		String str = null;
		// 11/28/04 - String encoding bug fix.
		try
		{
			str = s.substring(start, end);
			int loc = str.indexOf('\0');
			if (loc != -1)
			{
				str = str.substring(0, loc);
			}
		}
		catch (StringIndexOutOfBoundsException e)
		{
			// Skip encoding issues.
		  system.error("Cannot chopSubString " + e.getMessage());
		}
		return str;
	}

	/**
	 * Parse ID3v2 frames to add album (TALB), title (TIT2), date (TYER), author
	 * (TPE1), copyright (TCOP), comment (COMM) ...
	 * 
	 * @param frames
	 * @param props
	 */
	protected void parseID3v2Frames(InputStream frames, HashMap props)
	{
		byte[] bframes = null;
		int size = -1;
		try
		{
			size = frames.available();
			bframes = new byte[size];
			frames.mark(size);
			frames.read(bframes);
			frames.reset();
		}
		catch (IOException e)
		{
			system.error("Cannot parse ID3v2 :" + e.getMessage());
		}
		if (!"ID3".equals(new String(bframes, 0, 3)))
		{
			system.error("No ID3v2 header found!");
			return;
		}
		int v2version = (int)(bframes[3] & 0xFF);
		props.put("mp3.id3tag.v2.version", String.valueOf(v2version));
		if (v2version < 2 || v2version > 4)
		{
			system.error("Unsupported ID3v2 version " + v2version + "!");
			return;
		}
		try
		{
			system.debug("ID3v2 frame dump='" + new String(bframes, 0, bframes.length) + "'");
			/*
			 * ID3 tags :
			 * http://www.unixgods.org/~tilo/ID3/docs/ID3_comparison.html
			 */
			String value = null;
			for (int i = 10; i < bframes.length && bframes[i] > 0; i += size)
			{
				if (v2version == 3 || v2version == 4)
				{
					// ID3v2.3 & ID3v2.4
					String code = new String(bframes, i, 4);
					// build the size of the frame from the four size bytes
					size = (int)((bframes[i + 4] << 24) & 0xFF000000
							| (bframes[i + 5] << 16) & 0x00FF0000
							| (bframes[i + 6] << 8) & 0x0000FF00 | (bframes[i + 7]) & 0x000000FF);
					// inc i by 10 because the id3 frame header size is 10 bytes
					i += 10;
					if ( !codeToPropName.containsKey(code) )
					{
						system.error("Don't know the ID3 code " + code);
						continue;
					}
					if ( code.equals("COMM") )
					{
						value = parseText(bframes, i, size, 5);
					}
					else if ( code.startsWith("W") )
					{
						// W codes (URLs), don't have an encoding value
						// so we don't need to skip anything when parsing
						value = parseText(bframes, i, size, 0);
					}
					else
					{
						value = parseText(bframes, i, size, 1);
					}
					if (value == null)
					{
						value = "";
					}
					String propName = (String)codeToPropName.get(code);
					props.put(propName, value);
				}
				else
				{
					// ID3v2.2
					String scode = new String(bframes, i, 3);
					size = (int)(0x00000000) + (bframes[i + 3] << 16)
							+ (bframes[i + 4] << 8) + (bframes[i + 5]);
					i += 6;
					if ( !codeToPropName.containsKey(scode) )
					{
						system.error("Don't know the ID3 code " + scode);
						continue;
					}
					if (scode.equals("COM"))
					{
						value = parseText(bframes, i, size, 5);
					}
					else
					{
						value = parseText(bframes, i, size, 1);
					}
					if ( value == null )
					{
						value = "";
					}
					String propName = (String)codeToPropName.get(scode);
					props.put(propName, value);
				}
			}
		}
		catch (RuntimeException e)
		{
			// Ignore all parsing errors.
			system.error("Error parsing ID3v2: " + e.getMessage());
		}
		system.debug("ID3v2 parsed");
	}
	
	private static String[] ENC_TYPES = { "ISO-8859-1", "UTF16", "UTF-16BE", "UTF-8" };

	/**
	 * Parse Text Frames.
	 * 
	 * @param bframes
	 * @param offset
	 * @param size
	 * @param skip
	 * @return
	 */
	protected String parseText(byte[] bframes, int offset, int size, int skip)
	{
		String value = null;
		try
		{
			String enc = ENC_TYPES[0];
			if ( bframes[offset] >= 0 && bframes[offset] < 4 )
			{
				enc = ENC_TYPES[bframes[offset]];
			}
			value = new String(bframes, offset + skip, size - skip, enc);
			value = chopSubstring(value, 0, value.length());
		}
		catch (UnsupportedEncodingException e)
		{
			system.error("ID3v2 Encoding error: " + e.getMessage());
		}
		return value;
	}

	/**
	 * Load shoutcast (ICY) info.
	 * 
	 * @param input
	 * @param props
	 * @throws IOException
	 */
	protected void loadShoutcastInfo(InputStream input, HashMap props)
			throws IOException
	{
		IcyInputStream icy = new IcyInputStream(new BufferedInputStream(input));
		// HashMap metadata = icy.getTagHash();
		MP3Tag titleMP3Tag = icy.getTag("icy-name");
		if (titleMP3Tag != null)
			props.put("title", ((String)titleMP3Tag.getValue()).trim());
		MP3Tag[] meta = icy.getTags();
		if (meta != null)
		{
			// StringBuffer metaStr = new StringBuffer();
			for (int i = 0; i < meta.length; i++)
			{
				String key = meta[i].getName();
				String value = ((String)icy.getTag(key).getValue()).trim();
				props.put("mp3.shoutcast.metadata." + key, value);
			}
		}
	}
}