AutoDJ.metaReader.OggIndexer.java Source code

Java tutorial

Introduction

Here is the source code for AutoDJ.metaReader.OggIndexer.java

Source

/**
 * OggIndexer.java
 * (C) 2011 Florian Staudacher, Christian Wurst
 * 
 * This file is part of AutoDJ.
 *
 * AutoDJ is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * AutoDJ 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with AutoDJ.  If not, see <http://www.gnu.org/licenses/>.
 */

package AutoDJ.metaReader;

import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.HashMap;

import javax.imageio.ImageIO;

import org.apache.commons.codec.binary.Base64;

/**
 * class OggIndexer
 * 
 * provides the functionality to read metadata (VorbisComments) from Ogg Vorbis files
 * Format specification can be found at
 * http://xiph.org/vorbis/doc/Vorbis_I_spec.html
 * 
 * @author Florian Staudacher <florian_staudacher@yahoo.de>
 *
 */
public class OggIndexer extends AudioFileIndexer {

    /**
    *  64k, maximum size of an ogg packet
    */
    protected final int two16 = 255 * 255;

    /**
     * remember the position the last ogg header started
     * (we need this, in case a page spans more than one packet)
     */
    protected long headerStart = 0;

    /**
     * this value is true, if the current packet needs to be 
     * concatenated with the following packet
     * (that also means that the current package has a size of 64kb)
     */
    protected boolean concatNext = false;

    /**
     * keep the total number of vorbis comments to know when to stop
     */
    protected int numberVorbisComments;

    /**
     * store the keys and values of the vorbis comments in a map
     */
    protected HashMap<String, String> vorbisComments;

