@@ -47,11 +47,19 @@ public class NokiaOTTDecoder
4747 private static final double SEMITONE_CONST = 17.31234049066755 ; // 1/(ln(2^(1/12)))
4848
4949 private static int parsePos = 0 ; // Used exclusively as a marker for OTA/OTT Parsing
50- private static boolean [] toneBitArray ;
50+ private static byte curBitPos = 0 ; // Current bit position in the current parsePos
51+ private static byte [] data ;
5152 private static float noteScale = 1f ; // Default scale of 880Hz
5253 private static int noteStyle = NATURAL_STYLE ; // The default style is NATURAL
5354 private static int curTick = 0 ; // To keep track of the current midi note tick, or else all notes will play at the same time.
5455
56+ /* These are used for pattern specifiers that reuse a prior pattern */
57+ private static int lastPatternPos = 0 ;
58+ private static byte lastPatternBitPos = 0 ;
59+ private static int lastPatternLen = 0 ;
60+ private static int restorePatternPos = 0 ;
61+ private static byte restorePatternBitPos = 0 ;
62+
5563 // This one is used for debugging.
5664 private static final String [] noteStrings = new String [] {"Pause" , "C" , "C#" , "D" , "D#" , "E" , "F" , "F#" , "G" , "G#" , "A" , "A#" , "H" , "Reserved" , "Reserved" , "Reserved" };
5765
@@ -63,19 +71,11 @@ public static synchronized byte[] convertToMidi(byte[] data) throws MidiUnavaila
6371 try
6472 {
6573 parsePos = 0 ; // Reset the parsePos counter
74+ curBitPos = 0 ; // Reset current bit position
6675 noteScale = 1f ; // Reset scale as well
6776 noteStyle = NATURAL_STYLE ; // Reset note style too
6877 curTick = 0 ; // Also move curTick to the beginning
69- toneBitArray = new boolean [data .length * 8 ];
70-
71- // Convert the byte array into a bit array for much easier manipulation and reading
72- for (int i = 0 ; i < data .length ; i ++)
73- {
74- for (int j = 0 ; j < 8 ; j ++)
75- {
76- toneBitArray [i * 8 + j ] = (data [i ] & (1 << (7 - j ))) != 0 ;
77- }
78- }
78+ NokiaOTTDecoder .data = data ; // Save the byte array's reference for reading
7979
8080 // Create a new sequence and track for the converted tone
8181 Sequence sequence = new Sequence (Sequence .PPQ , 24 );
@@ -98,24 +98,33 @@ public static synchronized byte[] convertToMidi(byte[] data) throws MidiUnavaila
9898
9999 for (int i = 0 ; i < commandLength ; i ++)
100100 {
101- if (toneBitArray .length - parsePos - 8 <= 0 ) { Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "OTT tried to read beyond bounds. Returning stream early. " ); break ; }
102101 int commandType = readBits (8 ); // Check command type (first 7 bits + filler bit which is always 0)
103102
104- switch (commandType ) {
103+ switch (commandType )
104+ {
105105 case 0x4A : // Ringing tone programming
106106 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Ringing tone programming detected." );
107107 parseRingingTone (track );
108+ i ++; // We processed another command inside ringingTone
108109 break ;
109- case 0x44 : // Unicode (not handled yet, and should have nothing appended into the media track)
110+ case 0x44 : // Unicode
110111 Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Unicode detected." );
111112 parseUnicode ();
112113 break ;
113114 case 0x3A : // Sound
114115 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Sound detected." );
115116 parseSound (track );
116117 break ;
117- case 0xA : // Cancel command, Does any actual OTT/OTA ringtone use this?
118- Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Cancel command detected." );
118+ case 0xA : // Cancel command, the specification says nothing about what it really does... Does any actual OTT/OTA ringtone use this?
119+ if (readBits (7 ) == 0x05 )
120+ {
121+ parseUnicode ();
122+ }
123+ else
124+ {
125+ throw new IllegalArgumentException ("Invalid cancel command specifier" );
126+ }
127+ Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Cancel command detected. Returning." );
119128 break ;
120129 case 0x0 : // This should happen at the end of every parsing procedure.
121130 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "End of ringtone programming!" );
@@ -153,14 +162,21 @@ private static void parseRingingTone(Track track)
153162 }
154163 else if (nextCheck == 0x22 )
155164 {
156- // Ideally, at this point this check should resolve to a <cancel-command-specifier>
157- Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Detected Unicode!" );
165+ // We must read a unicode, which will be placed before any Sound
158166 parseUnicode ();
159167 }
168+ else
169+ {
170+ throw new IllegalArgumentException ("Invalid set of bits for ringing-tone-programming" );
171+ }
160172 }
161173
162- // Let's just ignore unicode decoding at all for now, this shouldn't be part of a ringtone
163- private static void parseUnicode () { }
174+ // A unicode is defined in the spec as a 16-bit UCS-2 encoded char
175+ private static void parseUnicode ()
176+ {
177+ short unicode = (short ) readBits (16 );
178+ Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Unicode:" + unicode );
179+ }
164180
165181 private static void parseSound (Track track )
166182 {
@@ -177,29 +193,29 @@ private static void parseSound(Track track)
177193 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Temporary Song Detected!" );
178194 parseTemporarySong (track );
179195 break ;
196+
197+ // These are all 'reserved for future extension' in the latest
198+ // spec revision i have (v3.0.0), likely never actually used.
180199 case 0x3 : // MIDI song type
181200 Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "MIDI Song Detected!" );
182- parseMidiSong (track );
183- break ;
201+ throw new IllegalArgumentException ("Unsupported song format" );
184202 case 0x4 : // Digitized song type
185203 Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Digitized Song Detected!" );
186- parseDigitizedSong (track );
187- break ;
204+ throw new IllegalArgumentException ("Unsupported song format" );
188205 case 0x5 : // Polyphonic song type
189206 Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Polyphonic Song Detected!" );
190- parsePolyphonicSong (track );
191- break ;
207+ throw new IllegalArgumentException ("Unsupported song format" );
192208 default :
193209 Mobile .log (Mobile .LOG_ERROR , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Unknown song type: " + Integer .toBinaryString (songType ));
194- break ;
210+ throw new IllegalArgumentException ( "Unsupported song format" ) ;
195211 }
196212 }
197213
198214 private static void parseBasicSong (Track track )
199215 {
200- // Read title length
201- int titleLength = readBits (4 ); // Upper 4 bits
202-
216+ // Read title length (upper 4 bits)
217+ int titleLength = readBits (4 );
218+
203219 StringBuilder title = new StringBuilder ();
204220 for (int i = 0 ; i < titleLength ; i ++)
205221 {
@@ -209,14 +225,9 @@ private static void parseBasicSong(Track track)
209225 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Title Length:" + titleLength + " | Basic Song Title: " + title .toString ());
210226
211227 // Read song sequence length
212- int songSequenceLength = readBits (8 ); // Read the number of patterns
213- Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Basic Song Sequence Length: " + songSequenceLength );
214-
215- // Parse each song pattern
216- for (int i = 0 ; i < songSequenceLength ; i ++) { parseSongPattern (track ); }
228+ parseTemporarySong (track );
217229 }
218230
219- // Implement similar methods for parseTemporarySong, parseMidiSong, parseDigitizedSong, and parsePolyphonicSong
220231 private static void parseTemporarySong (Track track )
221232 {
222233 // Read song sequence length
@@ -227,41 +238,69 @@ private static void parseTemporarySong(Track track)
227238 for (int i = 0 ; i < songSequenceLength ; i ++) { parseSongPattern (track ); }
228239 }
229240
230- private static void parseMidiSong (Track track ) { /* MIDI song parsing logic, Stubbed */ }
231-
232- private static void parseDigitizedSong (Track track ) { /* Digitized song parsing logic, Stubbed */ }
233-
234- private static void parsePolyphonicSong (Track track ) { /* Polyphonic song parsing logic, Stubbed */ }
235-
236241 private static void parseSongPattern (Track track )
237242 {
238243 // Read the pattern header
239- int patternHeader = readBits (3 ); // 3 bits for Pattern Header's beginning
244+ int patternHeader = readBits (3 ); // 3 bits for Pattern Header's beginning (000)
240245 int patternId = readBits (2 ); // 2 bits for pattern ID
241246 int loopValue = readBits (4 ); // 4 bits for loop value
242247
243248 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Pattern Header - ID: " + patternHeader + ", Pattern ID: " + patternId + ", Loop Value: " + loopValue );
244249
245250 if (loopValue == 0xF ) { Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "OTA/OTT Tone Infinite Loop parsing is not implemented. Parsing pattern without loop..." ); loopValue = 0 ; }
246251
247- int loopParsePosMark = parsePos ; // Marker for the current pattern start position, as we'll re-read it as many times as there are loops, to simulate looping parts of a track on MIDI.
252+ // Marker for the current pattern start position, as we'll re-read it as many times as there are loops.
253+ int loopParsePosMark = parsePos ;
254+ byte loopCurBitPos = curBitPos ;
248255
249256 while (loopValue >= 0 ) // LoopValue == 0 still means the pattern has to be entirely parsed at least one time.
250257 {
251258 parsePos = loopParsePosMark ;
259+ curBitPos = loopCurBitPos ;
252260
253261 // Read the pattern specifier
254262 int patternSpecifier = readBits (8 );
255263
256- // TODO: For specifier 0b00000000, we should probably re-read the previous pattern?
257- if (patternSpecifier == 0x0 ) { Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Using already-defined pattern. (NOT IMPLEMENTED)" ); }
264+ // For specifier 0b00000000, we must reuse the prior pattern
265+ if (patternSpecifier == 0x0 )
266+ {
267+ Mobile .log (Mobile .LOG_WARNING , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Using already-defined pattern." );
268+
269+ // Well restore back to this position after reusing a pattern
270+ restorePatternPos = parsePos ;
271+ restorePatternBitPos = curBitPos ;
272+
273+ // Parse/Loop the last known pattern
274+ while (loopValue >= 0 )
275+ {
276+ Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "New pattern length: " + patternSpecifier );
277+
278+ parsePos = lastPatternPos ;
279+ curBitPos = lastPatternBitPos ;
280+
281+ for (int j = 0 ; j < lastPatternLen ; j ++) { parsePatternInstruction (track ); }
282+ loopValue --;
283+ }
284+
285+ // We can now continue reading the next bits
286+ parsePos = restorePatternPos ;
287+ curBitPos = restorePatternBitPos ;
288+
289+ // We already looped internally, so we can return outright
290+ return ;
291+ }
292+
293+ // This means we have a new pattern length
258294 else
259295 {
260- // This means we have a new pattern length
261296 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "New pattern length: " + patternSpecifier );
262297 int numberOfInstructions = patternSpecifier ; // The number of instructions to read
263298
264- // Reset note Style and Scale, otherwise it'll carry over from the last pattern (which is incorrect despite the Smart Message API not disclosing it)
299+ lastPatternLen = numberOfInstructions ;
300+ lastPatternPos = parsePos ;
301+ lastPatternBitPos = curBitPos ;
302+
303+ // Reset note Style and Scale, otherwise they will carry over from the last pattern (which is incorrect despite the Smart Message API not disclosing it)
265304 noteStyle = NATURAL_STYLE ;
266305 noteScale = 1f ;
267306
@@ -281,10 +320,6 @@ private static void parsePatternInstruction(Track track)
281320
282321 switch (instructionType )
283322 {
284- case 0x0 : // Pattern Header ID
285- Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "New pattern found. Backtracking to read it." );
286- parsePos -= 3 ; // Parse Song Pattern will parse the pattern from the beginning, which also includes the 3 bits just read
287- return ;
288323 case 0x1 : // Note Instruction
289324 parseNoteInstruction (track );
290325 break ;
@@ -325,35 +360,26 @@ private static void parseNoteInstruction(Track track)
325360 {
326361 if (midiNote != -1 )
327362 {
328- if ( noteStyle == STACCATO_STYLE ) // Simulate shorter notes for a subtle staccato effect by making NOTE_OFF end before the next note's NOTE_ON
329- {
330- ShortMessage noteOn = new ShortMessage ();
331- noteOn .setMessage (ShortMessage .NOTE_ON , 0 , midiNote , 93 );
332- track .add (new MidiEvent (noteOn , curTick ));
363+ ShortMessage noteOn = new ShortMessage ();
364+ ShortMessage noteOff = new ShortMessage ();
365+
366+ noteOn .setMessage (ShortMessage .NOTE_ON , 0 , midiNote , 93 );
367+ track .add (new MidiEvent (noteOn , curTick ));
333368
334- ShortMessage noteOff = new ShortMessage ();
369+ if (noteStyle == STACCATO_STYLE ) // STACCATO has shorter notes with longer rest by making NOTE_OFF end way before the next note's NOTE_ON
370+ {
335371 noteOff .setMessage (ShortMessage .NOTE_OFF , 0 , midiNote , 0 );
336- track .add (new MidiEvent (noteOff , curTick + (int ) (ticks * 0.70f )));
372+ track .add (new MidiEvent (noteOff , curTick + (int ) (ticks * 0.6f )));
337373 }
338- else if (noteStyle == CONTINUOUS_STYLE ) // Try to add a small overlap between notes to connect them a bit better, making NOTE_OFF go a bit beyond the next note's NOTE_ON
374+ else if (noteStyle == CONTINUOUS_STYLE ) // Notes flow into each other
339375 {
340- ShortMessage noteOn = new ShortMessage ();
341- noteOn .setMessage (ShortMessage .NOTE_ON , 0 , midiNote , 93 );
342- track .add (new MidiEvent (noteOn , curTick ));
343-
344- ShortMessage noteOff = new ShortMessage ();
345376 noteOff .setMessage (ShortMessage .NOTE_OFF , 0 , midiNote , 0 );
346- track .add (new MidiEvent (noteOff , curTick + ( int ) ( ticks * 1.1f ) ));
377+ track .add (new MidiEvent (noteOff , curTick + ticks ));
347378 }
348- else // NATURAL just adds notes as is .
379+ else // NATURAL adds notes with a small rest between them .
349380 {
350- ShortMessage noteOn = new ShortMessage ();
351- noteOn .setMessage (ShortMessage .NOTE_ON , 0 , midiNote , 93 );
352- track .add (new MidiEvent (noteOn , curTick ));
353-
354- ShortMessage noteOff = new ShortMessage ();
355381 noteOff .setMessage (ShortMessage .NOTE_OFF , 0 , midiNote , 0 );
356- track .add (new MidiEvent (noteOff , curTick + ticks ));
382+ track .add (new MidiEvent (noteOff , curTick + ( int ) ( ticks * 0.8f ) ));
357383 }
358384 }
359385
@@ -557,15 +583,15 @@ private static int convertNoteValueToMidi(int noteValue)
557583 case 0x0 :
558584 Mobile .log (Mobile .LOG_DEBUG , NokiaOTTDecoder .class .getPackage ().getName () + "." + NokiaOTTDecoder .class .getSimpleName () + ": " + "Parsed Pause note. " );
559585 return -1 ; // Pause (no MIDI note)
560- case 0x1 : baseFrequency = 523 ; break ;// C1
561- case 0x2 : baseFrequency = 554 ; break ;// C#1 (D1b)
562- case 0x3 : baseFrequency = 587 ; break ;// D1
563- case 0x4 : baseFrequency = 622 ; break ;// D#1 (E1b, so on)
564- case 0x5 : baseFrequency = 659 ; break ;// E1
565- case 0x6 : baseFrequency = 698 ; break ;// F1
566- case 0x7 : baseFrequency = 740 ; break ;// F#1
567- case 0x8 : baseFrequency = 784 ; break ;// G1
568- case 0x9 : baseFrequency = 831 ; break ;// G#1
586+ case 0x1 : baseFrequency = 523 ; break ;// C1
587+ case 0x2 : baseFrequency = 554 ; break ;// C#1 (D1b)
588+ case 0x3 : baseFrequency = 587 ; break ;// D1
589+ case 0x4 : baseFrequency = 622 ; break ;// D#1 (E1b, so on)
590+ case 0x5 : baseFrequency = 659 ; break ;// E1
591+ case 0x6 : baseFrequency = 698 ; break ;// F1
592+ case 0x7 : baseFrequency = 740 ; break ;// F#1
593+ case 0x8 : baseFrequency = 784 ; break ;// G1
594+ case 0x9 : baseFrequency = 831 ; break ;// G#1
569595 case 0xA : baseFrequency = 880 ; break ;// A1
570596 case 0xB : baseFrequency = 932 ; break ;// A#1
571597 case 0xC : baseFrequency = 988 ; break ;// B(or H)1
@@ -631,14 +657,26 @@ private static int convertDurationToTicks(int noteDuration, int durationSpecifie
631657 return baseTicks ;
632658 }
633659
634- // Helper function to read a given number of bits from the bitArray.
635- private static int readBits (int numBits )
660+ private static int readBits (int numBits )
636661 {
637662 int value = 0 ;
638663 for (int i = 0 ; i < numBits ; i ++)
639664 {
640- value <<= 1 ;
641- value |= toneBitArray [parsePos ++] ? 1 : 0 ; // Increment the current parser position by the number of bits read
665+ if (parsePos >= data .length )
666+ {
667+ throw new IndexOutOfBoundsException ("No more data to read." );
668+ }
669+
670+ value <<= 1 ;
671+ value |= (data [parsePos ] & (1 << (7 - curBitPos ))) != 0 ? 1 : 0 ;
672+ curBitPos ++;
673+
674+ // If the current bit value is 8, we reached a new byte, wrap to 0
675+ if (curBitPos == 8 )
676+ {
677+ curBitPos = 0 ;
678+ parsePos ++;
679+ }
642680 }
643681 return value ;
644682 }
0 commit comments