package org.farng.mp3.id3; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Iterator; import java.util.LinkedList; 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; /** * <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: 2374 $ */ 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; } }