Package entagged.audioformats.asf

Source Code of entagged.audioformats.asf.AsfFileWriter

/*
* Entagged Audio Tag library
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/
package entagged.audioformats.asf;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;

import entagged.audioformats.Tag;
import entagged.audioformats.asf.data.AsfHeader;
import entagged.audioformats.asf.data.Chunk;
import entagged.audioformats.asf.data.ContentDescription;
import entagged.audioformats.asf.data.ContentDescriptor;
import entagged.audioformats.asf.data.ExtendedContentDescription;
import entagged.audioformats.asf.data.GUID;
import entagged.audioformats.asf.io.AsfHeaderReader;
import entagged.audioformats.asf.util.ChunkPositionComparator;
import entagged.audioformats.asf.util.TagConverter;
import entagged.audioformats.exceptions.CannotWriteException;
import entagged.audioformats.generic.AudioFileWriter;
import entagged.audioformats.generic.GenericTag;

/**
* This class writes given tags to asf files containing wma content. <br>
* <br>
*
* @author Christian Laireiter
*/
public class AsfFileWriter extends AudioFileWriter {

    /**
     * This method copies <code>number</code> of bytes from
     * <code>source</code> to <code>destination</code>.<br>
     *
     * @param source
     *                   Source for copy
     * @param destination
     *                   Destination for copy
     * @param number
     *                   number of bytes to copy.
     * @throws IOException
     *                    read or write errors.
     */
    private void copy(RandomAccessFile source, RandomAccessFile destination,
            long number) throws IOException {
        byte[] buffer = new byte[8192];
        long bytesCopied = 0;
        int read = -1;
        while ((read = source.read(buffer, 0,
                ((bytesCopied + 8192 > number) ? (int) (number - bytesCopied)
                        : 8192))) > 0) {
            bytesCopied += read;
            destination.write(buffer, 0, read);
        }
    }