    /**
     * initialize this object and start reading the file
     * @param String path
     */
    public OggIndexer(String path) {
        filePath = path;
        try {
            readFile(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * get the values out of the metadata hashmap
     */
    public void populateMetadata() {
        title = vorbisComments.get("title");
        album = vorbisComments.get("album");
        artist = vorbisComments.get("artist");
        year = vorbisComments.get("date");
        trackno = vorbisComments.get("tracknumber");
        genre = vorbisComments.get("genre");
        cover = readAlbumImage(vorbisComments.get("metadata_block_picture"));
    }

    /**
     * open the file, jump to the comment header and put it in the buffer,
     * then populate the internal hashmap of metadata
     * 
     * @param String path
     */
    public void readFile(String path) throws Exception {
        audioFile = new File(path);
        raf = new RandomAccessFile(audioFile, "r");

        // we need to read that in order to know where to start looking
        readIdentificationHeader();

        // skip over the parts we don't care about
        readCommentHeader();

        int venLen = getIntFromBuff();
        raf.skipBytes(venLen); // skip over the vendor string (always the same)

        numberVorbisComments = getIntFromBuff();
        vorbisComments = new HashMap<String, String>();

        for (int i = 0; i < numberVorbisComments; i++) {
            int readLength = getIntFromBuff();
            long newFilePos = raf.getFilePointer() + readLength;
            String content = "";

            // take care of pages spanning more than one packet
            while (concatNext && (newFilePos - headerStart) >= two16) {
                long packetEnd = headerStart + two16;
                int readLengthShort = readLength - ((int) (newFilePos - packetEnd));

                byte[] contentFirst = new byte[readLengthShort];
                raf.read(contentFirst);
                content += new String(contentFirst);

                concatNext = false;

                // now comes another ogg header...
                //System.out.println("looking for new header at "+raf.getFilePointer() );
                readOggHeader();
            }

            byte[] contentRest = new byte[readLength];
            raf.read(contentRest);

            content += new String(contentRest);
            addToMap(content.getBytes());
        }
    }

    public String toString() {
        return "Ogg-File: " + super.toString();
    }

    /**
     * adds a vorbis comment tag to the metadata map
     * 
     * @param byte[] pair
     */
    protected void addToMap(byte[] pair) {
        String[] vals = new String(pair).split("=");
        if (vals.length > 1) {
            vorbisComments.put(vals[0].toLowerCase(), vals[1]);
        }
    }

    /**
     * read the ogg bitstream package header and do some sanity checks
     * 
     * @throws Exception
     */
    protected void readOggHeader() throws Exception {
        byte[] header = new byte[27];
        raf.readFully(header);

        if (!(header[0] == 'O' && header[1] == 'g' && header[2] == 'g' && header[3] == 'S')) {
            // this is not an ogg! abort...
            //System.out.println("not an ogg header at " + raf.getFilePointer() );
            return;
        }

        // we now skip the following:
        //    version         = 1 byte,
        //   headerType      = 1 byte,
        //   granulePosition = 8 bytes,
        //    bitstreamSerial = 4 bytes,
        //   pageSequenceNum = 4 bytes,
        //   crcChecksum     = 4 bytes
        // makes 22 bytes + 4 from "OggS" = 26
        int pageSegments = unsignedByteToInt(header[26]);
        if (pageSegments == 255) {
            concatNext = true;
        }
        int totalLength = 0;

        for (int i = 0; i < pageSegments; i++) {
            int l = 0;
            byte[] tmp = new byte[1];
            raf.read(tmp);
            l = tmp[0] & 0xff;
            totalLength += l;
        }

        // save where this header ended
        headerStart = raf.getFilePointer();
    }

    /**
     * reads the OGG Vorbis identification header
     * @throws Exception 
     */
    protected void readIdentificationHeader() throws Exception {
        readOggHeader();

        // if this is not an identificationHeader, we can stop going any further
        if (raf.read() != 0x01)
            return;

        byte[] vorbisIdent = new byte[6];
        raf.read(vorbisIdent);

        // this needs to be a vorbis header...
        if (!(new String(vorbisIdent).equals("vorbis")))
            return;

        // we will now skip the following:
        //     vorbisVersion = 4 bytes,
        //     audioChannels = 1 byte,
        //     sampleRate    = 4 bytes,
        //     bitrateMax    = 4 bytes,
        //     bitrateNom    = 4 bytes,
        //     bitrateMin    = 4 bytes,
        //     blocksize0    = 1 byte,
        //     blocksize1    = 1 byte
        // is 23 bytes of info we don't care about...
        raf.skipBytes(23);
    }

    /**
     * starts reading the vorbis comment header.
     * the actual metadata is parsed later
     * @throws Exception 
     */
    protected void readCommentHeader() throws Exception {
        readOggHeader();

        byte[] headerTest = new byte[7];

        // unfortunately we need to look for the vorbis header
        do {
            raf.read(headerTest);
            raf.seek(raf.getFilePointer() - 6);

        } while (headerTest[0] != 0x03 && // comment header ident
                headerTest[1] != 0x76 && // 'v'
                headerTest[2] != 0x6F && // 'o'
                headerTest[3] != 0x72 && // 'r'
                headerTest[4] != 0x62 && // 'b'
                headerTest[4] != 0x69 && // 'i'
                headerTest[4] != 0x73 // 's'
        );

        // assume, we found the header - skip until after the ident string
        raf.skipBytes(6);
    }

    /**
     * Extracts the Image from a FLAC picture structure
     * http://flac.sourceforge.net/format.html#metadata_block_picture
     * Expects a base64 encoded string
     * 
     * @param String pictureBlock (base64)
     * @return BufferedImage
     */
    BufferedImage readAlbumImage(String pictureBlock) {
        if (pictureBlock == null || pictureBlock.isEmpty())
            return null;

        byte[] pictureBytes = Base64.decodeBase64(pictureBlock);
        BufferedImage img = null;

        String mimeString = "", description = "";
        ByteBuffer picBuff = ByteBuffer.allocate(pictureBytes.length);
        picBuff.put(pictureBytes);
        picBuff.rewind();

        /*int picType = */picBuff.getInt(); // not interesting, discard

        // read the mime type string
        int mimeStrLength = picBuff.getInt();
        byte[] mimeBytes = new byte[mimeStrLength];
        picBuff.get(mimeBytes);
        mimeString = new String(mimeBytes);

        // read the string describing the image 
        int descStrLength = picBuff.getInt();
        byte[] descBytes = new byte[descStrLength];
        picBuff.get(descBytes);
        try {
            description = new String(descBytes, "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        }

        // skip over some unnecessary information
        /*int picWidth  = */picBuff.getInt();
        /*int picHeight = */picBuff.getInt();
        /*int colDepth  = */picBuff.getInt();
        /*int idxColors = */picBuff.getInt();

        // read the image data
        int picDataLength = picBuff.getInt();
        byte[] picBytes = new byte[picDataLength];
        picBuff.get(picBytes);
        try {
            img = ImageIO.read(new ByteArrayInputStream(picBytes));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return img;
    }

}