/* * MpegAudioFileReader. * * 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. *---------------------------------------------------------------------- */ package javazoom.spi.mpeg.sampled.file; 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.util.HashMap; 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.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. */ public class MpegAudioFileReader extends TAudioFileReader { private final int SYNC = 0xFFE00000; private final AudioFormat.Encoding[][] sm_aEncodings = { {MpegEncoding.MPEG2L1, MpegEncoding.MPEG2L2, MpegEncoding.MPEG2L3}, {MpegEncoding.MPEG1L1, MpegEncoding.MPEG1L2, MpegEncoding.MPEG1L3}, {MpegEncoding.MPEG2DOT5L1, MpegEncoding.MPEG2DOT5L2, MpegEncoding.MPEG2DOT5L3}, }; private static final int INITAL_READ_LENGTH = 64000; 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" }; public MpegAudioFileReader() { super(MARK_LIMIT, true); if (TDebug.TraceAudioFileReader) TDebug.out(">MpegAudioFileReader()"); } /** * Returns AudioFileFormat from File. */ @Override public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException { return super.getAudioFileFormat(file); } /** * Returns AudioFileFormat from URL. */ @Override 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 { if (TDebug.TraceAudioFileReader) TDebug.out(">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[12]; pis.read(head); if (TDebug.TraceAudioFileReader) { TDebug.out("InputStream : "+inputStream + " =>" + new String(head)); } /* * Check for WAV, AU, and AIFF file formats. * * Next check for Shoutcast (supported) and OGG (unsupported) streams. * * Note -- the check for WAV files will reject Broadcast WAV files. * This may be incorrect as broadcast WAV files may contain MPEG data. * Need to investigate. * */ 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')) { if (TDebug.TraceAudioFileReader) TDebug.out("WAV stream found"); throw new UnsupportedAudioFileException("WAV stream found"); } else if ((head[0] == '.') && (head[1] == 's') && (head[2] == 'n') && (head[3] == 'd')) { if (TDebug.TraceAudioFileReader) TDebug.out("AU stream found"); 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')) { if (TDebug.TraceAudioFileReader) TDebug.out("AIFF stream found"); throw new UnsupportedAudioFileException("AIFF 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'))) { if (TDebug.TraceAudioFileReader) TDebug.out("Ogg stream found"); 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 is not used //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", nVBR); aff_properties.put("mp3.vbr", 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", m_header.copyright()); aff_properties.put("mp3.original", m_header.original()); aff_properties.put("mp3.crc", m_header.checksums()); aff_properties.put("mp3.padding", 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) { if (TDebug.TraceAudioFileReader) TDebug.out("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) { if (TDebug.TraceAudioFileReader) TDebug.out("not a MPEG stream: wrong version"); throw new UnsupportedAudioFileException("not a MPEG stream: wrong version"); } int cSFIndex = (nHeader >> 10) & 0x3; if (cSFIndex == 3) { if (TDebug.TraceAudioFileReader) TDebug.out("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]; long bytesSkipped = fis.skip(inputStream.available()-id3v1.length); int read = fis.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. */ @Override 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 { if (TDebug.TraceAudioFileReader) {TDebug.out("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; } if (TDebug.TraceAudioFileReader) {TDebug.out("MpegAudioFileReader.getAudioInputStream(URL): end"); } return audioInputStream; } /** * Return the AudioInputStream from the given InputStream. */ @Override public AudioInputStream getAudioInputStream(InputStream inputStream) throws UnsupportedAudioFileException, IOException { if (TDebug.TraceAudioFileReader) TDebug.out("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) { String tag = new String(frames, 0, frames.length); 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("year",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]); } } private String chopSubstring(String s, int start, int end) { String str = s.substring(start, end); int loc = str.indexOf('\0'); if (loc != -1) str = str.substring(0, loc); 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) { if (TDebug.TraceAudioFileReader) TDebug.out("Cannot parse ID3v2 :"+e.getMessage()); } try { String value; for (int i=0;i<bframes.length-4;i++) { String code = new String(bframes,i,4); if ((code.equals("TALB")) || (code.equals("TIT2")) || (code.equals("TYER")) || (code.equals("TPE1")) || (code.equals("TCOP")) || (code.equals("COMM")) || (code.equals("TCON")) || (code.equals("TRCK"))) { i=i+10; size = (int) (bframes[i-6] << 24) + (bframes[i-5] << 16) + (bframes[i-4] << 8) + (bframes[i-3]); if (code.equals("COMM")) value = parseText(bframes, i, size, 5); else value = parseText(bframes,i, size, 1); if ((value != null) && (value.length()>0)) { if (code.equals("TALB")) props.put("album",value); else if (code.equals("TIT2")) props.put("title",value); else if (code.equals("TYER")) props.put("date",value); else if (code.equals("TPE1")) props.put("author",value); else if (code.equals("TCOP")) props.put("copyright",value); else if (code.equals("COMM")) props.put("comment",value); else if (code.equals("TCON")) props.put("mp3.id3tag.genre",value); else if (code.equals("TRCK")) props.put("mp3.id3tag.track",value); } i=i+size-1; } } } catch (RuntimeException e) { // Ignore all parsing errors. if (TDebug.TraceAudioFileReader) TDebug.out("Cannot parse ID3v2 :"+e.getMessage()); } } /** * 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_TYPES = {"ISO-8859-1", "UTF16","UTF-16BE", "UTF-8"}; value = new String(bframes,offset+skip,size-skip,ENC_TYPES[bframes[offset]]); } catch (UnsupportedEncodingException e) { if (TDebug.TraceAudioFileReader) TDebug.out("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) { // metaStr is not used //StringBuilder metaStr = new StringBuilder(); 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); } } } }