    /**
     * This method creates a modfied copy of the input <code>raf</code> and
     * writes it to <code>rafTemp</code>.<br>
     * If the source contains no extended content description but one is needed (
     * {@link #isExtendedContentDescriptionMandatory(Tag)}) it will be created.
     * If the new file doesn't require such a chunk and the source didn't
     * contain optional values in it, it will be deleted.
     *
     * @param tag
     *                   Tag containing the new values.
     * @param header
     *                   Read header of <code>raf</code>.
     * @param raf
     *                   source
     * @param rafTemp
     *                   destination.
     * @param ignoreDescriptions
     *                   if <code>true</code> the content description and the
     *                   extended content description won't be written. (For deleting
     *                   tags.).
     * @throws IOException
     *                    read or write errors.
     */
    private void createModifiedCopy(Tag tag, AsfHeader header,
            RandomAccessFile raf, RandomAccessFile rafTemp,
            boolean ignoreDescriptions) throws IOException {
        /*
         * First of all Copy the first 30 bytes of the file. The GUID and one
         * unknown field is needed. (All other values [chunkcount, chunklength]
         * will be adjusted at the end of the method)
         */
        raf.seek(0);
        copy(raf, rafTemp, 30);
        // Get all chunks in order of their position
        Chunk[] chunks = getOrderedChunks(header);
        // We need to know the amount of bytes that differs from the original
        long fileSizeDifference = 0;
        long chunkCount = header.getChunkCount();
        // This field stores the location of the fileheader at the destination
        long newFileHeaderPos = -1;
        /*
         * Now check if the source doesn't contain a property chunk but one is
         * needed to store all tag values.
         */
        if (header.getExtendedContentDescription() == null
                && isExtendedContentDescriptionMandatory(tag)
                && !ignoreDescriptions) {
            // The source had no property chunk but one is written. So on chunk
            // more.
            chunkCount++;
            // Create the property chunk (A new one will always be placed at
            // first).
            fileSizeDifference += createNewExtendedContentDescription(tag,
                    null, rafTemp).getChunkLength().longValue();
        }
        /*
         * Now check if the source doesn't contain a content description but one
         * is needed to store all tag values. Content description stores title,
         * author, copyright, description and rating of the file
         */
        if (header.getContentDescription() == null
                && isContentdescriptionMandatory(tag) && !ignoreDescriptions) {
            // The source had no content description chunk but one is written.
            // So on
            // chunk more.
            chunkCount++;
            // Now create the new content description.
            fileSizeDifference += createNewContentDescription(tag, null,
                    rafTemp).getChunkLength().longValue();
        }
        // Now copy chunks (and modify if necessary)
        for (int i = 0; i < chunks.length; i++) {
            if (chunks[i] == header.getContentDescription()) {
                if (ignoreDescriptions) {
                    // Remove file size and so on
                    chunkCount--;
                    fileSizeDifference -= header.getContentDescription()
                            .getChunkLength().longValue();
                } else {
                    // Now write new content description , because of
                    // modifications
                    // Store the difference of sizes between old and new
                    // contentdescription.
                    fileSizeDifference += createNewContentDescription(tag,
                            header.getContentDescription(), rafTemp)
                            .getChunkLength().subtract(
                                    header.getContentDescription()
                                            .getChunkLength()).longValue();

                }
            } else if (chunks[i] == header.getExtendedContentDescription()) {
                if (deleteExtendedContentDescription(header
                        .getExtendedContentDescription(), tag)
                        || ignoreDescriptions) {
                    /*
                     * No property chunk will be written. So lower the
                     * chunkCount and store the amount of bytes lost from the
                     * original.
                     */
                    chunkCount--;
                    fileSizeDifference -= header
                            .getExtendedContentDescription().getChunkLength()
                            .longValue();
                } else {
                    /*
                     * write a modified copy of the property chunk;
                     */
                    fileSizeDifference += createNewExtendedContentDescription(
                            tag, header.getExtendedContentDescription(),
                            rafTemp).getChunkLength().subtract(
                            header.getExtendedContentDescription()
                                    .getChunkLength()).longValue();
                }
            } else {
                // No special handling required. This chunk can simply be
                // copied.
                if (GUID.GUID_FILE.equals(chunks[i].getGuid())) {
                    newFileHeaderPos = rafTemp.getFilePointer();
                }
                raf.seek(chunks[i].getPosition());
                copy(raf, rafTemp, chunks[i].getChunkLength().longValue());
            }
        }
        // Now the rest of the file needs to be copied
        raf.seek(header.getChunckEnd());
        copy(raf, rafTemp, raf.length() - raf.getFilePointer());
        // Now the asf header and the fileheader need to be adjusted.
        // First the number of chunks may differ, and second the size of the
        // file might have been changed. From second comes third: all changes
        // of the filesize occur in the asf header.
        rafTemp.seek(24);
        write16UINT(chunkCount, rafTemp);
        // Seek to the new file header and 40 more to reach the filesize
        // property
        rafTemp.seek(newFileHeaderPos + 40);
        write32UINT(header.getFileHeader().getFileSize().longValue()
                + fileSizeDifference, rafTemp);
        // Seekt to the asf header size and write the new value
        rafTemp.seek(16);
        write32UINT(header.getChunkLength().longValue() + fileSizeDifference,
                rafTemp);
    }

    /**
     * Like
     * {@link #createNewExtendedContentDescription(Tag, ExtendedContentDescription, RandomAccessFile)},
     * this method creates a new chunk for title, ...
     *
     * @param tag
     *                   tag which contains the values.
     * @param contentDescription
     *                   Description containing old values.
     * @param rafTemp
     *                   output.
     * @return Representation of new Chunk in <code>rafTemp</code>
     * @throws IOException
     *                    write errors.
     */
    private Chunk createNewContentDescription(Tag tag,
            ContentDescription contentDescription, RandomAccessFile rafTemp)
            throws IOException {
        // Store current Location
        long chunkStart = rafTemp.getFilePointer();
        // Create a content description object out of tag values.
        ContentDescription description = TagConverter
                .createContentDescription(tag);
        // Add optional existing values which aren't covered with tag.
        if (contentDescription != null) {
            description.setRating(contentDescription.getRating());
        }
        // Create byte[] for the asf file.
        byte[] asfContent = description.getBytes();
        // write content
        rafTemp.write(asfContent);
        // Create chunk information
        return new Chunk(GUID.GUID_CONTENTDESCRIPTION, chunkStart, BigInteger
                .valueOf(asfContent.length));
    }

