package jp.kshoji.blemidi.util; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.SparseIntArray; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import jp.kshoji.blemidi.device.MidiInputDevice; import jp.kshoji.blemidi.listener.OnMidiInputEventListener; /** * BLE MIDI Parser<br /> * The protocol compatible with Apple's `MIDI over Bluetooth LE` specification.<br /> * One BleMidiParser instance belongs to one MidiInputDevice instance. * * @author K.Shoji */ public final class BleMidiParser { // MIDI event message private int midiEventKind; private int midiEventNote; private int midiEventVelocity; // for RPN/NRPN messages private static final int RPN_STATUS_NONE = 0; private static final int RPN_STATUS_RPN = 1; private static final int RPN_STATUS_NRPN = 2; private int rpnNrpnFunction; private int rpnNrpnValueMsb; private int rpnNrpnValueLsb; private int rpnStatus = RPN_STATUS_NONE; private int rpnFunctionMsb = 0x7f; private int rpnFunctionLsb = 0x7f; private int nrpnFunctionMsb = 0x7f; private int nrpnFunctionLsb = 0x7f; private final SparseIntArray rpnCacheMsb = new SparseIntArray(); private final SparseIntArray rpnCacheLsb = new SparseIntArray(); private final SparseIntArray nrpnCacheMsb = new SparseIntArray(); private final SparseIntArray nrpnCacheLsb = new SparseIntArray(); // for SysEx messages private final Object systemExclusiveLock = new Object(); private final ReusableByteArrayOutputStream systemExclusiveStream = new ReusableByteArrayOutputStream(); private final ReusableByteArrayOutputStream systemExclusiveRecoveryStream = new ReusableByteArrayOutputStream(); // states private static final int MIDI_STATE_TIMESTAMP = 0; private static final int MIDI_STATE_WAIT = 1; private static final int MIDI_STATE_SIGNAL_2BYTES_2 = 21; private static final int MIDI_STATE_SIGNAL_3BYTES_2 = 31; private static final int MIDI_STATE_SIGNAL_3BYTES_3 = 32; private static final int MIDI_STATE_SIGNAL_SYSEX = 41; private int midiState; // for Timestamp private static final int MAX_TIMESTAMP = 8192; private static final int BUFFER_LENGTH_MILLIS = 30; private int timestamp = 0; private int lastTimestamp; private long lastTimestampRecorded = 0; private int zeroTimestampCount = 0; private Boolean isTimestampAlwaysZero = null; private OnMidiInputEventListener midiInputEventListener = null; private final MidiInputDevice sender; private final EventDequeueRunnable eventDequeueRunnable; private final Thread eventDequeueThread; /** * Constructor * * @param sender the sender */ public BleMidiParser(@NonNull final MidiInputDevice sender) { this.sender = sender; midiState = MIDI_STATE_TIMESTAMP; midiEventKind = 0; midiEventNote = 0; midiEventVelocity = 0; eventDequeueRunnable = new EventDequeueRunnable(); eventDequeueThread = new Thread(eventDequeueRunnable, "EventDequeueThread"); eventDequeueThread.start(); } /** * Sets {@link jp.kshoji.blemidi.listener.OnMidiInputEventListener} * * @param midiInputEventListener the listener for MIDI events */ public void setMidiInputEventListener(@Nullable OnMidiInputEventListener midiInputEventListener) { this.midiInputEventListener = midiInputEventListener; } /** * Stops the internal Thread */ public void stop() { if (eventDequeueRunnable != null) { eventDequeueRunnable.isRunning = false; } } /** * {@link Runnable} with MIDI event data, and firing timing */ private abstract class MidiEventWithTiming implements Runnable { private static final int INVALID = -1; private final long timing; private final int arg1; private final int arg2; private final int arg3; private final byte[] array; /** * Calculate `time to wait` for the event's timestamp * * @param timestamp the event's timestamp * @return time to wait */ private long calculateEventFireTime(final int timestamp) { final long currentTimeMillis = System.currentTimeMillis(); // checks timestamp value is always zero if (isTimestampAlwaysZero != null) { if (isTimestampAlwaysZero) { return currentTimeMillis; } } else { if (timestamp == 0) { if (zeroTimestampCount >= 3) { // decides timestamp is always zero: event fires immediately isTimestampAlwaysZero = true; return currentTimeMillis; } else { zeroTimestampCount++; } } else { isTimestampAlwaysZero = false; } } if (lastTimestampRecorded == 0) { // first time: event fires immediately lastTimestamp = timestamp; lastTimestampRecorded = currentTimeMillis; return currentTimeMillis; } if (currentTimeMillis - lastTimestampRecorded >= MAX_TIMESTAMP) { // the event comes after long pause lastTimestamp = timestamp; lastTimestampRecorded = currentTimeMillis; return currentTimeMillis; } int adjustedTimestamp = timestamp; if (timestamp + MAX_TIMESTAMP / 2 < lastTimestamp) { adjustedTimestamp += MAX_TIMESTAMP; } final long result = BUFFER_LENGTH_MILLIS + adjustedTimestamp - lastTimestamp + lastTimestampRecorded; lastTimestamp = timestamp; lastTimestampRecorded = currentTimeMillis; return result; } private MidiEventWithTiming(int arg1, int arg2, int arg3, byte[] array, int timestamp) { this.arg1 = arg1; this.arg2 = arg2; this.arg3 = arg3; this.array = array; timing = calculateEventFireTime(timestamp); } /** * Constructor with no arguments * * @param timestamp BLE MIDI timestamp */ MidiEventWithTiming(int timestamp) { this(INVALID, INVALID, INVALID, null, timestamp); } /** * Constructor with 1 argument * * @param arg1 argument 1 * @param timestamp BLE MIDI timestamp */ MidiEventWithTiming(int arg1, int timestamp) { this(arg1, INVALID, INVALID, null, timestamp); } /** * Constructor with 2 arguments * * @param arg1 argument 1 * @param arg2 argument 2 * @param timestamp BLE MIDI timestamp */ MidiEventWithTiming(int arg1, int arg2, int timestamp) { this(arg1, arg2, INVALID, null, timestamp); } /** * Constructor with 3 arguments * * @param arg1 argument 1 * @param arg2 argument 2 * @param arg3 argument 3 * @param timestamp BLE MIDI timestamp */ MidiEventWithTiming(int arg1, int arg2, int arg3, int timestamp) { this (arg1, arg2, arg3, null, timestamp); } /** * Constructor with array * * @param array data * @param timestamp BLE MIDI timestamp */ MidiEventWithTiming(@NonNull byte[] array, int timestamp) { this(INVALID, INVALID, INVALID, array, timestamp); } public long getTiming() { return timing; } public int getArg1() { return arg1; } public int getArg2() { return arg2; } public int getArg3() { return arg3; } public byte[] getArray() { return array; } } /** * Parses MIDI events * * @param header the header bits * @param event the event byte */ private void parseMidiEvent(final int header, final byte event) { final int midiEvent = event & 0xff; if (midiState == MIDI_STATE_TIMESTAMP) { if ((midiEvent & 0x80) == 0) { // running status midiState = MIDI_STATE_WAIT; } if (midiEvent == 0xf7) { // is this end of SysEx??? synchronized (systemExclusiveLock) { if (systemExclusiveRecoveryStream.size() > 0) { // previous SysEx has been failed, due to timestamp was 0xF7 // process SysEx again // last written byte is for timestamp int removed = systemExclusiveRecoveryStream.replaceLastByte(midiEvent); if (removed >= 0) { timestamp = ((header & 0x3f) << 7) | (removed & 0x7f); addEventToQueue(new MidiEventWithTiming(systemExclusiveRecoveryStream.toByteArray(), timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiSystemExclusive(sender, getArray()); } } }); } systemExclusiveRecoveryStream.reset(); } } // process next byte with state: MIDI_STATE_TIMESTAMP midiState = MIDI_STATE_TIMESTAMP; return; } else { // there is no error. reset the stream for recovery synchronized (systemExclusiveLock) { if (systemExclusiveRecoveryStream.size() > 0) { systemExclusiveRecoveryStream.reset(); } } } } if (midiState == MIDI_STATE_TIMESTAMP) { timestamp = ((header & 0x3f) << 7) | (midiEvent & 0x7f); midiState = MIDI_STATE_WAIT; } else if (midiState == MIDI_STATE_WAIT) { switch (midiEvent & 0xf0) { case 0xf0: { switch (midiEvent) { case 0xf0: synchronized (systemExclusiveLock) { systemExclusiveStream.reset(); systemExclusiveStream.write(midiEvent); systemExclusiveRecoveryStream.reset(); } midiState = MIDI_STATE_SIGNAL_SYSEX; break; case 0xf1: case 0xf3: // 0xf1 MIDI Time Code Quarter Frame. : 2bytes // 0xf3 Song Select. : 2bytes midiEventKind = midiEvent; midiState = MIDI_STATE_SIGNAL_2BYTES_2; break; case 0xf2: // 0xf2 Song Position Pointer. : 3bytes midiEventKind = midiEvent; midiState = MIDI_STATE_SIGNAL_3BYTES_2; break; case 0xf6: // 0xf6 Tune Request : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiTuneRequest(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xf8: // 0xf8 Timing Clock : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiTimingClock(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xfa: // 0xfa Start : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiStart(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xfb: // 0xfb Continue : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiContinue(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xfc: // 0xfc Stop : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiStop(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xfe: // 0xfe Active Sensing : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiActiveSensing(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xff: // 0xff Reset : 1byte addEventToQueue(new MidiEventWithTiming(timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiReset(sender); } } }); midiState = MIDI_STATE_TIMESTAMP; break; default: break; } } break; case 0x80: case 0x90: case 0xa0: case 0xb0: case 0xe0: // 3bytes pattern midiEventKind = midiEvent; midiState = MIDI_STATE_SIGNAL_3BYTES_2; break; case 0xc0: // program change case 0xd0: // channel after-touch // 2bytes pattern midiEventKind = midiEvent; midiState = MIDI_STATE_SIGNAL_2BYTES_2; break; default: // 0x00 - 0x70: running status if ((midiEventKind & 0xf0) != 0xf0) { // previous event kind is multi-bytes pattern midiEventNote = midiEvent; midiState = MIDI_STATE_SIGNAL_3BYTES_3; } break; } } else if (midiState == MIDI_STATE_SIGNAL_2BYTES_2) { switch (midiEventKind & 0xf0) { // 2bytes pattern case 0xc0: // program change midiEventNote = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiProgramChange(sender, getArg1() & 0xf, getArg2()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xd0: // channel after-touch midiEventNote = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiChannelAftertouch(sender, getArg1() & 0xf, getArg2()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xf0: { switch (midiEventKind) { case 0xf1: // 0xf1 MIDI Time Code Quarter Frame. : 2bytes midiEventNote = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventNote, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiTimeCodeQuarterFrame(sender, getArg1()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xf3: // 0xf3 Song Select. : 2bytes midiEventNote = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventNote, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiSongSelect(sender, getArg1()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; default: // illegal state midiState = MIDI_STATE_TIMESTAMP; break; } } break; default: // illegal state midiState = MIDI_STATE_TIMESTAMP; break; } } else if (midiState == MIDI_STATE_SIGNAL_3BYTES_2) { switch (midiEventKind & 0xf0) { case 0x80: case 0x90: case 0xa0: case 0xb0: case 0xe0: case 0xf0: // 3bytes pattern midiEventNote = midiEvent; midiState = MIDI_STATE_SIGNAL_3BYTES_3; break; default: // illegal state midiState = MIDI_STATE_TIMESTAMP; break; } } else if (midiState == MIDI_STATE_SIGNAL_3BYTES_3) { switch (midiEventKind & 0xf0) { // 3bytes pattern case 0x80: // note off midiEventVelocity = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, midiEventVelocity, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiNoteOff(sender, getArg1() & 0xf, getArg2(), getArg3()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0x90: // note on midiEventVelocity = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, midiEventVelocity, timestamp) { @Override public void run() { if (midiInputEventListener != null) { if (midiEventVelocity == 0) { midiInputEventListener.onMidiNoteOff(sender, getArg1() & 0xf, getArg2(), getArg3()); } else { midiInputEventListener.onMidiNoteOn(sender, getArg1() & 0xf, getArg2(), getArg3()); } } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xa0: // control polyphonic key pressure midiEventVelocity = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, midiEventVelocity, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiPolyphonicAftertouch(sender, getArg1() & 0xf, getArg2(), getArg3()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xb0: // control change midiEventVelocity = midiEvent; // process RPN/NRPN messages switch (midiEventNote) { case 6: { // RPN/NRPN value MSB rpnNrpnValueMsb = midiEventVelocity & 0x7f; if (rpnStatus == RPN_STATUS_RPN) { rpnNrpnFunction = ((rpnFunctionMsb & 0x7f) << 7) | (rpnFunctionLsb & 0x7f); rpnCacheMsb.put(rpnNrpnFunction, rpnNrpnValueMsb); rpnNrpnValueLsb = rpnCacheLsb.get(rpnNrpnFunction, 0/*if not found*/); addEventToQueue(new MidiEventWithTiming(midiEventKind, rpnNrpnFunction, (rpnNrpnValueMsb << 7 | rpnNrpnValueLsb), timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onRPNMessage(sender, getArg1() & 0xf, getArg2() & 0x3fff, getArg3() & 0x3fff); } } }); } else if (rpnStatus == RPN_STATUS_NRPN) { rpnNrpnFunction = ((nrpnFunctionMsb & 0x7f) << 7) | (nrpnFunctionLsb & 0x7f); nrpnCacheMsb.put(rpnNrpnFunction, rpnNrpnValueMsb); rpnNrpnValueLsb = nrpnCacheLsb.get(rpnNrpnFunction, 0/*if not found*/); addEventToQueue(new MidiEventWithTiming(midiEventKind, rpnNrpnFunction, (rpnNrpnValueMsb << 7 | rpnNrpnValueLsb), timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onNRPNMessage(sender, getArg1() & 0xf, getArg2() & 0x3fff, getArg3() & 0x3fff); } } }); } break; } case 38: { // RPN/NRPN value LSB rpnNrpnValueLsb = midiEventVelocity & 0x7f; if (rpnStatus == RPN_STATUS_RPN) { rpnNrpnFunction = ((rpnFunctionMsb & 0x7f) << 7) | (rpnFunctionLsb & 0x7f); rpnNrpnValueMsb = rpnCacheMsb.get(rpnNrpnFunction, 0/*if not found*/); rpnCacheLsb.put(rpnNrpnFunction, rpnNrpnValueLsb); addEventToQueue(new MidiEventWithTiming(midiEventKind, rpnNrpnFunction, (rpnNrpnValueMsb << 7 | rpnNrpnValueLsb), timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onRPNMessage(sender, getArg1() & 0xf, getArg2() & 0x3fff, getArg3() & 0x3fff); } } }); } else if (rpnStatus == RPN_STATUS_NRPN) { rpnNrpnFunction = ((nrpnFunctionMsb & 0x7f) << 7) | (nrpnFunctionLsb & 0x7f); rpnNrpnValueMsb = nrpnCacheMsb.get(rpnNrpnFunction, 0/*if not found*/); nrpnCacheLsb.put(rpnNrpnFunction, rpnNrpnValueLsb); addEventToQueue(new MidiEventWithTiming(midiEventKind, rpnNrpnFunction, (rpnNrpnValueMsb << 7 | rpnNrpnValueLsb), timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onNRPNMessage(sender, getArg1() & 0xf, getArg2() & 0x3fff, getArg3() & 0x3fff); } } }); } break; } case 98: { // NRPN parameter number LSB nrpnFunctionLsb = midiEventVelocity & 0x7f; rpnStatus = RPN_STATUS_NRPN; break; } case 99: { // NRPN parameter number MSB nrpnFunctionMsb = midiEventVelocity & 0x7f; rpnStatus = RPN_STATUS_NRPN; break; } case 100: { // RPN parameter number LSB rpnFunctionLsb = midiEventVelocity & 0x7f; if (rpnFunctionMsb == 0x7f && rpnFunctionLsb == 0x7f) { rpnStatus = RPN_STATUS_NONE; } else { rpnStatus = RPN_STATUS_RPN; } break; } case 101: { // RPN parameter number MSB rpnFunctionMsb = midiEventVelocity & 0x7f; if (rpnFunctionMsb == 0x7f && rpnFunctionLsb == 0x7f) { rpnStatus = RPN_STATUS_NONE; } else { rpnStatus = RPN_STATUS_RPN; } break; } default: // do nothing break; } addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, midiEventVelocity, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiControlChange(sender, getArg1() & 0xf, getArg2(), getArg3()); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xe0: // pitch bend midiEventVelocity = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventKind, midiEventNote, midiEventVelocity, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiPitchWheel(sender, getArg1() & 0xf, (getArg2() & 0x7f) | ((getArg3() & 0x7f) << 7)); } } }); midiState = MIDI_STATE_TIMESTAMP; break; case 0xf0: // Song Position Pointer. midiEventVelocity = midiEvent; addEventToQueue(new MidiEventWithTiming(midiEventNote, midiEventVelocity, timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiSongPositionPointer(sender, (getArg1() & 0x7f) | ((getArg2() & 0x7f) << 7)); } } }); midiState = MIDI_STATE_TIMESTAMP; break; default: // illegal state midiState = MIDI_STATE_TIMESTAMP; break; } } else if (midiState == MIDI_STATE_SIGNAL_SYSEX) { if (midiEvent == 0xf7) { // the end of message synchronized (systemExclusiveLock) { // last written byte is for timestamp int replacedEvent = systemExclusiveStream.replaceLastByte(midiEvent); if (replacedEvent >= 0) { timestamp = ((header & 0x3f) << 7) | (replacedEvent & 0x7f); } addEventToQueue(new MidiEventWithTiming(systemExclusiveStream.toByteArray(), timestamp) { @Override public void run() { if (midiInputEventListener != null) { midiInputEventListener.onMidiSystemExclusive(sender, getArray()); } } }); // for error recovery systemExclusiveRecoveryStream.reset(); try { systemExclusiveStream.writeTo(systemExclusiveRecoveryStream); } catch (IOException ignored) { } systemExclusiveRecoveryStream.replaceLastByte(replacedEvent); systemExclusiveRecoveryStream.write(midiEvent); } midiState = MIDI_STATE_TIMESTAMP; } else { synchronized (systemExclusiveLock) { systemExclusiveStream.write(midiEvent); } } } } /** * Updates incoming data * * @param data incoming data */ public void parse(@NonNull byte[] data) { if (data.length > 1) { int header = data[0] & 0xff; for (int i = 1; i < data.length; i++) { parseMidiEvent(header, data[i]); } } } private final Collection<MidiEventWithTiming> queuedEventList = new ArrayList<>(); /** * Add a event to event queue * @param event the MIDI Event */ private void addEventToQueue(MidiEventWithTiming event) { synchronized (queuedEventList) { queuedEventList.add(event); } eventDequeueThread.interrupt(); } /** * Runnable for MIDI event queueing */ private class EventDequeueRunnable implements Runnable { private volatile boolean isRunning = true; private final List<MidiEventWithTiming> dequeuedEvents = new ArrayList<>(); private final Comparator<MidiEventWithTiming> midiTimerTaskComparator = new Comparator<MidiEventWithTiming>() { @Override public int compare(final MidiEventWithTiming lhs, final MidiEventWithTiming rhs) { // sort by tick int tickDifference = (int) (lhs.getTiming() - rhs.getTiming()); if (tickDifference != 0) { return tickDifference * 256; } int lhsMessage = lhs.getArg1(); int rhsMessage = rhs.getArg1(); // apply zero if message is empty if (lhsMessage == MidiEventWithTiming.INVALID) { final byte[] lhsArray = lhs.getArray(); if (lhsArray == null || lhsArray.length < 1) { lhsMessage = 0; } else { lhsMessage = lhsArray[0]; } } if (rhsMessage == MidiEventWithTiming.INVALID) { final byte[] rhsArray = rhs.getArray(); if (rhsArray == null || rhsArray.length < 1) { rhsMessage = 0; } else { rhsMessage = rhsArray[0]; } } // same timing // sort by the MIDI data priority order, as: // system message > control messages > note on > note off // swap the priority of note on, and note off int lhsInt = lhsMessage & 0xf0; int rhsInt = rhsMessage & 0xf0; if ((lhsInt & 0x90) == 0x80) { lhsInt |= 0x10; } else { lhsInt &= ~0x10; } if ((rhsInt & 0x90) == 0x80) { rhsInt |= 0x10; } else { rhsInt &= ~0x10; } return -(lhsInt - rhsInt); } }; @Override public void run() { while (isRunning) { // deque events dequeuedEvents.clear(); final long currentTime = System.currentTimeMillis(); synchronized (queuedEventList) { for (MidiEventWithTiming event : queuedEventList) { if (event.getTiming() <= currentTime) { // collect past events dequeuedEvents.add(event); } } queuedEventList.removeAll(dequeuedEvents); } if (!dequeuedEvents.isEmpty()) { // sort event order Collections.sort(dequeuedEvents, midiTimerTaskComparator); // fire events for (MidiEventWithTiming event : dequeuedEvents) { event.run(); } } // sleep until interrupt try { boolean isEmpty; synchronized (queuedEventList) { isEmpty = queuedEventList.isEmpty(); } if (isEmpty) { Thread.sleep(1000); } else { Thread.sleep(1); } } catch (InterruptedException ignored) { } } } } }