/*
* Entagged Audio Tag library
* Copyright (c) 2003-2005 Raphael Slinckx <raphael@slinckx.net>
*
* 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 org.jaudiotagger.audio.flac;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.CannotWriteException;
import org.jaudiotagger.audio.flac.metadatablock.*;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.flac.FlacTag;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* Write Flac Tag
*/
public class FlacTagWriter {
// Logger Object
public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.flac");
private MetadataBlock streamInfoBlock;
private List<MetadataBlock> metadataBlockPadding = new ArrayList<MetadataBlock>(1);
private List<MetadataBlock> metadataBlockApplication = new ArrayList<MetadataBlock>(1);
private List<MetadataBlock> metadataBlockSeekTable = new ArrayList<MetadataBlock>(1);
private List<MetadataBlock> metadataBlockCueSheet = new ArrayList<MetadataBlock>(1);
private FlacTagCreator tc = new FlacTagCreator();
private FlacTagReader reader = new FlacTagReader();
/**
* Delete Tag from file
*
* @param raf
* @param tempRaf
* @throws IOException
* @throws CannotWriteException
*/
public void delete(RandomAccessFile raf, RandomAccessFile tempRaf) throws IOException, CannotWriteException {
//This will save the file without any Comment or PictureData blocks
FlacTag emptyTag = new FlacTag(null, new ArrayList<MetadataBlockDataPicture>());
raf.seek(0);
tempRaf.seek(0);
write(emptyTag, raf, tempRaf);
}
/**
* Write tag to file
*
* @param tag
* @param raf
* @param rafTemp
* @throws CannotWriteException
* @throws IOException
*/
public void write(Tag tag, RandomAccessFile raf, RandomAccessFile rafTemp) throws CannotWriteException, IOException {
// //logger.info("Writing tag");
//Clean up old data
streamInfoBlock = null;
metadataBlockPadding.clear();
metadataBlockApplication.clear();
metadataBlockSeekTable.clear();
metadataBlockCueSheet.clear();
//Read existing data
FlacStreamReader flacStream = new FlacStreamReader(raf);
try {
flacStream.findStream();
} catch (CannotReadException cre) {
throw new CannotWriteException(cre.getMessage());
}
boolean isLastBlock = false;
while (!isLastBlock) {
MetadataBlockHeader mbh = MetadataBlockHeader.readHeader(raf);
switch (mbh.getBlockType()) {
case STREAMINFO: {
streamInfoBlock = new MetadataBlock(mbh, new MetadataBlockDataStreamInfo(mbh, raf));
break;
}
case VORBIS_COMMENT:
case PADDING:
case PICTURE: {
//All these will be replaced by the new metadata so we just treat as padding in order
//to determine how much space is already allocated in the file
raf.seek(raf.getFilePointer() + mbh.getDataLength());
MetadataBlockData mbd = new MetadataBlockDataPadding(mbh.getDataLength());
metadataBlockPadding.add(new MetadataBlock(mbh, mbd));
break;
}
case APPLICATION: {
MetadataBlockData mbd = new MetadataBlockDataApplication(mbh, raf);
metadataBlockApplication.add(new MetadataBlock(mbh, mbd));
break;
}
case SEEKTABLE: {
MetadataBlockData mbd = new MetadataBlockDataSeekTable(mbh, raf);
metadataBlockSeekTable.add(new MetadataBlock(mbh, mbd));
break;
}
case CUESHEET: {
MetadataBlockData mbd = new MetadataBlockDataCueSheet(mbh, raf);
metadataBlockCueSheet.add(new MetadataBlock(mbh, mbd));
break;
}
default: {
//What are the consequences of doing this
raf.seek(raf.getFilePointer() + mbh.getDataLength());
break;
}
}
isLastBlock = mbh.isLastBlock();
}
//Number of bytes in the existing file available before audio data
int availableRoom = computeAvailableRoom();
//Minimum Size of the New tag data without padding
int newTagSize = tc.convert(tag).limit();
//Number of bytes required for new tagdata and other metadata blocks
int neededRoom = newTagSize + computeNeededRoom();
//Go to start of Flac within file
raf.seek(flacStream.getStartOfFlacInFile());
// //logger.info("Writing tag available bytes:" + availableRoom + ":needed bytes:" + neededRoom);
//There is enough room to fit the tag without moving the audio just need to
//adjust padding accordingly need to allow space for padding header if padding required
if ((availableRoom == neededRoom) || (availableRoom > neededRoom + MetadataBlockHeader.HEADER_LENGTH)) {
//Jump over Id3 (if exists) Flac and StreamInfoBlock
raf.seek(flacStream.getStartOfFlacInFile() + FlacStreamReader.FLAC_STREAM_IDENTIFIER_LENGTH);
//Write StreamInfo, we always write this first even if wasnt first in original spec
raf.write(streamInfoBlock.getHeader().getBytesWithoutIsLastBlockFlag());
raf.write(streamInfoBlock.getData().getBytes());
//Write Application Blocks
for (MetadataBlock aMetadataBlockApplication : metadataBlockApplication) {
raf.write(aMetadataBlockApplication.getHeader().getBytesWithoutIsLastBlockFlag());
raf.write(aMetadataBlockApplication.getData().getBytes());
}
//Write Seek Table Blocks
for (MetadataBlock aMetadataBlockSeekTable : metadataBlockSeekTable) {
raf.write(aMetadataBlockSeekTable.getHeader().getBytesWithoutIsLastBlockFlag());
raf.write(aMetadataBlockSeekTable.getData().getBytes());
}
//Write Cue sheet Blocks
for (MetadataBlock aMetadataBlockCueSheet : metadataBlockCueSheet) {
raf.write(aMetadataBlockCueSheet.getHeader().getBytesWithoutIsLastBlockFlag());
raf.write(aMetadataBlockCueSheet.getData().getBytes());
}
//Write tag (and padding)
raf.getChannel().write(tc.convert(tag, availableRoom - neededRoom));
}
//Need to move audio
else {
//Skip to start of Audio
//Write FlacStreamReader and StreamIfoMetablock to new file
int dataStartSize = flacStream.getStartOfFlacInFile() + FlacStreamReader.FLAC_STREAM_IDENTIFIER_LENGTH + MetadataBlockHeader.HEADER_LENGTH + MetadataBlockDataStreamInfo.STREAM_INFO_DATA_LENGTH;
raf.seek(0);
rafTemp.getChannel().transferFrom(raf.getChannel(), 0, dataStartSize);
rafTemp.seek(dataStartSize);
//Write all the metadatablocks
for (MetadataBlock aMetadataBlockApplication : metadataBlockApplication) {
rafTemp.write(aMetadataBlockApplication.getHeader().getBytesWithoutIsLastBlockFlag());
rafTemp.write(aMetadataBlockApplication.getData().getBytes());
}
for (MetadataBlock aMetadataBlockSeekTable : metadataBlockSeekTable) {
rafTemp.write(aMetadataBlockSeekTable.getHeader().getBytesWithoutIsLastBlockFlag());
rafTemp.write(aMetadataBlockSeekTable.getData().getBytes());
}
for (MetadataBlock aMetadataBlockCueSheet : metadataBlockCueSheet) {
rafTemp.write(aMetadataBlockCueSheet.getHeader().getBytesWithoutIsLastBlockFlag());
rafTemp.write(aMetadataBlockCueSheet.getData().getBytes());
}
//Write tag data use default padding
rafTemp.write(tc.convert(tag, FlacTagCreator.DEFAULT_PADDING).array());
//Write audio to new file
raf.seek(dataStartSize + availableRoom);
rafTemp.getChannel().transferFrom(raf.getChannel(), rafTemp.getChannel().position(), raf.getChannel().size());
}
}
/**
* @return space currently availble for writing all Flac metadatablocks exceprt for StreamInfo which is fixed size
*/
private int computeAvailableRoom() {
int length = 0;
for (MetadataBlock aMetadataBlockApplication : metadataBlockApplication) {
length += aMetadataBlockApplication.getLength();
}
for (MetadataBlock aMetadataBlockSeekTable : metadataBlockSeekTable) {
length += aMetadataBlockSeekTable.getLength();
}
for (MetadataBlock aMetadataBlockCueSheet : metadataBlockCueSheet) {
length += aMetadataBlockCueSheet.getLength();
}
for (MetadataBlock aMetadataBlockPadding : metadataBlockPadding) {
length += aMetadataBlockPadding.getLength();
}
return length;
}
/**
* @return space required to write the metadata blocks that are part of Flac but are not part of tagdata
* in the normal sense.
*/
private int computeNeededRoom() {
int length = 0;
for (MetadataBlock aMetadataBlockApplication : metadataBlockApplication) {
length += aMetadataBlockApplication.getLength();
}
for (MetadataBlock aMetadataBlockSeekTable : metadataBlockSeekTable) {
length += aMetadataBlockSeekTable.getLength();
}
for (MetadataBlock aMetadataBlockCueSheet : metadataBlockCueSheet) {
length += aMetadataBlockCueSheet.getLength();
}
return length;
}
}