    /**
     * This method writes a completely new extended content description to
     * <code>rafTemp</code>.<br>
     * Some values of {@link Tag}are nested within this chunk. Those values of
     * <code>tagChunk</code> which do not belong to tag will be kept, the rest
     * replaced or even added. <br>
     *
     * @param tag
     *                   contains new Elements.
     * @param tagChunk
     *                   chunk from source. May contain values, which are not reflected
     *                   by <code>tag</code>. Parameter can be <code>null</code>.
     * @param rafTemp
     *                   File to write to.
     * @return A new chunk object reflecting position and size.
     * @throws IOException
     *                    in Case of write errors.
     */
    private Chunk createNewExtendedContentDescription(Tag tag,
            ExtendedContentDescription tagChunk, RandomAccessFile rafTemp)
            throws IOException {
        long chunkStart = rafTemp.getFilePointer();
        if (tagChunk == null) {
            tagChunk = new ExtendedContentDescription();
        }
        TagConverter.assignCommonTagValues(tag, tagChunk);
        TagConverter.assignOptionalTagValues (tag,tagChunk);
       
        /*
         * Now perform the creation of the extended content description
         */
        byte[] asfBytes = tagChunk.getBytes();
        rafTemp.write(asfBytes);

        return new Chunk(GUID.GUID_EXTENDED_CONTENT_DESCRIPTION, chunkStart,
                BigInteger.valueOf(asfBytes.length));
    }

    /**
     * This method decides if the extended content description may be ignored
     * for the modified file. <br>
     * This is the case if the extended content description doesn't contain any
     * additional tags which are not contained in <code>tag</code> and
     * {@link #isExtendedContentDescriptionMandatory(Tag)}returns
     * <code>false</code>.
     *
     * @param tagHeader
     *                   ContentDescriptor chunk from source.
     * @param tag
     *                   Tag that is written.
     * @return <code>true</code>, if property chunk can be skipped.
     */
    private boolean deleteExtendedContentDescription(
            ExtendedContentDescription tagHeader, Tag tag) {
        HashSet ignoreDescriptors = new HashSet(Arrays.asList(new String[] {
                ContentDescriptor.ID_GENRE, ContentDescriptor.ID_GENREID,
                ContentDescriptor.ID_TRACKNUMBER, ContentDescriptor.ID_ALBUM,
                ContentDescriptor.ID_YEAR }));
        Iterator it = tagHeader.getDescriptors().iterator();
        boolean found = false;
        /*
         * Now search for a content descriptor whose id is not one of
         * ignoreDescriptors. These descriptors cannot be modified by entagged
         * and will be preserved
         */
        while (it.hasNext() && !found) {
            ContentDescriptor current = (ContentDescriptor) it.next();
            found = !ignoreDescriptors.contains(current.getName());
        }
        return !found && !isExtendedContentDescriptionMandatory(tag);
    }

    /**
     * (overridden)
     *
     * @see entagged.audioformats.generic.AudioFileWriter#deleteTag(java.io.RandomAccessFile,
     *           java.io.RandomAccessFile)
     */
    protected void deleteTag(RandomAccessFile raf, RandomAccessFile tempRaf)
            throws CannotWriteException, IOException {
        try {
            AsfHeader header = AsfHeaderReader.readHeader(raf);
            if (header == null) {
                throw new NullPointerException(
                        "Header is null, so file couldn't be read properly. "
                                + "(Interpretation of data, not file access rights.)");
            }
            createModifiedCopy(new GenericTag(), header, raf, tempRaf, true);
        } catch (IOException ioe) {
            throw ioe;
        } catch (Exception cre) {
            throw new CannotWriteException(
                    "Cannot modify tag because exception occured:\n   "
                            + cre.getMessage());
        }
    }

    /**
     * This method returns all chunks contained within the given
     * <code>header</code> and returns them in an array. Additionally the
     * chunks in the array are sorted ascending by their appearance in the file.
     * <br>
     * This should make it easy to create a copy an modify just specified
     * chunks.
     *
     * @param header
     *                   The header object containing all chunks.
     * @return All chunks ordered by position.
     */
    private Chunk[] getOrderedChunks(AsfHeader header) {
        ArrayList result = new ArrayList();
        // Add all chunks which simply need to be copied.
        for (int i = 0; i < header.getUnspecifiedChunkCount(); i++) {
            result.add(header.getUnspecifiedChunk(i));
        }
        // Add all stream chunks
        for (int i = 0; i < header.getStreamChunkCount(); i++) {
            result.add(header.getStreamChunk(i));
        }
        // Now the rest.
        if (header.getContentDescription() != null) {
            result.add(header.getContentDescription());
        }
        result.add(header.getFileHeader());
        if (header.getExtendedContentDescription() != null) {
            result.add(header.getExtendedContentDescription());
        }
        if (header.getEncodingChunk() != null) {
            result.add(header.getEncodingChunk());
        }
        if (header.getStreamBitratePropertiesChunk() != null) {
          result.add(header.getStreamBitratePropertiesChunk());
        }
        Chunk[] tmp = (Chunk[]) result.toArray(new Chunk[result.size()]);
        Arrays.sort(tmp, new ChunkPositionComparator());
        return tmp;
    }

