diff options
| -rw-r--r-- | core/java/com/android/internal/midi/EventScheduler.java | 236 | ||||
| -rw-r--r-- | core/java/com/android/internal/midi/MidiConstants.java | 88 | ||||
| -rw-r--r-- | core/java/com/android/internal/midi/MidiDispatcher.java (renamed from media/java/android/media/midi/MidiDispatcher.java) | 22 | ||||
| -rw-r--r-- | core/java/com/android/internal/midi/MidiEventScheduler.java | 119 | ||||
| -rw-r--r-- | core/java/com/android/internal/midi/MidiFramer.java | 92 | ||||
| -rw-r--r-- | media/java/android/media/midi/MidiDeviceServer.java | 2 | ||||
| -rw-r--r-- | media/java/android/media/midi/MidiOutputPort.java | 2 | ||||
| -rw-r--r-- | services/usb/java/com/android/server/usb/UsbMidiDevice.java | 1 |
8 files changed, 551 insertions, 11 deletions
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 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 @@ public final class MidiDispatcher extends MidiReceiver { 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 @@ public final class MidiDispatcher extends MidiReceiver { } /** - * 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 @@ public final class MidiDispatcher extends MidiReceiver { }; /** - * 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 @@ public final class MidiDispatcher extends MidiReceiver { } /** - * 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.os.RemoteException; 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.ParcelFileDescriptor; 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..7c101a4 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 @@ package com.android.server.usb; 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; |
