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 | } |