| 1 | package org.farng.mp3.id3; |
| 2 | |
| 3 | import org.farng.mp3.InvalidTagException; |
| 4 | import org.farng.mp3.TagUtility; |
| 5 | import org.farng.mp3.object.ObjectID3v2LyricLine; |
| 6 | import org.farng.mp3.object.ObjectLyrics3Line; |
| 7 | import org.farng.mp3.object.ObjectLyrics3TimeStamp; |
| 8 | |
| 9 | import java.io.IOException; |
| 10 | import java.io.RandomAccessFile; |
| 11 | import java.util.Iterator; |
| 12 | import java.util.LinkedList; |
| 13 | |
| 14 | /** |
| 15 | * <h3>4.9. Synchronised lyrics/text</h3> |
| 16 | * <p/> |
| 17 | * <p> This is another way of incorporating the words, said or sung lyrics,<br> in the audio |
| 18 | * file as text, this time, however, in sync with the<br> audio. It might also be used to describing events |
| 19 | * e.g. occurring on a<br> |
| 20 | * <p/> |
| 21 | * stage or on the screen in sync with the audio. The header includes a<br> content |
| 22 | * descriptor, represented with as terminated text string. If no<br> descriptor is entered, 'Content |
| 23 | * descriptor' is $00 (00) only.</p> |
| 24 | * <p/> |
| 25 | * <p> <Header for 'Synchronised lyrics/text', ID: "SYLT"><br> |
| 26 | * <p/> |
| 27 | * Text encoding $xx<br> |
| 28 | * Language $xx xx xx<br> |
| 29 | * Time stamp format $xx<br> |
| 30 | * <p/> |
| 31 | * Content type $xx<br> |
| 32 | * Content descriptor <text string according to encoding> $00 (00)</p> |
| 33 | * <p/> |
| 34 | * <p> Content type: $00 is other<br> |
| 35 | * <p/> |
| 36 | * $01 is |
| 37 | * lyrics<br> |
| 38 | * $02 is text transcription<br> |
| 39 | * $03 is movement/part name (e.g. "Adagio")<br> |
| 40 | * <p/> |
| 41 | * $04 is |
| 42 | * events (e.g. "Don Quijote enters the stage")<br> |
| 43 | * $05 is chord (e.g. "Bb F Fsus")<br> |
| 44 | * $06 is trivia/'pop up' information<br> |
| 45 | * <p/> |
| 46 | * $07 is |
| 47 | * URLs to webpages<br> |
| 48 | * $08 is URLs to images</p> |
| 49 | * <p/> |
| 50 | * <p> Time stamp format:</p> |
| 51 | * <p/> |
| 52 | * <p> $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit<br> |
| 53 | * <p/> |
| 54 | * $02 Absolute time, 32 bit sized, using milliseconds as unit</p> |
| 55 | * <p/> |
| 56 | * <p> Absolute time means that every stamp contains the time from the<br> beginning of the |
| 57 | * file.</p> |
| 58 | * <p/> |
| 59 | * <p> The text that follows the frame header differs from that of the<br> |
| 60 | * <p/> |
| 61 | * unsynchronised lyrics/text transcription in one major way. Each<br> syllable (or whatever |
| 62 | * size of text is considered to be convenient by<br> the encoder) is a null terminated string followed by |
| 63 | * a time stamp<br> denoting where in the sound file it belongs. Each sync thus has the<br> |
| 64 | * following structure:</p> |
| 65 | * <p/> |
| 66 | * <p> Terminated text to be synced (typically a syllable)<br> Sync |
| 67 | * identifier (terminator to above string) $00 (00)<br> Time |
| 68 | * stamp |
| 69 | * $xx (xx ...)</p> |
| 70 | * <p/> |
| 71 | * <p> The 'time stamp' is set to zero or the whole sync is omitted if<br> located directly at |
| 72 | * the beginning of the sound. All time stamps<br> should be sorted in chronological order. The sync can be |
| 73 | * considered<br> as a validator of the subsequent string.</p> |
| 74 | * <p/> |
| 75 | * <p> Newline characters are allowed in all "SYLT" frames and MUST be used<br> after |
| 76 | * every entry (name, event etc.) in a frame with the content type<br> $03 - $04.</p> |
| 77 | * <p/> |
| 78 | * <p> A few considerations regarding whitespace characters: Whitespace<br> |
| 79 | * <p/> |
| 80 | * separating words should mark the beginning of a new word, thus<br> occurring in front of |
| 81 | * the first syllable of a new word. This is also<br> valid for new line characters. A syllable followed by |
| 82 | * a comma should<br> not be broken apart with a sync (both the syllable and the comma<br> |
| 83 | * should be before the sync).</p> |
| 84 | * <p/> |
| 85 | * <p> An example: The "USLT" passage</p> |
| 86 | * <p/> |
| 87 | * <p> "Strangers in the night" $0A "Exchanging glances"</p> |
| 88 | * <p/> |
| 89 | * <p> would be "SYLT" encoded as:</p> |
| 90 | * <p/> |
| 91 | * <p> "Strang" $00 xx xx "ers" $00 xx xx " in" $00 xx xx " |
| 92 | * the" $00 xx xx<br> |
| 93 | * <p/> |
| 94 | * " night" $00 xx xx 0A "Ex" $00 xx xx "chang" $00 xx xx |
| 95 | * "ing" $00 xx<br> xx "glan" $00 xx xx "ces" $00 xx xx</p> |
| 96 | * <p/> |
| 97 | * <p> There may be more than one "SYLT" frame in each tag, but only one<br> with the |
| 98 | * same language and content descriptor.<br> </p> |
| 99 | * |
| 100 | * @author Eric Farng |
| 101 | * @version $Revision: 1.5 $ |
| 102 | */ |
| 103 | public class FrameBodySYLT extends AbstractID3v2FrameBody { |
| 104 | |
| 105 | LinkedList lines = new LinkedList(); |
| 106 | String description = ""; |
| 107 | String language = ""; |
| 108 | byte contentType = 0; |
| 109 | byte textEncoding = 0; |
| 110 | byte timeStampFormat = 0; |
| 111 | |
| 112 | /** |
| 113 | * Creates a new FrameBodySYLT object. |
| 114 | */ |
| 115 | public FrameBodySYLT() { |
| 116 | super(); |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Creates a new FrameBodySYLT object. |
| 121 | */ |
| 122 | public FrameBodySYLT(final FrameBodySYLT copyObject) { |
| 123 | super(copyObject); |
| 124 | this.description = new String(copyObject.description); |
| 125 | this.language = new String(copyObject.language); |
| 126 | this.contentType = copyObject.contentType; |
| 127 | this.textEncoding = copyObject.textEncoding; |
| 128 | this.timeStampFormat = copyObject.timeStampFormat; |
| 129 | ObjectID3v2LyricLine newLine; |
| 130 | for (int i = 0; i < copyObject.lines.size(); i++) { |
| 131 | newLine = new ObjectID3v2LyricLine((ObjectID3v2LyricLine) copyObject.lines.get(i)); |
| 132 | this.lines.add(newLine); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | /** |
| 137 | * Creates a new FrameBodySYLT object. |
| 138 | */ |
| 139 | public FrameBodySYLT(final byte textEncoding, |
| 140 | final String language, |
| 141 | final byte timeStampFormat, |
| 142 | final byte contentType, |
| 143 | final String description) { |
| 144 | this.textEncoding = textEncoding; |
| 145 | this.language = language; |
| 146 | this.timeStampFormat = timeStampFormat; |
| 147 | this.contentType = contentType; |
| 148 | this.description = description; |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Creates a new FrameBodySYLT object. |
| 153 | */ |
| 154 | public FrameBodySYLT(final RandomAccessFile file) throws IOException, InvalidTagException { |
| 155 | this.read(file); |
| 156 | } |
| 157 | |
| 158 | public byte getContentType() { |
| 159 | return this.contentType; |
| 160 | } |
| 161 | |
| 162 | public String getDescription() { |
| 163 | return this.description; |
| 164 | } |
| 165 | |
| 166 | public String getIdentifier() { |
| 167 | return "SYLT"; |
| 168 | } |
| 169 | |
| 170 | public String getLanguage() { |
| 171 | return this.language; |
| 172 | } |
| 173 | |
| 174 | public String getLyric() { |
| 175 | String lyrics = ""; |
| 176 | for (int i = 0; i < this.lines.size(); i++) { |
| 177 | lyrics += this.lines.get(i); |
| 178 | } |
| 179 | return lyrics; |
| 180 | } |
| 181 | |
| 182 | public int getSize() { |
| 183 | int size; |
| 184 | size = 1 + 3 + 1 + 1 + this.description.length(); |
| 185 | for (int i = 0; i < this.lines.size(); i++) { |
| 186 | size += ((ObjectID3v2LyricLine) this.lines.get(i)).getSize(); |
| 187 | } |
| 188 | return size; |
| 189 | } |
| 190 | |
| 191 | public byte getTextEncoding() { |
| 192 | return this.textEncoding; |
| 193 | } |
| 194 | |
| 195 | public byte getTimeStampFormat() { |
| 196 | return this.timeStampFormat; |
| 197 | } |
| 198 | |
| 199 | public void addLyric(final int timeStamp, final String text) { |
| 200 | final ObjectID3v2LyricLine line = new ObjectID3v2LyricLine("Lyric Line"); |
| 201 | line.setTimeStamp(timeStamp); |
| 202 | line.setText(text); |
| 203 | this.lines.add(line); |
| 204 | } |
| 205 | |
| 206 | public void addLyric(final ObjectLyrics3Line line) { |
| 207 | final Iterator iterator = line.getTimeStamp(); |
| 208 | ObjectLyrics3TimeStamp timeStamp; |
| 209 | final String lyric = line.getLyric(); |
| 210 | long time; |
| 211 | final ObjectID3v2LyricLine id3Line; |
| 212 | id3Line = new ObjectID3v2LyricLine("Lyric Line"); |
| 213 | if (iterator.hasNext() == false) { |
| 214 | // no time stamp, give it 0 |
| 215 | time = 0; |
| 216 | id3Line.setTimeStamp(time); |
| 217 | id3Line.setText(lyric); |
| 218 | this.lines.add(id3Line); |
| 219 | } else { |
| 220 | while (iterator.hasNext()) { |
| 221 | timeStamp = (ObjectLyrics3TimeStamp) iterator.next(); |
| 222 | time = (timeStamp.getMinute() * 60) + timeStamp.getSecond(); // seconds |
| 223 | time *= 1000; // milliseconds |
| 224 | id3Line.setTimeStamp(time); |
| 225 | id3Line.setText(lyric); |
| 226 | this.lines.add(id3Line); |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * This method is not yet supported. |
| 233 | * |
| 234 | * @throws java.lang.UnsupportedOperationException |
| 235 | * This method is not yet supported |
| 236 | */ |
| 237 | public void equals() { |
| 238 | // todo Implement this java.lang.Object method |
| 239 | throw new java.lang.UnsupportedOperationException("Method equals() not yet implemented."); |
| 240 | } |
| 241 | |
| 242 | public Iterator iterator() { |
| 243 | return this.lines.iterator(); |
| 244 | } |
| 245 | |
| 246 | protected void setupObjectList() { |
| 247 | // throw new UnsupportedOperationException(); |
| 248 | } |
| 249 | |
| 250 | public void read(final RandomAccessFile file) throws IOException, InvalidTagException { |
| 251 | final int size; |
| 252 | final int delim; |
| 253 | int offset = 0; |
| 254 | final byte[] buffer; |
| 255 | final String str; |
| 256 | size = readHeader(file); |
| 257 | buffer = new byte[size]; |
| 258 | file.read(buffer); |
| 259 | str = new String(buffer); |
| 260 | this.textEncoding = buffer[offset++]; |
| 261 | this.language = str.substring(offset, offset + 3); |
| 262 | offset += 3; |
| 263 | this.timeStampFormat = buffer[offset++]; |
| 264 | this.contentType = buffer[offset++]; |
| 265 | delim = str.indexOf(0, offset); |
| 266 | this.description = str.substring(offset, delim); |
| 267 | offset = delim + 1; |
| 268 | final byte[] data = new byte[size - offset]; |
| 269 | System.arraycopy(buffer, offset, data, 0, size - offset); |
| 270 | readByteArray(data); |
| 271 | } |
| 272 | |
| 273 | public void readByteArray(final byte[] arr) { |
| 274 | int offset = 0; |
| 275 | int delim; |
| 276 | byte[] line; |
| 277 | for (int i = 0; i < arr.length; i++) { |
| 278 | if (arr[i] == 0) { |
| 279 | delim = i; |
| 280 | line = new byte[offset - delim + 4]; |
| 281 | System.arraycopy(arr, offset, line, 0, offset - delim + 4); |
| 282 | this.lines.add(new ObjectID3v2LyricLine("Lyric Line")); |
| 283 | i += 4; |
| 284 | offset += 4; |
| 285 | } |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | public String toString() { |
| 290 | String str; |
| 291 | str = getIdentifier() + " " + this |
| 292 | .textEncoding + " " + this |
| 293 | .language + " " + this |
| 294 | .timeStampFormat + " " + this |
| 295 | .contentType + " " + this |
| 296 | .description; |
| 297 | for (int i = 0; i < this.lines.size(); i++) { |
| 298 | str += (this.lines.get(i)).toString(); |
| 299 | } |
| 300 | return str; |
| 301 | } |
| 302 | |
| 303 | public void write(final RandomAccessFile file) throws IOException { |
| 304 | final byte[] buffer; |
| 305 | int offset = 0; |
| 306 | writeHeader(file, this.getSize()); |
| 307 | buffer = new byte[this.getSize()]; |
| 308 | buffer[offset++] = this.textEncoding; // text encoding; |
| 309 | this.language = TagUtility.truncate(this.language, 3); |
| 310 | for (int i = 0; i < this.language.length(); i++) { |
| 311 | buffer[i + offset] = (byte) this.language.charAt(i); |
| 312 | } |
| 313 | offset += this.language.length(); |
| 314 | buffer[offset++] = this.timeStampFormat; |
| 315 | buffer[offset++] = this.contentType; |
| 316 | for (int i = 0; i < this.description.length(); i++) { |
| 317 | buffer[i + offset] = (byte) this.description.charAt(i); |
| 318 | } |
| 319 | offset += this.description.length(); |
| 320 | buffer[offset++] = 0; // null character |
| 321 | System.arraycopy(writeByteArray(), 0, buffer, offset, buffer.length - offset); |
| 322 | file.write(buffer); |
| 323 | } |
| 324 | |
| 325 | public byte[] writeByteArray() { |
| 326 | final byte[] arr; |
| 327 | ObjectID3v2LyricLine line = null; |
| 328 | int offset = 0; |
| 329 | int size = 0; |
| 330 | for (int i = 0; i < this.lines.size(); i++) { |
| 331 | line = (ObjectID3v2LyricLine) this.lines.get(i); |
| 332 | size += line.getSize(); |
| 333 | } |
| 334 | arr = new byte[size]; |
| 335 | for (int i = 0; i < this.lines.size(); i++) { |
| 336 | line = (ObjectID3v2LyricLine) this.lines.get(i); |
| 337 | } |
| 338 | if (line != null) { |
| 339 | System.arraycopy(line.writeByteArray(), 0, arr, offset, line.getSize()); |
| 340 | offset += line.getSize(); |
| 341 | } |
| 342 | return arr; |
| 343 | } |
| 344 | } |