001: /*
002: * Copyright (c) 2007, Sun Microsystems, Inc.
003: *
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * * Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: * * Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: * * Neither the name of Sun Microsystems, Inc. nor the names of its
017: * contributors may be used to endorse or promote products derived
018: * from this software without specific prior written permission.
019: *
020: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
021: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
022: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
023: * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
024: * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
025: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
026: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
027: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
028: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
029: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
030: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
031: */
032: package example.mmademo;
033:
034: import java.util.*;
035: import java.io.*;
036: import javax.microedition.io.*;
037: import javax.microedition.media.control.*;
038:
039: /**
040: * Converts ring tone files to the MMAPI tone player format.
041: * Those file as can be downloaded from the Internet, e.g.
042: * http://www.surgeryofsound.co.uk/
043: * http://www.convertyourtone.com/
044: * http://www.filmfind.tv/ringtones/
045: *
046: * @version 1.4
047: */
048: public class RingToneConverter {
049: // internal parser data
050: private byte[] inputData;
051: private int readPos;
052: private Vector notes;
053: private Vector lengths;
054: private char lastSeparator;
055: private int tempo = 80; // in beats per second
056:
057: // output data
058: private String name;
059: private byte[] sequence;
060:
061: /**
062: * Tries to convert the passed file. The entire contents are read
063: * into memory and then different parsers are tried.
064: *
065: * @exception IOException - on read error
066: * @exception Exception - if <code>url</code> does not contain a valid ring tone text file
067: */
068: public RingToneConverter(String url) throws IOException, Exception {
069: this (url, URL2Name(url));
070: }
071:
072: public RingToneConverter(String url, String name)
073: throws IOException, Exception {
074: this (Connector.openInputStream(url), name);
075: }
076:
077: public RingToneConverter(InputStream is, String name)
078: throws IOException, Exception {
079: this (readInputStream(is), name);
080: }
081:
082: public RingToneConverter(byte[] data, String name) throws Exception {
083: this .inputData = data;
084: this .name = name;
085: notes = new Vector();
086: lengths = new Vector();
087: boolean success = parseRTTTL();
088: if (!success) {
089: throw new Exception("Not a supported ringtone text file");
090: }
091: if (tempo < 20 || tempo > 508) {
092: throw new Exception("tempo is out of range");
093: }
094: inputData = null;
095: System.gc();
096: sequence = new byte[notes.size() * 2 + 4];
097: sequence[0] = ToneControl.VERSION;
098: sequence[1] = 1;
099: sequence[2] = ToneControl.TEMPO;
100: sequence[3] = (byte) ((tempo >> 2) & 0x7f);
101: for (int i = 0; i < notes.size(); i++) {
102: sequence[2 * i + 4] = (byte) (((Integer) notes.elementAt(i))
103: .intValue() & 0xff);
104: sequence[2 * i + 5] = (byte) (((Integer) lengths
105: .elementAt(i)).intValue() & 0x7f);
106: }
107: notes = null;
108: lengths = null;
109: System.gc();
110: }
111:
112: public String getName() {
113: return name;
114: }
115:
116: public byte[] getSequence() {
117: return sequence;
118: }
119:
120: /**
121: * Dump the sequence as hexadecimal
122: * numbers to standard out. This
123: * can be used to create .jts files
124: * from RTTTL files.
125: */
126: public void dumpSequence() {
127: String[] hexChars = { "0", "1", "2", "3", "4", "5", "6", "7",
128: "8", "9", "A", "B", "C", "D", "E", "F" };
129: for (int i = 0; i < sequence.length; i++) {
130: System.out.print(hexChars[(sequence[i] & 0xF0) >> 4]
131: + hexChars[sequence[i] & 0xF] + " ");
132: if (i % 8 == 7)
133: System.out.println("");
134: }
135: System.out.println("");
136: }
137:
138: // note: strings must be sorted in descending order of their length
139: private static final String[] durationStrings = { "16", "32", "1",
140: "2", "4", "8" };
141: private static final int[] durationValues = { 16, 32, 1, 2, 4, 8 };
142: private static final String[] noteStrings = { "C#", "D#", "F#",
143: "G#", "A#", "C", "D", "E", "F", "G", "A", "H", "B" };
144: private static final int[] noteValues = { 1, 3, 6, 8, 10, 0, 2, 4,
145: 5, 7, 9, 11, 11 }; // H (German) == B (English)
146: private static final String[] scaleStrings = { "4", "5", "6", "7",
147: "8" };
148: private static final int[] scaleValues = { 4, 5, 6, 7, 8 };
149:
150: /**
151: * Parse a ringtone sequence in RTTTL
152: * (Ringing Tones text transfer language) format.
153: * The format is explained at
154: * http://www.convertyourtone.com/rtttl.html .
155: *
156: * Example:
157: * Entertainer:d=4, o=5, b=140:8d, 8d#, 8e, c6, 8e, c6, 8e,
158: * 2c.6, 8c6, 8d6, 8d#6, 8e6, 8c6, 8d6, e6, 8b, d6, 2c6, p,
159: * 8d, 8d#, 8e, c6, 8e, c6, 8e, 2c.6, 8p, 8a, 8g, 8f#, 8a,
160: * 8c6, e6, 8d6, 8c6, 8a, 2d6
161: *
162: * @return true if parsing was successful
163: */
164: private boolean parseRTTTL() {
165: boolean result = true;
166: try {
167: // default tempo is 63
168: tempo = 63;
169: // default duration is a quarter note
170: int defDuration = 4;
171: // default octave is 6
172: int defScale = 6;
173:
174: // start with Name, followed by colon :
175: String songName = readString(":", false, false);
176: if (songName.length() > 0) {
177: name = songName;
178: }
179: // read defaults
180: do {
181: String def = readString(",:", true, true);
182: if (def != "") {
183: if (def.startsWith("D=")) {
184: defDuration = Integer
185: .parseInt(def.substring(2));
186: } else if (def.startsWith("O=")) {
187: defScale = Integer.parseInt(def.substring(2));
188: } else if (def.startsWith("B=")) {
189: tempo = Integer.parseInt(def.substring(2));
190: } else {
191: throw new Exception("Unknown default \"" + def
192: + "\"");
193: }
194: } else {
195: if (lastSeparator != ':') {
196: throw new Exception("':' excepted");
197: }
198: break;
199: }
200: } while (lastSeparator == ',');
201:
202: // read note commands
203: StringBuffer noteCommand = new StringBuffer();
204: while (lastSeparator != 'E') {
205: noteCommand.setLength(0);
206: noteCommand.append(readString(",", true, true));
207: if (noteCommand.length() == 0) {
208: break;
209: }
210: // get duration
211: int duration = tableLookup(noteCommand,
212: durationStrings, durationValues, defDuration);
213:
214: // get note
215: int note = tableLookup(noteCommand, noteStrings,
216: noteValues, -1);
217:
218: // dotted duration ?
219: int dotCount = 0;
220: // dot may appear before or after scale
221: if (noteCommand.length() > 0
222: && noteCommand.charAt(0) == '.') {
223: dotCount = 1;
224: noteCommand.deleteCharAt(0);
225: }
226: if (note >= 0) {
227: // octave
228: int scale = tableLookup(noteCommand, scaleStrings,
229: scaleValues, defScale);
230: note = ToneControl.C4 + ((scale - 4) * 12) + note;
231: } else {
232: // pause ?
233: if (noteCommand.charAt(0) == 'P') {
234: note = ToneControl.SILENCE;
235: noteCommand.deleteCharAt(0);
236: } else {
237: throw new Exception(
238: "unexpected note command: '"
239: + noteCommand.toString() + "'");
240: }
241: }
242: // dot may appear before or after scale
243: if (noteCommand.length() > 0
244: && noteCommand.charAt(0) == '.') {
245: dotCount = 1;
246: noteCommand.deleteCharAt(0);
247: }
248: if (noteCommand.length() > 0) {
249: throw new Exception("unexpected note command: '"
250: + noteCommand.toString() + "'");
251: }
252: addNote(note, duration, dotCount);
253: }
254: Utils.debugOut("RingToneConverter: read " + notes.size()
255: + " notes successfully.");
256: } catch (Exception e) {
257: Utils.debugOut(e);
258: result = false;
259: }
260: return result;
261: }
262:
263: // utility methods
264:
265: private String readString(String separators,
266: boolean stripWhiteSpace, boolean toUpperCase) {
267: int start = readPos;
268: lastSeparator = 'E'; // end of file
269: boolean hasWhiteSpace = false;
270: while (lastSeparator == 'E' && readPos < inputData.length) {
271: char input = (char) inputData[readPos++];
272: if (input <= 32) {
273: hasWhiteSpace = true;
274: }
275: for (int i = 0; i < separators.length(); i++) {
276: if (input == separators.charAt(i)) {
277: // separator found
278: lastSeparator = input;
279: break;
280: }
281: }
282: }
283: int end = readPos - 1;
284: if (lastSeparator != 'E') {
285: // don't return separator
286: end--;
287: }
288: String result = "";
289: if (start <= end) {
290: result = new String(inputData, start, end - start + 1);
291: if (stripWhiteSpace && hasWhiteSpace) {
292: // trim result
293: StringBuffer sbResult = new StringBuffer(result);
294: int i = 0;
295: while (i < sbResult.length()) {
296:
297: if (sbResult.charAt(i) <= 32) {
298: sbResult.deleteCharAt(i);
299: } else {
300: i++;
301: }
302: }
303: result = sbResult.toString();
304: }
305: if (toUpperCase) {
306: result = result.toUpperCase();
307: }
308: }
309: Utils.debugOut("Returning '" + result + "' with lastSep='"
310: + lastSeparator + "'");
311: return result;
312: }
313:
314: private static int tableLookup(StringBuffer command,
315: String[] strings, int[] values, int defValue) {
316: String sCmd = command.toString();
317: int result = defValue;
318: for (int i = 0; i < strings.length; i++) {
319: if (sCmd.startsWith(strings[i])) {
320: command.delete(0, strings[i].length());
321: result = values[i];
322: break;
323: }
324: }
325: return result;
326: }
327:
328: /**
329: * add a note to the <code>notes</code> and <code>lengths</code> Vectors.
330: *
331: * @param note - 0-128, as defined in ToneControl
332: * @param duration - the divider of a full note. E.g. 4 stands for a quarter note
333: * @param dotCount - if 1, then the duration is increased by half its length, if 2 by 3/4 of its length, etc.
334: */
335: private void addNote(int note, int duration, int dotCount) {
336: // int length = (60000 * 4) /(duration * tempo);
337: int length = 64 / duration;
338: int add = 0;
339: int factor = 2;
340: for (; dotCount > 0; dotCount--) {
341: add += length / factor;
342: factor *= 2;
343: }
344: length += add;
345: if (length > 127)
346: length = 127;
347: notes.addElement(new Integer(note));
348: lengths.addElement(new Integer(length));
349: return;
350: }
351:
352: private static String URL2Name(String url) {
353: int lastSlash = url.lastIndexOf('/');
354: if (lastSlash == -1 || lastSlash == url.length() - 1) {
355: lastSlash = url.lastIndexOf(':');
356: }
357: return url.substring(lastSlash + 1);
358: }
359:
360: private static byte[] readInputStream(InputStream is)
361: throws IOException {
362: ByteArrayOutputStream baos = new ByteArrayOutputStream();
363: byte[] buffer = new byte[128];
364: while (true) {
365: int read = is.read(buffer);
366: if (read < 0) {
367: break;
368: }
369: baos.write(buffer, 0, read);
370: }
371: is.close();
372: byte[] data = baos.toByteArray();
373: buffer = null;
374: baos = null;
375: System.gc();
376: return data;
377: }
378:
379: /*
380: * Other formats:
381: * Nokia Composer
382: * Beethoven's 9th
383: * 16g1,16g1,16g1,4#d1,16f1,16f1,16f1,4d1,16g1,16g1,16g1,16#d1,
384: * 16#g1,16#g1, 16#g1,16g1,16#d2,16#d2,16#d2,4c2,16g1,16g1,16g1
385: * ,16d1,16#g1,16#g1,16#g1, 16g1,16f2,16f2,16f2,4d2
386: *
387: * Ericsson Composer
388: * Beethoven - Menuett in G
389: * a b + c b + c b + c b + C p + d a B p +
390: * c g A p f g a g a g a g A p b f G p a e F
391: * Beethoven 9th symphony theme
392: * f f f # C # d # d # d C p f f f # c # f #f
393: * # f f +# c + # c + # c # A ff f c # f # f
394: * # f f + # d + # d + # d
395: *
396: * Siemens Composer Format
397: * Inspector Gadget
398: * C2(1/8) D2(1/16) Dis2(1/8) F2(1/16) G2(1/8)
399: * P(1/16) Dis2(1/8) P(1/16) Fis2(1/8) P(1/16)
400: * D2(1/8) P(1/16) F2(1/8) P(1/16) Dis2(1/8)
401: * P(1/16) C2(1/8) D2(1/16) Dis2(1/8) F2(1/16)
402: * G2(1/8) P(1/16) C3(1/8) P(1/16) B2(1/2) P(1/4)
403: * C2(1/8) D2(1/16) Dis2(1/8) F2(1/16) G2(1/8) P(1/16)
404: * Dis2(1/8) P(1/16) Fis2(1/8) P(1/16) D2(1/8) P(1/16)
405: * F2(1/8) P(1/16) Dis2(1/8) P(1/16) C3(1/8) B2(1/16)
406: * Ais2(1/8) A2(1/16) Gis2(1/2) G2(1/8) P(1/16) C3(1/2)
407: *
408: * Motorola Composer
409: * Beethovens 9th
410: * 4 F2 F2 F2 C#4 D#2 D#2 D#2 C4 R2 F2 F2 F2 C#2 F#2 F#2
411: * F#2 F2 C#+2 C#+2 C#+2 A#4 F2 F2 F2 C2 F#2 F#2 F#2 F2
412: * D#+2 D#+2 D#+2
413: *
414: * Panasonic Composer
415: * Beethovens 9th
416: * 444** 444** 444** 1111* 4444** 4444** 4444** 111*
417: * 0** 444** 444** 444** 1111** 4444** 4444** 4444**
418: * 444** 11** 11** 11** 6666* 444** 444** 444** 111**
419: * 4444** 4444** 4444** 444** 22** 22** 22**
420: *
421: * Sony Composer
422: * Beethovens 9th
423: * 444****444****444****111#*****444#****444#****444#****111*****(JD)0000
424: * 444****444****444****111#****444#****444#****444#****444****11#****
425: * 11#****11#****666#*****444****444****444****111****444#****444#****
426: * 444#****444****22#****22#****22#****
427: *
428: */
429:
430: }
|