Source code: org/esau/ptarmigan/impl/filter/VorbisFilter.java
1 /* $Header: /cvsroot/ptarmigan/ptarmigan/src/java/org/esau/ptarmigan/impl/filter/VorbisFilter.java,v 1.4 2002/09/19 17:51:47 reedesau Exp $ */
2
3 package org.esau.ptarmigan.impl.filter;
4
5 import java.io.IOException;
6 import java.text.ParseException;
7
8 import org.xml.sax.SAXException;
9
10 import org.apache.commons.logging.Log;
11 import org.apache.commons.logging.LogFactory;
12
13 /**
14 * Extract metadata from an OGG Vorbis native tag format
15 *
16 * Following the three header packets, all packets in a Vorbis I stream
17 * are audio.
18 *
19 * @author Reed Esau
20 * @author Philip Gladstone
21 */
22 public final class VorbisFilter extends VorbisCommentFilter {
23
24 public VorbisFilter() throws SAXException {
25
26 //NOTE: don't declare mime-type in ctor because we might be reading
27 // vorbis tags from FLAC or some other format
28 }
29
30 String getNamespaceURI() {
31 return NS_URI;
32 }
33
34 String getNamespacePrefix() {
35 return NS_PREFIX;
36 }
37
38 /**
39 * SAX-Invoked parse of a 'document' from an input stream.
40 * <p>
41 * This module creates the document from the parsed Vorbis metadata.
42 */
43 public void doParse() throws SAXException, IOException, ParseException {
44
45 log.debug("doParse");
46
47 // resetData
48 m_bit_rate_nominal = -1;
49 m_channel_mode = 0;
50
51 // look for an initial marker
52 boolean marker_found = readMarker();
53 rewind(0);
54 if (marker_found == false)
55 return;
56
57 m_media_properties.setMimeType("audio/x-ogg");
58
59 final String local_name = "vorbis";
60 final String q_name = NS_PREFIX + ":" + local_name;
61
62 startElement(NS_URI, local_name, q_name, EMPTY_ATTRS);
63
64 while (true) {
65 int pos = counter();
66
67 marker_found = readMarker();
68 if (marker_found == false) {
69 rewind(pos); // just before start of data
70 break;
71 }
72
73 if (readPage() == false)
74 break;
75 }
76
77 endElement(NS_URI, local_name, q_name);
78
79 m_media_properties.getRange().increaseOffset(counter());
80
81 // ESTIMATE the duration, if all pieces are available
82 // TODO: if a file, allow for calculation of actual duration
83 long data_size = m_media_properties.getRange().getLength();
84 if (data_size > 0
85 && m_bit_rate_nominal > 0
86 && m_channel_mode > 0) {
87
88 long dur = ( (16 * 1000 * data_size)
89 / m_bit_rate_nominal
90 / m_channel_mode);
91
92 m_media_properties.setDuration(dur); // units are millisecs
93 }
94 }
95
96
97 /**
98 * InputStream should be positioned following the OggS prefix
99 *
100 * There are one or more packets on a page.
101 *
102 * The segment values are used to calculate the size of each packet.
103 */
104 boolean readPage()
105 throws IOException, SAXException, ParseException {
106
107 if (log.isDebugEnabled())
108 log.debug("readPage: counter=" + counter());
109
110 byte hdr[] = new byte[22];
111
112 if (read(hdr) < hdr.length)
113 throw new ParseException("unable to read packet header", 0);
114
115 int segments = read();
116
117 byte[] seglist = new byte[segments];
118 if (read(seglist) < seglist.length)
119 throw new ParseException("unable to read seglist array", 0);
120
121 int curseglen = 0;
122
123 for (int i = 0; i < segments; i++) {
124
125 curseglen += seglist[i] & 0xff;
126
127 boolean is_last = (i == (segments-1));
128
129 if ((seglist[i] & 0xff) != 0xff || is_last ) {
130
131 // we've calculated a total packet size; read that packet
132
133 if (readPacket(curseglen) == false)
134 return false;
135
136 curseglen = 0;
137 }
138 }
139
140 return true;
141 }
142
143
144 /**
145 * read the next packet
146 *
147 * @return false if the beginning of data is found
148 */
149 boolean readPacket(int size)
150 throws IOException, SAXException, ParseException {
151
152 if (log.isDebugEnabled())
153 log.debug("readPacket: counter=" + counter() + " size=" + size);
154
155 int packet_type = read();
156
157 if (log.isDebugEnabled())
158 log.debug("readPacket: type=" + packet_type);
159
160 final int remaining = (size - 1);
161
162 switch (packet_type) {
163 case PACKET_IDENTIFICATION:
164 readIdentificationPacket(remaining);
165 break;
166 case PACKET_COMMENT:
167 readCommentPacket(remaining);
168 break;
169 default:
170 if (log.isDebugEnabled())
171 log.debug("readPacket: probably reached 'vorbis' + start of audio pos=" + counter());
172 return false; // no more to be read
173 }
174 return true;
175 }
176
177
178 //
179 // internal methods
180 //
181
182
183 /** read identification header starting at the current position in the InputStream */
184 void readIdentificationPacket(int available)
185 throws IOException, SAXException, ParseException {
186
187 if (log.isDebugEnabled())
188 log.debug("readIdentificationPacket: available=" + available + " pos=" + counter());
189
190 checkForVorbis();
191
192 int bitstream_version = readInt32LE();
193 m_channel_mode = read();
194 int sample_rate = readInt32LE();
195 int bit_rate_maximal = readInt32LE();
196 m_bit_rate_nominal = readInt32LE();
197 int bit_rate_minimal = readInt32LE();
198 int block_size = read();
199 int stop_flag = read();
200
201 final String local_name = "identification";
202 final String q_name = NS_PREFIX + ":" + local_name;
203
204 startElement(NS_URI, local_name, q_name, EMPTY_ATTRS);
205
206 write("bitstream-version", bitstream_version);
207 write("channel-mode", m_channel_mode);
208 write("sample-rate", sample_rate);
209 if (bit_rate_maximal > 0)
210 write("bit-rate-maximal", bit_rate_maximal);
211 if (bit_rate_minimal > 0)
212 write("bit-rate-minimal", bit_rate_minimal);
213 if (m_bit_rate_nominal > 0)
214 write("bit-rate-nominal", m_bit_rate_nominal);
215 write("block-size", block_size);
216 write("stop-flag", stop_flag);
217
218 endElement(NS_URI, local_name, q_name);
219 }
220
221 /** read comments starting at the current position in the InputStream */
222 void readCommentPacket(int available)
223 throws IOException, SAXException, ParseException {
224
225 if (log.isDebugEnabled())
226 log.debug("readCommentPacket: available=" + available + " pos=" + counter());
227
228 if (available > MAX_COMMENT_SIZE)
229 throw new ParseException("allowable comment packet size exceeded", counter());
230
231 int total = 0;
232
233 int bytes_read = checkForVorbis();
234 total += bytes_read;
235
236 byte[] buf = new byte[2048]; // of arbitrary size
237
238 total += readCommentSubPacket();
239
240 int framing_byte = read();
241 total++;
242 if (framing_byte != 1)
243 log.warn("readCommentPacket: framing=" + framing_byte);
244
245 // reality-check the packet read
246
247 if (total < available) {
248 log.warn("readCommentPacket: recovery from underflow");
249 fastForward(available - total);
250 }
251 else if (available < total) {
252 throw new ParseException("packet specified more bytes than was available in page!", counter());
253 }
254 }
255
256 /** consume six (6) bytes of the InputStream to verify that they match 'vorbis' */
257 int checkForVorbis() throws IOException, ParseException {
258
259 byte[] buf = new byte[6];
260 read(buf);
261 if (buf[0] != 'v' || buf[1] != 'o' || buf[2] != 'r'
262 || buf[3] != 'b' || buf[4] != 'i' || buf[5] != 's')
263 throw new ParseException("expecting vorbis", 0); //TODO: supply the position
264
265 return buf.length;
266 }
267
268 /** */
269 boolean readMarker() throws IOException {
270 return(read() == 'O'
271 && read() == 'g'
272 && read() == 'g'
273 && read() == 'S');
274 }
275
276 //
277 // data members
278 //
279
280 int m_bit_rate_nominal;
281 int m_channel_mode;
282
283 //
284 // constants
285 //
286
287 // static final String NS_URI = "http://esau.org/ns/ptarmigan/vorbis";
288 // static final String NS_PREFIX = "ogg";
289
290 static final int PACKET_IDENTIFICATION = 1;
291 static final int PACKET_COMMENT = 3;
292 static final int PACKET_SETUP = 5;
293
294 static final String ENCODING = "UTF-8";
295
296 /**
297 * logging object
298 */
299 static Log log = LogFactory.getLog(VorbisFilter.class);
300 }
301
302 /*
303 PTARMIGAN MODIFIED BSD LICENSE
304
305 Copyright (c) 2002, Reed Esau (reed.esau@pobox.com) All rights reserved.
306
307 Redistribution and use in source and binary forms, with or without
308 modification, are permitted provided that the following conditions are
309 met:
310
311 Redistributions of source code must retain the above copyright notice,
312 this list of conditions and the following disclaimer.
313
314 Redistributions in binary form must reproduce the above copyright notice,
315 this list of conditions and the following disclaimer in the documentation
316 and/or other materials provided with the distribution.
317
318 Neither the name of the Ptarmigan Project
319 (http://ptarmigan.sourceforge.net) nor the names of its contributors may
320 be used to endorse or promote products derived from this software without
321 specific prior written permission.
322
323 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
324 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
325 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
326 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
327 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
328 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
329 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
330 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
331 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
332 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
333 POSSIBILITY OF SUCH DAMAGE.
334 */