package org.farng.mp3.id3;
import org.farng.mp3.InvalidTagException;
import org.farng.mp3.TagUtility;
import org.farng.mp3.object.ObjectID3v2LyricLine;
import org.farng.mp3.object.ObjectLyrics3Line;
import org.farng.mp3.object.ObjectLyrics3TimeStamp;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Iterator;
import java.util.LinkedList;
/**
* <h3>4.9. Synchronised lyrics/text</h3>
* <p/>
* <p> This is another way of incorporating the words, said or sung lyrics,<br> in the audio
* file as text, this time, however, in sync with the<br> audio. It might also be used to describing events
* e.g. occurring on a<br>
* <p/>
* stage or on the screen in sync with the audio. The header includes a<br> content
* descriptor, represented with as terminated text string. If no<br> descriptor is entered, 'Content
* descriptor' is $00 (00) only.</p>
* <p/>
* <p> <Header for 'Synchronised lyrics/text', ID: "SYLT"><br>
* <p/>
* Text encoding $xx<br>
* Language $xx xx xx<br>
* Time stamp format $xx<br>
* <p/>
* Content type $xx<br>
* Content descriptor <text string according to encoding> $00 (00)</p>
* <p/>
* <p> Content type: $00 is other<br>
* <p/>
* $01 is
* lyrics<br>
* $02 is text transcription<br>
* $03 is movement/part name (e.g. "Adagio")<br>
* <p/>
* $04 is
* events (e.g. "Don Quijote enters the stage")<br>
* $05 is chord (e.g. "Bb F Fsus")<br>
* $06 is trivia/'pop up' information<br>
* <p/>
* $07 is
* URLs to webpages<br>
* $08 is URLs to images</p>
* <p/>
* <p> Time stamp format:</p>
* <p/>
* <p> $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit<br>
* <p/>
* $02 Absolute time, 32 bit sized, using milliseconds as unit</p>
* <p/>
* <p> Absolute time means that every stamp contains the time from the<br> beginning of the
* file.</p>
* <p/>
* <p> The text that follows the frame header differs from that of the<br>
* <p/>
* unsynchronised lyrics/text transcription in one major way. Each<br> syllable (or whatever
* size of text is considered to be convenient by<br> the encoder) is a null terminated string followed by
* a time stamp<br> denoting where in the sound file it belongs. Each sync thus has the<br>
* following structure:</p>
* <p/>
* <p> Terminated text to be synced (typically a syllable)<br> Sync
* identifier (terminator to above string) $00 (00)<br> Time
* stamp
* $xx (xx ...)</p>
* <p/>
* <p> The 'time stamp' is set to zero or the whole sync is omitted if<br> located directly at
* the beginning of the sound. All time stamps<br> should be sorted in chronological order. The sync can be
* considered<br> as a validator of the subsequent string.</p>
* <p/>
* <p> Newline characters are allowed in all "SYLT" frames and MUST be used<br> after
* every entry (name, event etc.) in a frame with the content type<br> $03 - $04.</p>
* <p/>
* <p> A few considerations regarding whitespace characters: Whitespace<br>
* <p/>
* separating words should mark the beginning of a new word, thus<br> occurring in front of
* the first syllable of a new word. This is also<br> valid for new line characters. A syllable followed by
* a comma should<br> not be broken apart with a sync (both the syllable and the comma<br>
* should be before the sync).</p>
* <p/>
* <p> An example: The "USLT" passage</p>
* <p/>
* <p> "Strangers in the night" $0A "Exchanging glances"</p>
* <p/>
* <p> would be "SYLT" encoded as:</p>
* <p/>
* <p> "Strang" $00 xx xx "ers" $00 xx xx " in" $00 xx xx "
* the" $00 xx xx<br>
* <p/>
* " night" $00 xx xx 0A "Ex" $00 xx xx "chang" $00 xx xx
* "ing" $00 xx<br> xx "glan" $00 xx xx "ces" $00 xx xx</p>
* <p/>
* <p> There may be more than one "SYLT" frame in each tag, but only one<br> with the
* same language and content descriptor.<br> </p>
*
* @author Eric Farng
* @version $Revision: 1.5 $
*/
public class FrameBodySYLT extends AbstractID3v2FrameBody {
LinkedList lines = new LinkedList();
String description = "";
String language = "";
byte contentType = 0;
byte textEncoding = 0;
byte timeStampFormat = 0;
/**
* Creates a new FrameBodySYLT object.
*/
public FrameBodySYLT() {
super();
}
/**
* Creates a new FrameBodySYLT object.
*/
public FrameBodySYLT(final FrameBodySYLT copyObject) {
super(copyObject);
this.description = new String(copyObject.description);
this.language = new String(copyObject.language);
this.contentType = copyObject.contentType;
this.textEncoding = copyObject.textEncoding;
this.timeStampFormat = copyObject.timeStampFormat;
ObjectID3v2LyricLine newLine;
for (int i = 0; i < copyObject.lines.size(); i++) {
newLine = new ObjectID3v2LyricLine((ObjectID3v2LyricLine) copyObject.lines.get(i));
this.lines.add(newLine);
}
}
/**
* Creates a new FrameBodySYLT object.
*/
public FrameBodySYLT(final byte textEncoding,
final String language,
final byte timeStampFormat,
final byte contentType,
final String description) {
this.textEncoding = textEncoding;
this.language = language;
this.timeStampFormat = timeStampFormat;
this.contentType = contentType;
this.description = description;
}
/**
* Creates a new FrameBodySYLT object.
*/
public FrameBodySYLT(final RandomAccessFile file) throws IOException, InvalidTagException {
this.read(file);
}
public byte getContentType() {
return this.contentType;
}
public String getDescription() {
return this.description;
}
public String getIdentifier() {
return "SYLT";
}
public String getLanguage() {
return this.language;
}
public String getLyric() {
String lyrics = "";
for (int i = 0; i < this.lines.size(); i++) {
lyrics += this.lines.get(i);
}
return lyrics;
}
public int getSize() {
int size;
size = 1 + 3 + 1 + 1 + this.description.length();
for (int i = 0; i < this.lines.size(); i++) {
size += ((ObjectID3v2LyricLine) this.lines.get(i)).getSize();
}
return size;
}
public byte getTextEncoding() {
return this.textEncoding;
}
public byte getTimeStampFormat() {
return this.timeStampFormat;
}
public void addLyric(final int timeStamp, final String text) {
final ObjectID3v2LyricLine line = new ObjectID3v2LyricLine("Lyric Line");
line.setTimeStamp(timeStamp);
line.setText(text);
this.lines.add(line);
}
public void addLyric(final ObjectLyrics3Line line) {
final Iterator iterator = line.getTimeStamp();
ObjectLyrics3TimeStamp timeStamp;
final String lyric = line.getLyric();
long time;
final ObjectID3v2LyricLine id3Line;
id3Line = new ObjectID3v2LyricLine("Lyric Line");
if (iterator.hasNext() == false) {
// no time stamp, give it 0
time = 0;
id3Line.setTimeStamp(time);
id3Line.setText(lyric);
this.lines.add(id3Line);
} else {
while (iterator.hasNext()) {
timeStamp = (ObjectLyrics3TimeStamp) iterator.next();
time = (timeStamp.getMinute() * 60) + timeStamp.getSecond(); // seconds
time *= 1000; // milliseconds
id3Line.setTimeStamp(time);
id3Line.setText(lyric);
this.lines.add(id3Line);
}
}
}
/**
* This method is not yet supported.
*
* @throws java.lang.UnsupportedOperationException
* This method is not yet supported
*/
public void equals() {
// todo Implement this java.lang.Object method
throw new java.lang.UnsupportedOperationException("Method equals() not yet implemented.");
}
public Iterator iterator() {
return this.lines.iterator();
}
protected void setupObjectList() {
// throw new UnsupportedOperationException();
}
public void read(final RandomAccessFile file) throws IOException, InvalidTagException {
final int size;
final int delim;
int offset = 0;
final byte[] buffer;
final String str;
size = readHeader(file);
buffer = new byte[size];
file.read(buffer);
str = new String(buffer);
this.textEncoding = buffer[offset++];
this.language = str.substring(offset, offset + 3);
offset += 3;
this.timeStampFormat = buffer[offset++];
this.contentType = buffer[offset++];
delim = str.indexOf(0, offset);
this.description = str.substring(offset, delim);
offset = delim + 1;
final byte[] data = new byte[size - offset];
System.arraycopy(buffer, offset, data, 0, size - offset);
readByteArray(data);
}
public void readByteArray(final byte[] arr) {
int offset = 0;
int delim;
byte[] line;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == 0) {
delim = i;
line = new byte[offset - delim + 4];
System.arraycopy(arr, offset, line, 0, offset - delim + 4);
this.lines.add(new ObjectID3v2LyricLine("Lyric Line"));
i += 4;
offset += 4;
}
}
}
public String toString() {
String str;
str = getIdentifier() + " " + this
.textEncoding + " " + this
.language + " " + this
.timeStampFormat + " " + this
.contentType + " " + this
.description;
for (int i = 0; i < this.lines.size(); i++) {
str += (this.lines.get(i)).toString();
}
return str;
}
public void write(final RandomAccessFile file) throws IOException {
final byte[] buffer;
int offset = 0;
writeHeader(file, this.getSize());
buffer = new byte[this.getSize()];
buffer[offset++] = this.textEncoding; // text encoding;
this.language = TagUtility.truncate(this.language, 3);
for (int i = 0; i < this.language.length(); i++) {
buffer[i + offset] = (byte) this.language.charAt(i);
}
offset += this.language.length();
buffer[offset++] = this.timeStampFormat;
buffer[offset++] = this.contentType;
for (int i = 0; i < this.description.length(); i++) {
buffer[i + offset] = (byte) this.description.charAt(i);
}
offset += this.description.length();
buffer[offset++] = 0; // null character
System.arraycopy(writeByteArray(), 0, buffer, offset, buffer.length - offset);
file.write(buffer);
}
public byte[] writeByteArray() {
final byte[] arr;
ObjectID3v2LyricLine line = null;
int offset = 0;
int size = 0;
for (int i = 0; i < this.lines.size(); i++) {
line = (ObjectID3v2LyricLine) this.lines.get(i);
size += line.getSize();
}
arr = new byte[size];
for (int i = 0; i < this.lines.size(); i++) {
line = (ObjectID3v2LyricLine) this.lines.get(i);
}
if (line != null) {
System.arraycopy(line.writeByteArray(), 0, arr, offset, line.getSize());
offset += line.getSize();
}
return arr;
}
}