Skip to content

Commit 1fbceda

Browse files
committed
Nokia: More improvements to audio playback and the OTA/OTT decoder
The OTA decoder should now support everything the most recent spec (v3.0.0) does, aside from infinite looping patterns which can't be supported due to MIDI limitations (SquirrelJME will be supporting that though, so it should be a virtually perfect OTT/OTA decoder). The source was also cleaned up and optimized thanks to needing a nigh-complete overhaul in order to be ported into SJME, so OTA decoding should be noticeably faster now, although it was hardly a bottleneck to begin with.
1 parent f6ee192 commit 1fbceda

File tree

2 files changed

+131
-91
lines changed

2 files changed

+131
-91
lines changed

src/com/nokia/mid/sound/Sound.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ public void release()
195195
public void resume()
196196
{
197197
if(player == null || getState() == SOUND_UNINITIALIZED || getState() == SOUND_PLAYING) { return; }
198+
199+
if(((PlatformPlayer)player).nokiaListener != listener) { ((PlatformPlayer) player).setSoundListener(this, listener); }
200+
((PlatformPlayer.volumeControl)player.getControl("VolumeControl")).setLevel((int) (gain / 255f * 100f));
198201
player.start();
199202
}
200203

@@ -213,11 +216,10 @@ public int getGain()
213216

214217
public void stop()
215218
{
216-
if(player != null)
217-
{
218-
if(((PlatformPlayer)player).nokiaListener != listener) { ((PlatformPlayer) player).setSoundListener(this, listener); }
219-
player.stop();
220-
}
219+
if(player == null || getState() == Sound.SOUND_STOPPED || getState() == Sound.SOUND_UNINITIALIZED) { return; }
220+
221+
if(((PlatformPlayer)player).nokiaListener != listener) { ((PlatformPlayer) player).setSoundListener(this, listener); }
222+
player.stop();
221223
}
222224

223225
// This is the same conversion used in Sprintpcs' DualTone implementation., as it also uses this constant.

src/javax/microedition/media/decoders/NokiaOTTDecoder.java

Lines changed: 124 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)