    /**
     * This method decides if a content description chunk is needed in order to
     * store selected values of <code>tag</code>.<br>
     * The selected values are: <br>
     * {@link Tag#getTitle()}<br>
     * {@link Tag#getComment()}<br>
     * {@link Tag#getArtist()}<br>
     * <br>
     * The content description stores another fields which holds the
     * copyright,rating. <br>
     * However entagged doesn't support this.
     *
     * @param tag
     *                   Tag which should be written.
     * @return <code>true</code>, if a property chunk must be written in
     *               order to store all needed values of tag.
     */
    private boolean isContentdescriptionMandatory(Tag tag) {
        return tag.getFirstArtist().trim().length() > 0
                || tag.getFirstComment().trim().length() > 0
                || tag.getFirstTitle().trim().length() > 0;
    }

    /**
     * This method decides if a property chunk is needed in order to store
     * selected values of <code>tag</code>.<br>
     * The selected values are: <br>
     * {@link Tag#getTrack()}<br>
     * {@link Tag#getYear()}<br>
     * {@link Tag#getGenre()}<br>
     * {@link Tag#getAlbum()}<br>
     *
     * @param tag
     *                   Tag which should be written.
     * @return <code>true</code>, if a property chunk must be written in
     *               order to store all needed values of tag.
     */
    private boolean isExtendedContentDescriptionMandatory(Tag tag) {
        return tag.getFirstTrack().trim().length() > 0
                || tag.getFirstYear().trim().length() > 0
                || tag.getFirstGenre().trim().length() > 0
                || tag.getFirstAlbum().trim().length() > 0;
    }

    /**
     * This method writes the lower 16 Bit of <code>value</code> to raf. <br>
     *
     * @param value
     *                   value
     * @param raf
     *                   output
     * @return number of bytes written (Here always 2)
     * @throws IOException
     *                    Write errrors.
     */
    private int write16UINT(long value, RandomAccessFile raf)
            throws IOException {
        raf.write((int) value & 0x00FF);
        value >>= 8;
        raf.write((int) value & 0x00FF);
        return 2;
    }

    /**
     * This method writes the lower 32 Bit of <code>value</code> to raf. <br>
     *
     * @param value
     *                   value
     * @param raf
     *                   output
     * @return number of bytes written (Here always 4)
     * @throws IOException
     *                    Write errrors.
     */
    private int write32UINT(long value, RandomAccessFile raf)
            throws IOException {
        raf.write((int) value & 0x00FF);
        value >>= 8;
        raf.write((int) value & 0x00FF);
        value >>= 8;
        raf.write((int) value & 0x00FF);
        value >>= 8;
        raf.write((int) value & 0x00FF);
        return 4;
    }

    /**
     * Writes the tag into <code>rafTemp</code>.<br>
     * First the file <code>raf</code> is read as an asf file. If it could be
     * interpreted correctly it should be possible to properly write the
     * modifications.
     *
     *
     * @see entagged.audioformats.generic.AudioFileWriter#writeTag(entagged.audioformats.Tag,
     *           java.io.RandomAccessFile, java.io.RandomAccessFile)
     */
    protected void writeTag(Tag tag, RandomAccessFile raf,
            RandomAccessFile rafTemp) throws CannotWriteException, IOException {
        try {
            AsfHeader header = AsfHeaderReader.readHeader(raf);
            if (header == null) {
                throw new NullPointerException(
                        "Header is null, so file couldn't be read properly. "
                                + "(Interpretation of data, not file access rights.)");
            }
            createModifiedCopy(tag, header, raf, rafTemp, false);
        } catch (IOException ioe) {
            throw ioe;
        } catch (Exception cre) {
            throw new CannotWriteException(
                    "Cannot modify tag because exception occured:\n   "
                            + cre.getMessage());
        }
    }

}
TOP

Related Classes of entagged.audioformats.asf.AsfFileWriter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.