001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.generation;
018:
019: import org.xml.sax.SAXException;
020:
021: import java.io.File;
022: import java.io.IOException;
023: import java.io.RandomAccessFile;
024:
025: /**
026: * @cocoon.sitemap.component.documentation
027: * An extension of DirectoryGenerators that adds extra attributes for MP3
028: * files.
029: *
030: * @cocoon.sitemap.component.name mp3directory
031: * @cocoon.sitemap.component.label content
032: * @cocoon.sitemap.component.logger sitemap.generator.mp3directory
033: * @cocoon.sitemap.component.documentation.caching
034: * Uses the last modification date of the directory and the contained files
035: *
036: *
037: * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a>
038: * @version CVS $Id: MP3DirectoryGenerator.java 433543 2006-08-22 06:22:54Z crossley $
039: */
040: public class MP3DirectoryGenerator extends DirectoryGenerator {
041: // MP3 Constants
042: private static final int VERSION_MPEG1 = 3;
043: // private static final int VERSION_MPEG2 = 2;
044: private static final int MODE_DUAL_CHANNEL = 2;
045: private static final int MODE_JOINT_STEREO = 1;
046: private static final int MODE_SINGLE_CHANNEL = 3;
047: private static final int MODE_STEREO = 0;
048: private static final int VBR_FRAMES_FLAG = 1;
049: // private static final int VBR_BYTES_FLAG = 2;
050: // private static final int VBR_TOC_FLAG = 4;
051: // private static final int VBR_SCALE_FLAG = 8;
052:
053: // Attributes
054: protected final static String MP3_FREQUENCY_ATTR_NAME = "frequency";
055: protected final static String MP3_BITRATE_ATTR_NAME = "bitrate";
056: protected final static String MP3_MODE_ATTR_NAME = "mode";
057: protected final static String MP3_VBR_ATTR_NAME = "variable-rate";
058:
059: protected final static String MP3_TITLE_ATTR_NAME = "title";
060: protected final static String MP3_ARTIST_ATTR_NAME = "artist";
061: protected final static String MP3_ALBUM_ATTR_NAME = "album";
062: protected final static String MP3_YEAR_ATTR_NAME = "year";
063: protected final static String MP3_COMMENT_ATTR_NAME = "comment";
064: protected final static String MP3_TRACK_ATTR_NAME = "track";
065: protected final static String MP3_GENRE_ATTR_NAME = "genre";
066:
067: /**
068: * Extends the <code>setNodeAttributes</code> method from the
069: * <code>DirectoryGenerator</code> by adding MP3 tag attributes
070: * if the path is a MP3 file with valid tag.
071: */
072: protected void setNodeAttributes(File path) throws SAXException {
073: super .setNodeAttributes(path);
074: if (path.isDirectory()) {
075: return;
076: }
077:
078: RandomAccessFile in = null;
079: try {
080: in = new RandomAccessFile(path, "r");
081: setID3HeaderAttributes(in);
082: setID3TagAttributes(in);
083: } catch (IOException e) {
084: getLogger()
085: .debug("Could not set attributes for " + path, e);
086: } finally {
087: if (in != null)
088: try {
089: in.close();
090: } catch (IOException ignored) {
091: }
092: }
093: }
094:
095: /**
096: * Read ID3 Tag
097: */
098: private void setID3TagAttributes(RandomAccessFile in)
099: throws IOException {
100: String s;
101:
102: // TAG takes 128 bytes
103: if (in.length() < 128)
104: return;
105: in.seek(in.length() - 128);
106: byte[] buf = new byte[128];
107: // Read TAG
108: if (in.read(buf, 0, 128) != 128)
109: return;
110: // Check TAG presence
111: if (buf[0] != 'T' || buf[1] != 'A' || buf[2] != 'G')
112: return;
113:
114: s = getID3TagValue(buf, 3, 30);
115: if (s.length() > 0)
116: attributes.addAttribute("", MP3_TITLE_ATTR_NAME,
117: MP3_TITLE_ATTR_NAME, "CDATA", s);
118: s = getID3TagValue(buf, 33, 30);
119: if (s.length() > 0)
120: attributes.addAttribute("", MP3_ARTIST_ATTR_NAME,
121: MP3_ARTIST_ATTR_NAME, "CDATA", s);
122: s = getID3TagValue(buf, 63, 30);
123: if (s.length() > 0)
124: attributes.addAttribute("", MP3_ALBUM_ATTR_NAME,
125: MP3_ALBUM_ATTR_NAME, "CDATA", s);
126: s = getID3TagValue(buf, 93, 4);
127: if (s.length() > 0)
128: attributes.addAttribute("", MP3_YEAR_ATTR_NAME,
129: MP3_YEAR_ATTR_NAME, "CDATA", s);
130: s = getID3TagValue(buf, 97, 29);
131: if (s.length() > 0)
132: attributes.addAttribute("", MP3_COMMENT_ATTR_NAME,
133: MP3_COMMENT_ATTR_NAME, "CDATA", s);
134: if (buf[126] > 0)
135: attributes.addAttribute("", MP3_TRACK_ATTR_NAME,
136: MP3_TRACK_ATTR_NAME, "CDATA", Byte
137: .toString(buf[126]));
138: if (buf[127] > 0)
139: attributes.addAttribute("", MP3_GENRE_ATTR_NAME,
140: MP3_GENRE_ATTR_NAME, "CDATA", Byte
141: .toString(buf[127]));
142: }
143:
144: private String getID3TagValue(byte[] buf, int offset, int length) {
145: String s = new String(buf, offset, length);
146: int index = s.indexOf(0x00);
147: if (index != -1) {
148: s = s.substring(0, index);
149: }
150: return s.trim();
151: }
152:
153: private void setID3HeaderAttributes(RandomAccessFile in)
154: throws IOException {
155: byte[] buffer = new byte[4];
156:
157: // http://floach.pimpin.net/grd/mp3info/frmheader/index.html
158: if (in.read(buffer, 0, 3) != 3) {
159: return;
160: }
161: int header = ((buffer[0] << 16) & 0x00FF0000)
162: | ((buffer[1] << 8) & 0x0000FF00)
163: | ((buffer[2] << 0) & 0x000000FF);
164: do {
165: header <<= 8;
166: if (in.read(buffer, 3, 1) != 1) {
167: return;
168: }
169: header |= (buffer[3] & 0x000000FF);
170: } while (!isSyncMark(header));
171:
172: int version = (header >>> 19) & 3;
173: int layer = 4 - (header >>> 17) & 3;
174: // int protection = (header >>> 16) & 1;
175: int bitrate = (header >>> 12) & 0xF;
176: int frequency = (header >>> 10) & 3;
177: // Value 3 is reserved
178: if (frequency == 3) {
179: return;
180: }
181: // int padding = (header >>> 9) & 1;
182: int mode = ((header >>> 6) & 3);
183:
184: attributes.addAttribute("", MP3_FREQUENCY_ATTR_NAME,
185: MP3_FREQUENCY_ATTR_NAME, "CDATA", frequencyString(
186: version, frequency));
187: attributes.addAttribute("", MP3_MODE_ATTR_NAME,
188: MP3_MODE_ATTR_NAME, "CDATA", mode(mode));
189:
190: int frames = getVBRHeaderFrames(in, version, mode);
191: if (frames != -1) {
192: // get average frame size by deviding fileSize by the number of frames
193: float medFrameSize = (float) in.length() / frames;
194: // This does not work properly: (version == VERSION_MPEG1? 12000.0:144000.0)
195: bitrate = (int) (medFrameSize
196: * frequency(version, frequency) / 144000.0);
197: attributes.addAttribute("", MP3_BITRATE_ATTR_NAME,
198: MP3_BITRATE_ATTR_NAME, "CDATA", Integer
199: .toString(bitrate));
200: } else {
201: attributes.addAttribute("", MP3_BITRATE_ATTR_NAME,
202: MP3_BITRATE_ATTR_NAME, "CDATA", bitrate(version,
203: layer, bitrate));
204: }
205: }
206:
207: private static boolean isSyncMark(int header) {
208: boolean sync = ((header & 0xFFF00000) == 0xFFF00000);
209: // filter out invalid sample rate
210: if (sync)
211: sync = ((header >>> 10) & 3) != 3;
212: // filter out invalid layer
213: if (sync)
214: sync = ((header >>> 17) & 3) != 0;
215: // filter out invalid version
216: if (sync)
217: sync = ((header >>> 19) & 3) != 1;
218: return sync;
219: }
220:
221: private int getVBRHeaderFrames(RandomAccessFile in, int version,
222: int mode) throws IOException {
223: byte[] buffer = new byte[12];
224:
225: // Try to detect VBR header
226: int skip;
227: if (version == VERSION_MPEG1) {
228: if (mode == MODE_SINGLE_CHANNEL)
229: skip = 17;
230: else
231: skip = 32;
232: } else { // mpeg version 2 or 2.5
233: if (mode == MODE_SINGLE_CHANNEL)
234: skip = 9;
235: else
236: skip = 17;
237: }
238: while (skip > 0) {
239: if (in.read() == -1)
240: return -1;
241: skip--;
242: }
243:
244: if (in.read(buffer, 0, 12) != 12) {
245: return -1;
246: }
247: if (buffer[0] != 'X' || buffer[1] != 'i' || buffer[2] != 'n'
248: || buffer[3] != 'g') {
249: return -1;
250: }
251:
252: attributes.addAttribute("", MP3_VBR_ATTR_NAME,
253: MP3_VBR_ATTR_NAME, "CDATA", "yes");
254:
255: int flags = ((buffer[4] & 0xFF) << 24)
256: | ((buffer[5] & 0xFF) << 16)
257: | ((buffer[6] & 0xFF) << 8) | (buffer[7] & 0xFF);
258:
259: if ((flags & VBR_FRAMES_FLAG) == VBR_FRAMES_FLAG) {
260: int frames = ((buffer[8] & 0xFF) << 24)
261: | ((buffer[9] & 0xFF) << 16)
262: | ((buffer[10] & 0xFF) << 8) | (buffer[11] & 0xFF);
263: return frames;
264: } else {
265: return -1;
266: }
267: }
268:
269: // version - layer - bitrate index
270: private static final String bitrates[][][] = {
271: {
272: // MPEG2 - layer 1
273: { "free format", "32", "48", "56", "64", "80",
274: "96", "112", "128", "144", "160", "176",
275: "192", "224", "256", "forbidden" },
276: // MPEG2 - layer 2
277: { "free format", "8", "16", "24", "32", "40", "48",
278: "56", "64", "80", "96", "112", "128",
279: "144", "160", "forbidden" },
280: // MPEG2 - layer 3
281: { "free format", "8", "16", "24", "32", "40", "48",
282: "56", "64", "80", "96", "112", "128",
283: "144", "160", "forbidden" } },
284: {
285: // MPEG1 - layer 1
286: { "free format", "32", "64", "96", "128", "160",
287: "192", "224", "256", "288", "320", "352",
288: "384", "416", "448", "forbidden" },
289: // MPEG1 - layer 2
290: { "free format", "32", "48", "56", "64", "80",
291: "96", "112", "128", "160", "192", "224",
292: "256", "320", "384", "forbidden" },
293: // MPEG1 - layer 3
294: { "free format", "32", "40", "48", "56", "64",
295: "80", "96", "112", "128", "160", "192",
296: "224", "256", "320", "forbidden" } } };
297:
298: private static String bitrate(int version, int layer,
299: int bitrate_index) {
300: return bitrates[version & 1][layer - 1][bitrate_index];
301: }
302:
303: private static String mode(int mode) {
304: switch (mode) {
305: case MODE_STEREO:
306: return "Stereo";
307: case MODE_JOINT_STEREO:
308: return "Joint stereo";
309: case MODE_DUAL_CHANNEL:
310: return "Dual channel";
311: case MODE_SINGLE_CHANNEL:
312: return "Single channel";
313: }
314: return null;
315: }
316:
317: private static final int frequencies[][] = {
318: { 32000, 16000, 8000 }, //MPEG 2.5
319: { 0, 0, 0 }, //reserved
320: { 22050, 24000, 16000 }, //MPEG 2
321: { 44100, 48000, 32000 } //MPEG 1
322: };
323:
324: private static int frequency(int version, int frequency) {
325: return frequencies[version][frequency];
326: }
327:
328: private static String frequencyString(int version, int frequency) {
329: return String
330: .valueOf((float) frequency(version, frequency) / 1000);
331: }
332: }
|