Move MIDI utilities for internal use to com.android.internal.midi package
Change-Id: I7393ae1d4bca61667fb6ee809a7aa22c5c48de56
diff --git a/core/java/com/android/internal/midi/EventScheduler.java b/core/java/com/android/internal/midi/EventScheduler.java
new file mode 100644
index 0000000..7526609
--- /dev/null
+++ b/core/java/com/android/internal/midi/EventScheduler.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.midi;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Store arbitrary timestamped events using a Long timestamp.
+ * Only one Thread can write into the buffer.
+ * And only one Thread can read from the buffer.
+ */
+public class EventScheduler {
+ private static final long NANOS_PER_MILLI = 1000000;
+
+ private final Object lock = new Object();
+ private SortedMap<Long, FastEventQueue> mEventBuffer;
+ private FastEventQueue mEventPool = null;
+ private int mMaxPoolSize = 200;
+
+ public EventScheduler() {
+ mEventBuffer = new TreeMap<Long, FastEventQueue>();
+ }
+
+ // If we keep at least one node in the list then it can be atomic
+ // and non-blocking.
+ private class FastEventQueue {
+ // One thread takes from the beginning of the list.
+ volatile SchedulableEvent mFirst;
+ // A second thread returns events to the end of the list.
+ volatile SchedulableEvent mLast;
+ volatile long mEventsAdded;
+ volatile long mEventsRemoved;
+
+ FastEventQueue(SchedulableEvent event) {
+ mFirst = event;
+ mLast = mFirst;
+ mEventsAdded = 1;
+ mEventsRemoved = 0;
+ }
+
+ int size() {
+ return (int)(mEventsAdded - mEventsRemoved);
+ }
+
+ /**
+ * Do not call this unless there is more than one event
+ * in the list.
+ * @return first event in the list
+ */
+ public SchedulableEvent remove() {
+ // Take first event.
+ mEventsRemoved++;
+ SchedulableEvent event = mFirst;
+ mFirst = event.mNext;
+ return event;
+ }
+
+ /**
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ event.mNext = null;
+ mLast.mNext = event;
+ mLast = event;
+ mEventsAdded++;
+ }
+ }
+
+ /**
+ * Base class for events that can be stored in the EventScheduler.
+ */
+ public static class SchedulableEvent {
+ private long mTimestamp;
+ private SchedulableEvent mNext = null;
+
+ /**
+ * @param timestamp
+ */
+ public SchedulableEvent(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * @return timestamp
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * The timestamp should not be modified when the event is in the
+ * scheduling buffer.
+ */
+ public void setTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+ }
+
+ /**
+ * Get an event from the pool.
+ * Always leave at least one event in the pool.
+ * @return event or null
+ */
+ public SchedulableEvent removeEventfromPool() {
+ SchedulableEvent event = null;
+ if (mEventPool != null && (mEventPool.size() > 1)) {
+ event = mEventPool.remove();
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ public void addEventToPool(SchedulableEvent event) {
+ if (mEventPool == null) {
+ mEventPool = new FastEventQueue(event);
+ // If we already have enough items in the pool then just
+ // drop the event. This prevents unbounded memory leaks.
+ } else if (mEventPool.size() < mMaxPoolSize) {
+ mEventPool.add(event);
+ }
+ }
+
+ /**
+ * Add an event to the scheduler. Events with the same time will be
+ * processed in order.
+ *
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ synchronized (lock) {
+ FastEventQueue list = mEventBuffer.get(event.getTimestamp());
+ if (list == null) {
+ long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
+ : mEventBuffer.firstKey();
+ list = new FastEventQueue(event);
+ mEventBuffer.put(event.getTimestamp(), list);
+ // If the event we added is earlier than the previous earliest
+ // event then notify any threads waiting for the next event.
+ if (event.getTimestamp() < lowestTime) {
+ lock.notify();
+ }
+ } else {
+ list.add(event);
+ }
+ }
+ }
+
+ private SchedulableEvent removeNextEventLocked(long lowestTime) {
+ SchedulableEvent event;
+ FastEventQueue list = mEventBuffer.get(lowestTime);
+ // Remove list from tree if this is the last node.
+ if ((list.size() == 1)) {
+ mEventBuffer.remove(lowestTime);
+ }
+ event = list.remove();
+ return event;
+ }
+
+ /**
+ * Check to see if any scheduled events are ready to be processed.
+ *
+ * @param timestamp
+ * @return next event or null if none ready
+ */
+ public SchedulableEvent getNextEvent(long time) {
+ SchedulableEvent event = null;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for this list to be processed?
+ if (lowestTime <= time) {
+ event = removeNextEventLocked(lowestTime);
+ }
+ }
+ }
+ // Log.i(TAG, "getNextEvent: event = " + event);
+ return event;
+ }
+
+ /**
+ * Return the next available event or wait until there is an event ready to
+ * be processed. This method assumes that the timestamps are in nanoseconds
+ * and that the current time is System.nanoTime().
+ *
+ * @return event
+ * @throws InterruptedException
+ */
+ public SchedulableEvent waitNextEvent() throws InterruptedException {
+ SchedulableEvent event = null;
+ while (true) {
+ long millisToWait = Integer.MAX_VALUE;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long now = System.nanoTime();
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for the earliest list to be processed?
+ if (lowestTime <= now) {
+ event = removeNextEventLocked(lowestTime);
+ break;
+ } else {
+ // Figure out how long to sleep until next event.
+ long nanosToWait = lowestTime - now;
+ // Add 1 millisecond so we don't wake up before it is
+ // ready.
+ millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI);
+ // Clip 64-bit value to 32-bit max.
+ if (millisToWait > Integer.MAX_VALUE) {
+ millisToWait = Integer.MAX_VALUE;
+ }
+ }
+ }
+ lock.wait((int) millisToWait);
+ }
+ }
+ return event;
+ }
+}
diff --git a/core/java/com/android/internal/midi/MidiConstants.java b/core/java/com/android/internal/midi/MidiConstants.java
new file mode 100644
index 0000000..87552e4
--- /dev/null
+++ b/core/java/com/android/internal/midi/MidiConstants.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.midi;
+
+/**
+ * MIDI related constants and static methods.
+ */
+public class MidiConstants {
+ public static final byte STATUS_COMMAND_MASK = (byte) 0xF0;
+ public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F;
+
+ // Channel voice messages.
+ public static final byte STATUS_NOTE_OFF = (byte) 0x80;
+ public static final byte STATUS_NOTE_ON = (byte) 0x90;
+ public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0;
+ public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0;
+ public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0;
+ public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0;
+ public static final byte STATUS_PITCH_BEND = (byte) 0xE0;
+
+ // System Common Messages.
+ public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0;
+ public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1;
+ public static final byte STATUS_SONG_POSITION = (byte) 0xF2;
+ public static final byte STATUS_SONG_SELECT = (byte) 0xF3;
+ public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6;
+ public static final byte STATUS_END_SYSEX = (byte) 0xF7;
+
+ // System Real-Time Messages
+ public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8;
+ public static final byte STATUS_START = (byte) 0xFA;
+ public static final byte STATUS_CONTINUE = (byte) 0xFB;
+ public static final byte STATUS_STOP = (byte) 0xFC;
+ public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE;
+ public static final byte STATUS_RESET = (byte) 0xFF;
+
+ /** Number of bytes in a message nc from 8c to Ec */
+ public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 };
+
+ /** Number of bytes in a message Fn from F0 to FF */
+ public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1 };
+
+ /********************************************************************/
+
+ public static int getBytesPerMessage(int command) {
+ if ((command < 0x80) || (command > 0xFF)) {
+ return 0;
+ } else if (command >= 0xF0) {
+ return SYSTEM_BYTE_LENGTHS[command & 0x0F];
+ } else {
+ return CHANNEL_BYTE_LENGTHS[(command >> 4) - 8];
+ }
+ }
+
+ /**
+ * @param msg
+ * @param offset
+ * @param count
+ * @return true if the entire message is ActiveSensing commands
+ */
+ public static boolean isAllActiveSensing(byte[] msg, int offset,
+ int count) {
+ // Count bytes that are not active sensing.
+ int goodBytes = 0;
+ for (int i = 0; i < count; i++) {
+ byte b = msg[offset + i];
+ if (b != MidiConstants.STATUS_ACTIVE_SENSING) {
+ goodBytes++;
+ }
+ }
+ return (goodBytes == 0);
+ }
+}
diff --git a/media/java/android/media/midi/MidiDispatcher.java b/core/java/com/android/internal/midi/MidiDispatcher.java
similarity index 73%
rename from media/java/android/media/midi/MidiDispatcher.java
rename to core/java/com/android/internal/midi/MidiDispatcher.java
index 0868346..377bc68 100644
--- a/media/java/android/media/midi/MidiDispatcher.java
+++ b/core/java/com/android/internal/midi/MidiDispatcher.java
@@ -14,19 +14,20 @@
* limitations under the License.
*/
-package android.media.midi;
+package com.android.internal.midi;
+
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiSender;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
/**
- * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
- * This class subclasses {@link MidiReceiver} and dispatches any data it receives
+ * Utility class for dispatching MIDI data to a list of {@link android.media.midi.MidiReceiver}s.
+ * This class subclasses {@link android.media.midi.MidiReceiver} and dispatches any data it receives
* to its receiver list. Any receivers that throw an exception upon receiving data will
* be automatically removed from the receiver list, but no IOException will be returned
- * from the dispatcher's {@link #onReceive} in that case.
- *
- * @hide
+ * from the dispatcher's {@link android.media.midi.MidiReceiver#onReceive} in that case.
*/
public final class MidiDispatcher extends MidiReceiver {
@@ -35,7 +36,7 @@
private final MidiSender mSender = new MidiSender() {
/**
- * Called to connect a {@link MidiReceiver} to the sender
+ * Called to connect a {@link android.media.midi.MidiReceiver} to the sender
*
* @param receiver the receiver to connect
*/
@@ -44,7 +45,7 @@
}
/**
- * Called to disconnect a {@link MidiReceiver} from the sender
+ * Called to disconnect a {@link android.media.midi.MidiReceiver} from the sender
*
* @param receiver the receiver to disconnect
*/
@@ -54,7 +55,7 @@
};
/**
- * Returns the number of {@link MidiReceiver}s this dispatcher contains.
+ * Returns the number of {@link android.media.midi.MidiReceiver}s this dispatcher contains.
* @return the number of receivers
*/
public int getReceiverCount() {
@@ -62,7 +63,8 @@
}
/**
- * Returns a {@link MidiSender} which is used to add and remove {@link MidiReceiver}s
+ * Returns a {@link android.media.midi.MidiSender} which is used to add and remove
+ * {@link android.media.midi.MidiReceiver}s
* to the dispatcher's receiver list.
* @return the dispatcher's MidiSender
*/
diff --git a/core/java/com/android/internal/midi/MidiEventScheduler.java b/core/java/com/android/internal/midi/MidiEventScheduler.java
new file mode 100644
index 0000000..3a1d3fc
--- /dev/null
+++ b/core/java/com/android/internal/midi/MidiEventScheduler.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.midi;
+
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Add MIDI Events to an EventScheduler
+ */
+public class MidiEventScheduler extends EventScheduler {
+ private static final String TAG = "MidiEventScheduler";
+ // Maintain a pool of scheduled events to reduce memory allocation.
+ // This pool increases performance by about 14%.
+ private final static int POOL_EVENT_SIZE = 16;
+ private MidiReceiver mReceiver = new SchedulingReceiver();
+
+ private class SchedulingReceiver extends MidiReceiver
+ {
+ /**
+ * Store these bytes in the EventScheduler to be delivered at the specified
+ * time.
+ */
+ @Override
+ public void onReceive(byte[] msg, int offset, int count, long timestamp)
+ throws IOException {
+ MidiEvent event = createScheduledEvent(msg, offset, count, timestamp);
+ if (event != null) {
+ add(event);
+ }
+ }
+ }
+
+ public static class MidiEvent extends SchedulableEvent {
+ public int count = 0;
+ public byte[] data;
+
+ private MidiEvent(int count) {
+ super(0);
+ data = new byte[count];
+ }
+
+ private MidiEvent(byte[] msg, int offset, int count, long timestamp) {
+ super(timestamp);
+ data = new byte[count];
+ System.arraycopy(msg, offset, data, 0, count);
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ String text = "Event: ";
+ for (int i = 0; i < count; i++) {
+ text += data[i] + ", ";
+ }
+ return text;
+ }
+ }
+
+ /**
+ * Create an event that contains the message.
+ */
+ private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
+ long timestamp) {
+ MidiEvent event;
+ if (count > POOL_EVENT_SIZE) {
+ event = new MidiEvent(msg, offset, count, timestamp);
+ } else {
+ event = (MidiEvent) removeEventfromPool();
+ if (event == null) {
+ event = new MidiEvent(POOL_EVENT_SIZE);
+ }
+ System.arraycopy(msg, offset, event.data, 0, count);
+ event.count = count;
+ event.setTimestamp(timestamp);
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ @Override
+ public void addEventToPool(SchedulableEvent event) {
+ // Make sure the event is suitable for the pool.
+ if (event instanceof MidiEvent) {
+ MidiEvent midiEvent = (MidiEvent) event;
+ if (midiEvent.data.length == POOL_EVENT_SIZE) {
+ super.addEventToPool(event);
+ }
+ }
+ }
+
+ /**
+ * This MidiReceiver will write date to the scheduling buffer.
+ * @return the MidiReceiver
+ */
+ public MidiReceiver getReceiver() {
+ return mReceiver;
+ }
+
+}
diff --git a/core/java/com/android/internal/midi/MidiFramer.java b/core/java/com/android/internal/midi/MidiFramer.java
new file mode 100644
index 0000000..53d71bb
--- /dev/null
+++ b/core/java/com/android/internal/midi/MidiFramer.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.midi;
+
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Convert stream of bytes to discrete messages.
+ *
+ * Parses the incoming bytes and then posts individual messages to the receiver
+ * specified in the constructor. Short messages of 1-3 bytes will be complete.
+ * System Exclusive messages may be posted in pieces.
+ *
+ * Resolves Running Status and
+ * interleaved System Real-Time messages.
+ */
+public class MidiFramer extends MidiReceiver {
+
+ public String TAG = "MidiFramer";
+ private MidiReceiver mReceiver;
+ private byte[] mBuffer = new byte[3];
+ private int mCount;
+ private int mRunningStatus;
+ private int mNeeded;
+
+ public MidiFramer(MidiReceiver receiver) {
+ mReceiver = receiver;
+ }
+
+ public static String formatMidiData(byte[] data, int offset, int count) {
+ String text = "MIDI+" + offset + " : ";
+ for (int i = 0; i < count; i++) {
+ text += String.format("0x%02X, ", data[offset + i]);
+ }
+ return text;
+ }
+
+ /*
+ * @see android.midi.MidiReceiver#onPost(byte[], int, int, long)
+ */
+ @Override
+ public void onReceive(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ // Log.i(TAG, formatMidiData(data, offset, count));
+ for (int i = 0; i < count; i++) {
+ int b = data[offset] & 0xFF;
+ if (b >= 0x80) { // status byte?
+ if (b < 0xF0) { // channel message?
+ mRunningStatus = (byte) b;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(b) - 1;
+ } else if (b < 0xF8) { // system common?
+ mBuffer[0] = (byte) b;
+ mRunningStatus = 0;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(b) - 1;
+ } else { // real-time?
+ // Single byte message interleaved with other data.
+ mReceiver.sendWithTimestamp(data, offset, 1, timestamp);
+ }
+ } else { // data byte
+ mBuffer[mCount++] = (byte) b;
+ if (--mNeeded == 0) {
+ if (mRunningStatus != 0) {
+ mBuffer[0] = (byte) mRunningStatus;
+ }
+ mReceiver.sendWithTimestamp(mBuffer, 0, mCount, timestamp);
+ mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1;
+ mCount = 1;
+ }
+ }
+ ++offset;
+ }
+ }
+
+}
diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index d27351f..bc85f92 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -24,6 +24,8 @@
import android.system.OsConstants;
import android.util.Log;
+import com.android.internal.midi.MidiDispatcher;
+
import dalvik.system.CloseGuard;
import libcore.io.IoUtils;
diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java
index b8ed36f..0290a76 100644
--- a/media/java/android/media/midi/MidiOutputPort.java
+++ b/media/java/android/media/midi/MidiOutputPort.java
@@ -21,6 +21,8 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.midi.MidiDispatcher;
+
import dalvik.system.CloseGuard;
import libcore.io.IoUtils;
diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java
index 3b65709..7c101a40f 100644
--- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java
@@ -19,7 +19,6 @@
import android.content.Context;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceServer;
-import android.media.midi.MidiDispatcher;
import android.media.midi.MidiManager;
import android.media.midi.MidiReceiver;
import android.media.midi.MidiSender;