/*
Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
и других спецификаций для функционирования компактных приложений на языке
Java (мидлетов) в среде программного обеспечения Малик Эмулятор.
Copyright © 2016–2017, 2019–2023, 2025 Малик Разработчик
Это свободная программа: вы можете перераспространять ее и/или изменять
ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
в каком она была опубликована Фондом свободного программного обеспечения;
либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.
Эта программа распространяется в надежде, что она будет полезной,
но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
общественной лицензии GNU.
Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
вместе с этой программой. Если это не так, см.
<https://www.gnu.org/licenses/>.
*/
package malik.emulator.fileformats.sound.synthetic.midi;
import java.io.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.sound.synthetic.*;
import malik.emulator.microedition.media.*;
public final class MIDIDecoder extends ControlList implements SyntheticSoundDecoder, SyntheticPlayerDecoder
{
private static final class MetaData extends Object implements Control, MetaDataControl
{
public String copyright;
public MetaData() {
}
public String[] getKeys() {
return new String[] { COPYRIGHT_KEY };
}
public String getKeyValue(String key) {
if(COPYRIGHT_KEY.equals(key)) return copyright;
throw new IllegalArgumentException("MetaDataControl.getKeyValue: аргумент key имеет недопустимое значение.");
}
}
private static final class Track extends ByteArrayInputStream
{
public int lastStatusByte;
public long eventTimeInDelta;
public Track(byte[] buffer) {
super(buffer);
}
public void seek(int delta) {
pos += delta;
}
public void endOfTrack() {
eventTimeInDelta = Long.MAX_VALUE;
}
public void readTimeInDelta() {
eventTimeInDelta += (long) readVolatileLengthValue();
}
public int readVolatileLengthValue() {
int result = 0;
int position = pos;
byte[] buffer = buf;
for(int len = buffer != null ? buffer.length : 0, i = 4; position >= 0 && position < len && i-- > 0; )
{
int b;
result = result << 7 | (b = buffer[position++] & 0xff) & 0x7f;
if((b & 0x80) == 0) break;
}
pos = position;
return result;
}
public String readText() {
int len;
char[] chars;
byte[] bytes;
read(bytes = new byte[len = readVolatileLengthValue()], 0, len);
chars = new char[len];
for(int i = len; i-- > 0; chars[i] = (char) (bytes[i] & 0xff));
return new String(chars);
}
}
public static final long SIGNATURE = 0x4d546864L; /* MThd */
public static final String MIME_TYPE = "audio/midi";
private static final double DEFAULT_TEMPO = 500000.d;
private static final int MAXIMUM_TRACKS = 128;
private static final int MTRK = 0x4d54726b; /* MTrk */
private boolean stopped;
private long[] messages;
private final MetaData info;
public MIDIDecoder() {
MetaData info = new MetaData();
this.controls = new Control[] { info };
this.info = info;
}
public void loadFromInputStream(InputStream stream) throws IOException {
loadFromDataStream(new ExtendedDataInputStream(stream));
}
public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
double ppqn;
double tempo;
double currentTimeInMillis;
int len;
int ippqn;
int ntracks;
int ntracksEnds;
long currentTimeInDelta;
long[] messages;
Track[] tracks;
String copyright;
checkStopped();
if(stream.readInt() != 6)
{
throw new InvalidDataFormatException("MIDIDecoder.loadFromInputStream: неправильный формат данных.");
}
stream.readUnsignedShort();
if((ntracks = stream.readUnsignedShort()) > MAXIMUM_TRACKS)
{
throw new UnsupportedDataException("MIDIDecoder.loadFromInputStream: неподдерживаемые данные.");
}
if(ntracks <= 0)
{
throw new InvalidDataFormatException("MIDIDecoder.loadFromInputStream: неправильный формат данных.");
}
if((ippqn = stream.readShort()) <= 0)
{
throw new UnsupportedDataException("MIDIDecoder.loadFromInputStream: неподдерживаемые данные.");
}
tracks = new Track[ntracks];
for(int i = 0; i < ntracks; i++)
{
int chunkName;
int chunkSize;
byte[] trackData;
checkStopped();
chunkName = stream.readInt();
chunkSize = stream.readInt();
if(chunkName != MTRK)
{
stream.skip((long) chunkSize & 0x00000000ffffffffL);
continue;
}
if(chunkSize < 0)
{
throw new UnsupportedDataException("MIDIDecoder.loadFromInputStream: неподдерживаемые данные.");
}
stream.readFully(trackData = new byte[chunkSize]);
tracks[i] = new Track(trackData);
}
checkStopped();
ppqn = 1000.d * (double) ippqn;
tempo = DEFAULT_TEMPO;
currentTimeInMillis = 0.d;
len = 1;
ntracksEnds = 0;
currentTimeInDelta = 0L;
messages = new long[0x0f];
copyright = null;
/* подсчёт времени начал траков */
for(int i = ntracks; i-- > 0; )
{
checkStopped();
tracks[i].readTimeInDelta();
}
label0: for(; ; )
{
double offsetTimeInMillis;
int statusByte;
int dataByte1;
int dataByte2;
int dataByte3;
long minTimeInDelta;
long offsetTimeInDelta;
Track currentTrack;
checkStopped();
/* выбор трака с наименьшим дельта-временем наступления события */
minTimeInDelta = (currentTrack = tracks[0]).eventTimeInDelta;
for(int i = 1; i < ntracks; i++)
{
long thisTimeInDelta;
Track thisTrack;
checkStopped();
if((thisTimeInDelta = (thisTrack = tracks[i]).eventTimeInDelta) < minTimeInDelta)
{
currentTrack = thisTrack;
minTimeInDelta = thisTimeInDelta;
}
}
/* обработка очередного события в выбранном траке */
if((statusByte = currentTrack.read()) < 0) break label0;
if((statusByte & 0x80) == 0)
{
statusByte = currentTrack.lastStatusByte;
currentTrack.seek(-1);
}
else if(statusByte != 0xff)
{
currentTrack.lastStatusByte = statusByte;
}
label1:
{
if(statusByte >= 0x80 && statusByte < 0xc0 || statusByte >= 0xe0 && statusByte < 0xf0)
{
dataByte1 = currentTrack.read();
dataByte2 = currentTrack.read();
break label1;
}
if(statusByte >= 0xc0 && statusByte < 0xe0)
{
dataByte1 = currentTrack.read();
dataByte2 = 0;
break label1;
}
if(statusByte == 0xf0 || statusByte == 0xf7)
{
currentTrack.seek(currentTrack.readVolatileLengthValue());
currentTrack.readTimeInDelta();
continue label0;
}
if(statusByte == 0xf1 || statusByte == 0xf3)
{
currentTrack.seek(1);
currentTrack.readTimeInDelta();
continue label0;
}
if(statusByte == 0xf2)
{
currentTrack.seek(2);
currentTrack.readTimeInDelta();
continue label0;
}
if(statusByte != 0xff)
{
currentTrack.readTimeInDelta();
continue label0;
}
switch(currentTrack.read())
{
default:
currentTrack.seek(currentTrack.readVolatileLengthValue());
currentTrack.readTimeInDelta();
continue label0;
case -1:
break label0;
case 0x02:
copyright = currentTrack.readText();
currentTrack.readTimeInDelta();
continue label0;
case 0x2f:
if(currentTrack.read() == 0x00) break;
break label0;
case 0x51:
if((statusByte = currentTrack.readVolatileLengthValue()) < 3) break label0;
dataByte1 = currentTrack.read();
dataByte2 = currentTrack.read();
dataByte3 = currentTrack.read();
if((dataByte1 | dataByte2 | dataByte3) < 0) break label0;
tempo = (double) (dataByte1 << 16 | dataByte2 << 8 | dataByte3);
currentTrack.seek(statusByte - 3);
currentTrack.readTimeInDelta();
continue label0;
}
/* обработка окончания трака */
if(++ntracksEnds < ntracks)
{
currentTrack.endOfTrack();
continue label0;
}
offsetTimeInDelta = currentTrack.eventTimeInDelta - currentTimeInDelta;
offsetTimeInMillis = (double) offsetTimeInDelta * tempo / ppqn;
currentTimeInDelta += offsetTimeInDelta;
currentTimeInMillis += offsetTimeInMillis;
if(len == messages.length) Array.copy(messages, 0, messages = new long[(len << 1) + 1], 0, len);
messages[len++] = (long) currentTimeInMillis << 24 | 0xff2f00L;
break label0;
}
/* обработка голосового сообщения */
offsetTimeInDelta = currentTrack.eventTimeInDelta - currentTimeInDelta;
offsetTimeInMillis = (double) offsetTimeInDelta * tempo / ppqn;
currentTimeInDelta += offsetTimeInDelta;
currentTimeInMillis += offsetTimeInMillis;
if(len == messages.length) Array.copy(messages, 0, messages = new long[(len << 1) + 1], 0, len);
messages[len++] = (long) currentTimeInMillis << 24 | (long) (statusByte << 16 | dataByte1 << 8 | dataByte2);
currentTrack.readTimeInDelta();
}
if(len < messages.length) Array.copy(messages, 0, messages = new long[len], 0, len);
this.messages = messages;
this.info.copyright = copyright;
}
public void stopDecoding() {
stopped = true;
}
public void clear() {
stopped = false;
messages = null;
info.copyright = null;
}
public boolean isEmpty() {
return messages == null && info.copyright == null;
}
public long[] getMessages() {
int len;
long[] result;
if((result = messages) == null) return null;
Array.copy(result, 0, result = new long[len = result.length], 0, len);
return result;
}
public String getContentType() {
return MIME_TYPE;
}
private void checkStopped() throws InterruptedIOException {
if(stopped)
{
stopped = false;
throw new InterruptedIOException("MIDIDecoder.loadFromInputStream: процесс декодирования был прерван.");
}
}
}