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.serialization;
018:
019: import java.io.IOException;
020:
021: import org.apache.cocoon.ProcessingException;
022: import org.apache.cocoon.components.midi.xmidi.Constants;
023: import org.apache.cocoon.components.midi.xmidi.Utils;
024: import org.xml.sax.Attributes;
025: import org.xml.sax.SAXException;
026:
027: /**
028: * Takes SAX Events and serializes them as a standard MIDI file.
029: *
030: * The MIDI file generation parts of this class are based on code from the XMidi project, written
031: * by Peter Arthur Loeb (http://www.palserv.com/XMidi/) and used with permission.
032: * The warranty disclaimer of the MIT license (http://www.opensource.org/licenses/mit-license.html)
033: * applies to Peter Arthur Loeb's code.
034: *
035: * @author <a href="mailto:mark.leicester@energyintellect.com">Mark Leicester</a>
036: * @author <a href="mailto:peter@palserv.com">Peter Loeb</a>
037: *
038: * @version CVS $Id: XMidiSerializer.java 433543 2006-08-22 06:22:54Z crossley $
039: */
040:
041: public class XMidiSerializer extends AbstractSerializer {
042: private final static String mimeType = "audio/x-midi";
043:
044: private final static int OUTSIDE_XMIDI = 0;
045: private final static int INSIDE_XMIDI = 1;
046: private final static int INSIDE_CHUNK = 2;
047: private final static int INSIDE_MTHD = 3;
048: private final static int INSIDE_MTRK = 4;
049: private final static int INSIDE_DELTA = 5;
050: private final static int INSIDE_STATUS = 6;
051: private final static int INSIDE_DELTA_CHANNEL = 7;
052: private final static int INSIDE_STATUS_CHANNEL = 8;
053:
054: private int expectedBytes;
055: private boolean preventDataWrite;
056: private int state;
057: private StringBuffer buffer;
058: private boolean buffering = false;
059:
060: /* (non-Javadoc)
061: * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
062: */
063: public void recycle() {
064: preventDataWrite = false;
065: state = OUTSIDE_XMIDI;
066: super .recycle();
067: }
068:
069: /* (non-Javadoc)
070: * @see org.apache.cocoon.sitemap.SitemapOutputComponent#getMimeType()
071: */
072: public String getMimeType() {
073: return (mimeType);
074: }
075:
076: /* (non-Javadoc)
077: * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
078: */
079: public void startElement(String namespaceURI, String localName,
080: String qName, Attributes atts) throws SAXException {
081: try {
082: if (localName.equals("XMidi")) {
083: if (state != OUTSIDE_XMIDI) {
084: throw new SAXException(
085: "XMidi element not expected here");
086: }
087: state = INSIDE_XMIDI;
088: String version = atts.getValue("VERSION");
089: if (version == null) {
090: throw new SAXException(
091: "XMidi element has no version attribute");
092: } else if (!version.equals(Constants.VERSION)) {
093: throw new SAXException(
094: "XMidi element has wrong version: expecting "
095: + Constants.VERSION + ", got "
096: + version);
097: }
098: this .getLogger().debug(
099: "Found XMidi element, version " + version);
100: } else if (localName.equals("CHUNK")) {
101: if (state != INSIDE_XMIDI) {
102: throw new SAXException(localName
103: + " element not expected here, state = "
104: + state);
105: }
106: state = INSIDE_CHUNK;
107: writeString(atts.getValue("TYPE"), 4);
108: Integer iLen = new Integer(atts.getValue("LENGTH"));
109: writeFullWord(iLen.intValue());
110: this .getLogger().debug(
111: "chunk type is: " + atts.getValue("TYPE")
112: + " with length "
113: + atts.getValue("LENGTH"));
114: } else if (localName.equals("MThd")) {
115: if (state != INSIDE_XMIDI) {
116: throw new SAXException(localName
117: + " element not expected here, state = "
118: + state);
119: }
120: state = INSIDE_MTHD;
121: writeString(atts.getValue("TYPE"), 4);
122: writeFullWord(Utils
123: .stringToInt(atts.getValue("LENGTH")));
124: this .getLogger().debug(
125: "we have MThd chunk; len = "
126: + atts.getValue("LENGTH"));
127: } else if (localName.equals("MTrk")) {
128: if (state != INSIDE_XMIDI) {
129: throw new SAXException(localName
130: + " element not expected here, state = "
131: + state);
132: }
133: state = INSIDE_MTRK;
134: writeString(atts.getValue("TYPE"), 4);
135: writeFullWord(Utils
136: .stringToInt(atts.getValue("LENGTH")));
137: this .getLogger().debug(
138: "we have MTrk chunk; len = "
139: + atts.getValue("LENGTH"));
140: } else if (localName.equals("DELTA")) {
141: if (state != INSIDE_MTRK) {
142: throw new SAXException(localName
143: + " element not expected here, state = "
144: + state);
145: }
146: state = INSIDE_DELTA;
147: String dtime = atts.getValue("DTIME");
148: byte[] hdt = Utils.hexToBa(dtime, 4);
149: byte[] dt = Utils.intToDelta(hdt);
150: this .getLogger().debug(
151: "Delta: " + dtime + ", out = "
152: + Utils.baToHex(dt, 0, dt.length - 1));
153: this .output.write(dt);
154:
155: } else if (localName.equals("STATUS")) {
156: if (state != INSIDE_DELTA) {
157: throw new SAXException(localName
158: + " element not expected here, state = "
159: + state);
160: }
161: state = INSIDE_STATUS;
162: String sval = atts.getValue("SVAL");
163: writeHex(sval, 1);
164: String sl = atts.getValue("SLEN");
165: expectedBytes = Utils.stringToInt(sl);
166: this .getLogger().debug(
167: "Status: " + sval + ", len = " + expectedBytes);
168: if (sval.equals("FF")) {
169: String nmd = atts.getValue("SNMT");
170: writeHex(nmd, 1);
171: byte[] hdt = Utils.intToBa(expectedBytes, 4);
172: byte[] xdt = Utils.intToDelta(hdt);
173: this .output.write(xdt);
174: if (expectedBytes == 0) {
175: preventDataWrite = true;
176: }
177: this .getLogger().debug("Non-midi: " + nmd);
178: } else if (sval.equals("F0")) {
179: byte[] hdt = Utils
180: .intToBa(Utils.stringToInt(sl), 4);
181: this .output.write(Utils.intToDelta(hdt));
182: this .getLogger().debug("Sysex");
183: } else if (sval.equals("F6") | sval.equals("F8")
184: | sval.equals("FA") | sval.equals("FB")
185: | sval.equals("FC") | sval.equals("FE")) {
186: preventDataWrite = true;
187: this .getLogger().debug("no data");
188: }
189: } else if (localName.equals("CHANNEL")) {
190: if ((state != INSIDE_DELTA) && (state != INSIDE_STATUS)) {
191: throw new SAXException(localName
192: + " element not expected here, state = "
193: + state);
194: }
195: if (state == INSIDE_DELTA) {
196: state = INSIDE_DELTA_CHANNEL;
197: } else if (state == INSIDE_STATUS) {
198: state = INSIDE_STATUS_CHANNEL;
199: }
200: this .getLogger().debug("Channel");
201: } else if (localName.equals("NOTE_OFF")) {
202: if ((state != INSIDE_DELTA_CHANNEL)
203: && (state != INSIDE_STATUS_CHANNEL)) {
204: throw new SAXException(localName
205: + " element not expected here, state = "
206: + state);
207: }
208: String pitch = atts.getValue("PITCH");
209: this .output.write(Utils.stringToInt(pitch));
210: String vel = atts.getValue("VELOCITY");
211: this .output.write(Utils.stringToInt(vel));
212: this .getLogger().debug(
213: "Note off - " + pitch + ", " + vel);
214: } else if (localName.equals("NOTE_ON")) {
215: if ((state != INSIDE_DELTA_CHANNEL)
216: && (state != INSIDE_STATUS_CHANNEL)) {
217: throw new SAXException(localName
218: + " element not expected here, state = "
219: + state);
220: }
221: String pitch = atts.getValue("PITCH");
222: this .output.write(Utils.stringToInt(pitch));
223: String vel = atts.getValue("VELOCITY");
224: this .output.write(Utils.stringToInt(vel));
225: this .getLogger().debug(
226: "Note on - " + pitch + ", " + vel);
227: } else if (localName.equals("AFTER")) {
228: if ((state != INSIDE_DELTA_CHANNEL)
229: && (state != INSIDE_STATUS_CHANNEL)) {
230: throw new SAXException(localName
231: + " element not expected here, state = "
232: + state);
233: }
234: String pitch = atts.getValue("PITCH");
235: this .output.write(Utils.stringToInt(pitch));
236: String pres = atts.getValue("PRESSURE");
237: this .output.write(Utils.stringToInt(pres));
238: this .getLogger()
239: .debug("AFTER - " + pitch + ", " + pres);
240: } else if (localName.equals("CONTROL")) {
241: if ((state != INSIDE_DELTA_CHANNEL)
242: && (state != INSIDE_STATUS_CHANNEL)) {
243: throw new SAXException(localName
244: + " element not expected here, state = "
245: + state);
246: }
247: String cnum = atts.getValue("NUMBER");
248: this .output.write(Utils.stringToInt(cnum));
249: String val = atts.getValue("VALUE");
250: this .output.write(Utils.stringToInt(val));
251: this .getLogger()
252: .debug("CONTROL - " + cnum + ", " + val);
253: } else if (localName.equals("PROGRAM")) {
254: if ((state != INSIDE_DELTA_CHANNEL)
255: && (state != INSIDE_STATUS_CHANNEL)) {
256: throw new SAXException(localName
257: + " element not expected here, state = "
258: + state);
259: }
260: String patch = atts.getValue("NUMBER");
261: this .output.write(Utils.stringToInt(patch));
262: this .getLogger().debug("PATCH - " + patch);
263: } else if (localName.equals("PRESSURE")) {
264: if ((state != INSIDE_DELTA_CHANNEL)
265: && (state != INSIDE_STATUS_CHANNEL)) {
266: throw new SAXException(localName
267: + " element not expected here, state = "
268: + state);
269: }
270:
271: String amt = atts.getValue("AMOUNT");
272: this .output.write(Utils.stringToInt(amt));
273: this .getLogger().debug("PRESSURE - " + amt);
274: } else if (localName.equals("WHEEL")) {
275: if ((state != INSIDE_DELTA_CHANNEL)
276: && (state != INSIDE_STATUS_CHANNEL)) {
277: throw new SAXException(localName
278: + " element not expected here, state = "
279: + state);
280: }
281:
282: String amt = atts.getValue("AMOUNT");
283: int a = Utils.stringToInt(amt);
284: int b = a;
285: int c = a;
286: b &= 127;
287: c >>= 7;
288: this .output.write(c);
289: this .output.write(b);
290: this .getLogger().debug(
291: "Wheel - " + a + ": (" + c + "," + a + ")");
292: } else if (localName.equals("EDATA")) {
293: if ((state != INSIDE_DELTA) && (state != INSIDE_STATUS)) {
294: throw new SAXException(localName
295: + " element not expected here, state = "
296: + state);
297: }
298: buffer = new StringBuffer();
299: buffering = true;
300: this .getLogger().debug("EDATA (element, not text)");
301: } else if (localName.equals("FORMAT")
302: || localName.equals("TRACKS")
303: || localName.equals("PPNQ")) {
304: if (state != INSIDE_MTHD) {
305: throw new SAXException(localName
306: + " element not expected here, state = "
307: + state);
308: }
309: buffer = new StringBuffer();
310: buffering = true;
311: this .getLogger().debug(localName + " element");
312: } else {
313: this .getLogger().debug(
314: "Found " + localName + ", in state " + state);
315: }
316:
317: } catch (ProcessingException e) {
318: throw new SAXException(e);
319: } catch (IOException e) {
320: throw new SAXException(e);
321: }
322: }
323:
324: /* (non-Javadoc)
325: * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
326: */
327: public void endElement(String namespaceURI, String localName,
328: String qName) throws SAXException {
329: try {
330: if (localName.equals("CHUNK")) {
331: state = INSIDE_XMIDI;
332: } else if (localName.equals("MThd")
333: || localName.equals("MTrk")) {
334: state = INSIDE_XMIDI;
335: } else if (localName.equals("DELTA")) {
336: state = INSIDE_MTRK;
337: } else if (localName.equals("STATUS")) {
338: state = INSIDE_DELTA;
339: } else if (localName.equals("CHANNEL")) {
340: if (state == INSIDE_STATUS_CHANNEL) {
341: state = INSIDE_STATUS;
342: } else if (state == INSIDE_DELTA_CHANNEL) {
343: state = INSIDE_DELTA;
344: }
345: } else if (localName.equals("EDATA")) {
346: if (!preventDataWrite) {
347: writeHex(buffer.toString(), expectedBytes);
348: this .getLogger().debug(
349: "EDATA: " + buffer.toString());
350: } else {
351: preventDataWrite = false;
352: }
353: buffering = false;
354: } else if (localName.equals("FORMAT")) {
355: String typ = buffer.toString();
356: for (int i = 0; i < typ.length(); i++) {
357: if (typ.substring(i, i + 1).compareTo("0") < 0
358: || typ.substring(i, i + 1).compareTo("9") > 0) {
359: throw new ProcessingException(
360: "Invalid numeric midi format: " + typ);
361: }
362: }
363: int midiFormat = Utils.stringToInt(typ);
364: writeHalfWord(midiFormat);
365: this .getLogger().debug("Format is " + midiFormat);
366: buffering = false;
367: } else if (localName.equals("TRACKS")) {
368: String sNum = buffer.toString();
369: Integer iNum = new Integer(sNum);
370: writeHalfWord(iNum.intValue());
371: this .getLogger().debug(iNum + " tracks");
372: buffering = false;
373: } else if (localName.equals("PPNQ")) {
374: String sPNQ = buffer.toString();
375: writeHex(sPNQ, 2);
376: this .getLogger().debug("PPNQ is " + sPNQ);
377: buffering = false;
378: } else if (localName.equals("HEXDATA")) {
379: writeHex(buffer.toString(), buffer.length() / 2);
380: buffering = false;
381: }
382: } catch (ProcessingException e) {
383: throw new SAXException(e);
384: } catch (IOException e) {
385: throw new SAXException(e);
386: }
387: }
388:
389: void writeString(String s, int len) throws IOException,
390: ProcessingException {
391: int l = s.length();
392: if (l != len) {
393: throw new ProcessingException(
394: "writeString; string length (" + l
395: + ") != expected length (" + len + ")");
396: }
397: this .output.write(s.getBytes());
398: }
399:
400: void writeHex(String h, int len) throws ProcessingException,
401: IOException {
402: int l = h.length();
403: int cnt = 0;
404: int bs = 0;
405: int bc = 0;
406: for (int i = 0; i < l; i++) {
407: String s = h.substring(i, i + 1);
408: int x = " \n\t\r".indexOf(s);
409: if (x != -1) {
410: continue;
411: }
412: int tmp = "0123456789ABCDEF".indexOf(s.toUpperCase());
413: if (bc == 0) {
414: bs = tmp;
415: } else if (bc == 1) {
416: bs <<= 4;
417: bs |= tmp;
418: } else {
419: throw new ProcessingException(
420: "writeHex; internal error");
421: }
422: bc++;
423: if (bc >= 2) {
424: this .output.write(bs);
425: cnt++;
426: bc = 0;
427: }
428: }
429: if (bc != 0) {
430: throw new ProcessingException(
431: "un-even number of hex digits");
432: }
433: if (cnt != len) {
434: throw new ProcessingException("writeHex count (" + cnt
435: + ") != length (" + len + ")");
436: }
437: }
438:
439: void writeFullWord(int f) throws ProcessingException, IOException {
440: byte[] b = Utils.intToBa(f, 4);
441: this .output.write(b);
442: }
443:
444: void writeHalfWord(int h) throws ProcessingException, IOException {
445: byte[] b = Utils.intToBa(h, 2);
446: this .output.write(b);
447: }
448:
449: /* (non-Javadoc)
450: * @see org.xml.sax.ContentHandler#characters(char[], int, int)
451: */
452: public void characters(char[] str, int arg1, int arg2)
453: throws SAXException {
454: if (buffering) {
455: buffer.append(str, arg1, arg2);
456: }
457: }
458:
459: }
|