diff --git a/core/java/com/android/internal/midi/EventScheduler.java b/core/java/com/android/internal/midi/EventScheduler.java index 506902f61fd62..426cd74074ebf 100644 --- a/core/java/com/android/internal/midi/EventScheduler.java +++ b/core/java/com/android/internal/midi/EventScheduler.java @@ -16,7 +16,6 @@ package com.android.internal.midi; -import java.util.Iterator; import java.util.SortedMap; import java.util.TreeMap; @@ -26,11 +25,11 @@ import java.util.TreeMap; * And only one Thread can read from the buffer. */ public class EventScheduler { - private static final long NANOS_PER_MILLI = 1000000; + public static final long NANOS_PER_MILLI = 1000000; private final Object mLock = new Object(); - volatile private SortedMap mEventBuffer; - private FastEventQueue mEventPool = null; + protected volatile SortedMap mEventBuffer; + protected FastEventQueue mEventPool = null; private int mMaxPoolSize = 200; private boolean mClosed; @@ -38,9 +37,13 @@ public class EventScheduler { mEventBuffer = new TreeMap(); } - // If we keep at least one node in the list then it can be atomic - // and non-blocking. - private class FastEventQueue { + /** + * Class for a fast event queue. + * + * If we keep at least one node in the list then it can be atomic + * and non-blocking. + */ + public static 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. @@ -48,7 +51,7 @@ public class EventScheduler { volatile long mEventsAdded; volatile long mEventsRemoved; - FastEventQueue(SchedulableEvent event) { + public FastEventQueue(SchedulableEvent event) { mFirst = event; mLast = mFirst; mEventsAdded = 1; @@ -149,7 +152,8 @@ public class EventScheduler { * @param event */ public void add(SchedulableEvent event) { - synchronized (mLock) { + Object lock = getLock(); + synchronized (lock) { FastEventQueue list = mEventBuffer.get(event.getTimestamp()); if (list == null) { long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE @@ -159,7 +163,7 @@ public class EventScheduler { // 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) { - mLock.notify(); + lock.notify(); } } else { list.add(event); @@ -167,7 +171,7 @@ public class EventScheduler { } } - private SchedulableEvent removeNextEventLocked(long lowestTime) { + protected SchedulableEvent removeNextEventLocked(long lowestTime) { SchedulableEvent event; FastEventQueue list = mEventBuffer.get(lowestTime); // Remove list from tree if this is the last node. @@ -186,7 +190,8 @@ public class EventScheduler { */ public SchedulableEvent getNextEvent(long time) { SchedulableEvent event = null; - synchronized (mLock) { + Object lock = getLock(); + synchronized (lock) { if (!mEventBuffer.isEmpty()) { long lowestTime = mEventBuffer.firstKey(); // Is it time for this list to be processed? @@ -209,7 +214,8 @@ public class EventScheduler { */ public SchedulableEvent waitNextEvent() throws InterruptedException { SchedulableEvent event = null; - synchronized (mLock) { + Object lock = getLock(); + synchronized (lock) { while (!mClosed) { long millisToWait = Integer.MAX_VALUE; if (!mEventBuffer.isEmpty()) { @@ -231,7 +237,7 @@ public class EventScheduler { } } } - mLock.wait((int) millisToWait); + lock.wait((int) millisToWait); } } return event; @@ -242,10 +248,25 @@ public class EventScheduler { mEventBuffer = new TreeMap(); } + /** + * Stops the EventScheduler. + * The subscriber calling waitNextEvent() will get one final SchedulableEvent returning null. + */ public void close() { - synchronized (mLock) { + Object lock = getLock(); + synchronized (lock) { mClosed = true; - mLock.notify(); + lock.notify(); } } + + /** + * Gets the lock. This doesn't lock it in anyway. + * Subclasses can override this. + * + * @return Object + */ + protected Object getLock() { + return mLock; + } } diff --git a/core/java/com/android/internal/midi/MidiEventMultiScheduler.java b/core/java/com/android/internal/midi/MidiEventMultiScheduler.java new file mode 100644 index 0000000000000..16e4abecebdd8 --- /dev/null +++ b/core/java/com/android/internal/midi/MidiEventMultiScheduler.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 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; + +/** + * Uses multiple MidiEventSchedulers for waiting for events. + * + */ +public class MidiEventMultiScheduler { + private MultiLockMidiEventScheduler[] mMidiEventSchedulers; + private int mNumEventSchedulers; + private int mNumClosedSchedulers = 0; + private final Object mMultiLock = new Object(); + + private class MultiLockMidiEventScheduler extends MidiEventScheduler { + @Override + public void close() { + synchronized (mMultiLock) { + mNumClosedSchedulers++; + } + super.close(); + } + + @Override + protected Object getLock() { + return mMultiLock; + } + + public boolean isEventBufferEmptyLocked() { + return mEventBuffer.isEmpty(); + } + + public long getLowestTimeLocked() { + return mEventBuffer.firstKey(); + } + } + + /** + * MidiEventMultiScheduler constructor + * + * @param numSchedulers the number of schedulers to create + */ + public MidiEventMultiScheduler(int numSchedulers) { + mNumEventSchedulers = numSchedulers; + mMidiEventSchedulers = new MultiLockMidiEventScheduler[numSchedulers]; + for (int i = 0; i < numSchedulers; i++) { + mMidiEventSchedulers[i] = new MultiLockMidiEventScheduler(); + } + } + + /** + * Waits for the next MIDI event. This will return true when it receives it. + * If all MidiEventSchedulers have been closed, this will return false. + * + * @return true if a MIDI event is received and false if all schedulers are closed. + */ + public boolean waitNextEvent() throws InterruptedException { + synchronized (mMultiLock) { + while (true) { + if (mNumClosedSchedulers >= mNumEventSchedulers) { + return false; + } + long lowestTime = Long.MAX_VALUE; + long now = System.nanoTime(); + for (MultiLockMidiEventScheduler eventScheduler : mMidiEventSchedulers) { + if (!eventScheduler.isEventBufferEmptyLocked()) { + lowestTime = Math.min(lowestTime, + eventScheduler.getLowestTimeLocked()); + } + } + if (lowestTime <= now) { + return true; + } + long nanosToWait = lowestTime - now; + // Add 1 millisecond so we don't wake up before it is + // ready. + long millisToWait = 1 + (nanosToWait / EventScheduler.NANOS_PER_MILLI); + // Clip 64-bit value to 32-bit max. + if (millisToWait > Integer.MAX_VALUE) { + millisToWait = Integer.MAX_VALUE; + } + mMultiLock.wait(millisToWait); + } + } + } + + /** + * Gets the number of MidiEventSchedulers. + * + * @return the number of MidiEventSchedulers. + */ + public int getNumEventSchedulers() { + return mNumEventSchedulers; + } + + /** + * Gets a specific MidiEventScheduler based on the index. + * + * @param index the zero indexed index of a MIDI event scheduler + * @return a MidiEventScheduler + */ + public MidiEventScheduler getEventScheduler(int index) { + return mMidiEventSchedulers[index]; + } + + /** + * Closes all event schedulers. + */ + public void close() { + for (MidiEventScheduler eventScheduler : mMidiEventSchedulers) { + eventScheduler.close(); + } + } +} diff --git a/core/java/com/android/internal/midi/MidiEventScheduler.java b/core/java/com/android/internal/midi/MidiEventScheduler.java index 7b019044110fa..1b2934d519fd2 100644 --- a/core/java/com/android/internal/midi/MidiEventScheduler.java +++ b/core/java/com/android/internal/midi/MidiEventScheduler.java @@ -79,7 +79,7 @@ public class MidiEventScheduler extends EventScheduler { /** * Create an event that contains the message. */ - private MidiEvent createScheduledEvent(byte[] msg, int offset, int count, + public MidiEvent createScheduledEvent(byte[] msg, int offset, int count, long timestamp) { MidiEvent event; if (count > POOL_EVENT_SIZE) { diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java index 0dcf8ce961925..b1cd994207377 100644 --- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java @@ -19,7 +19,6 @@ package com.android.server.usb; import android.annotation.NonNull; import android.content.Context; import android.hardware.usb.UsbConfiguration; -import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; @@ -35,9 +34,12 @@ import android.os.Bundle; import android.service.usb.UsbDirectMidiDeviceProto; import android.util.Log; +import com.android.internal.midi.MidiEventMultiScheduler; import com.android.internal.midi.MidiEventScheduler; import com.android.internal.midi.MidiEventScheduler.MidiEvent; import com.android.internal.util.dump.DualDumpOutputStream; +import com.android.server.usb.descriptors.UsbACMidi10Endpoint; +import com.android.server.usb.descriptors.UsbDescriptor; import com.android.server.usb.descriptors.UsbDescriptorParser; import com.android.server.usb.descriptors.UsbEndpointDescriptor; import com.android.server.usb.descriptors.UsbInterfaceDescriptor; @@ -45,6 +47,7 @@ import com.android.server.usb.descriptors.UsbMidiBlockParser; import libcore.io.IoUtils; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; @@ -56,7 +59,7 @@ import java.util.ArrayList; */ public final class UsbDirectMidiDevice implements Closeable { private static final String TAG = "UsbDirectMidiDevice"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private Context mContext; private String mName; @@ -74,9 +77,6 @@ public final class UsbDirectMidiDevice implements Closeable { private MidiDeviceServer mServer; - // event schedulers for each input port of the physical device - private MidiEventScheduler[] mEventSchedulers; - // Timeout for sending a packet to a device. // If bulkTransfer times out, retry sending the packet up to 20 times. private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50; @@ -86,8 +86,21 @@ public final class UsbDirectMidiDevice implements Closeable { private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200; private ArrayList mUsbDeviceConnections; + + // Array of endpoints by device connection. private ArrayList> mInputUsbEndpoints; private ArrayList> mOutputUsbEndpoints; + + // Array of cable counts by device connection. + // Each number here maps to an entry in mInputUsbEndpoints or mOutputUsbEndpoints. + // This is needed because this info is part of UsbEndpointDescriptor but not UsbEndpoint. + private ArrayList> mInputUsbEndpointCableCounts; + private ArrayList> mOutputUsbEndpointCableCounts; + + // Array of event schedulers by device connection. + // Each number here maps to an entry in mOutputUsbEndpoints. + private ArrayList> mMidiEventMultiSchedulers; + private ArrayList mThreads; private UsbMidiBlockParser mMidiBlockParser = new UsbMidiBlockParser(); @@ -97,8 +110,6 @@ public final class UsbDirectMidiDevice implements Closeable { private boolean mIsOpen; private boolean mServerAvailable; - private UsbMidiPacketConverter mUsbMidiPacketConverter; - private PowerBoostSetter mPowerBoostSetter = null; private static final byte MESSAGE_TYPE_MIDI_1_CHANNEL_VOICE = 0x02; @@ -238,9 +249,9 @@ public final class UsbDirectMidiDevice implements Closeable { interfaceDescriptor.getEndpointDescriptor(endpointIndex); // 0 is output, 1 << 7 is input. if (endpoint.getDirection() == 0) { - numOutputs++; + numOutputs += getNumJacks(endpoint); } else { - numInputs++; + numInputs += getNumJacks(endpoint); } } } @@ -307,19 +318,21 @@ public final class UsbDirectMidiDevice implements Closeable { Log.d(TAG, "openLocked()"); UsbManager manager = mContext.getSystemService(UsbManager.class); - // Converting from raw MIDI to USB MIDI is not thread-safe. - // UsbMidiPacketConverter creates a converter from raw MIDI - // to USB MIDI for each USB output. - mUsbMidiPacketConverter = new UsbMidiPacketConverter(mNumOutputs); - mUsbDeviceConnections = new ArrayList(); mInputUsbEndpoints = new ArrayList>(); mOutputUsbEndpoints = new ArrayList>(); + mInputUsbEndpointCableCounts = new ArrayList>(); + mOutputUsbEndpointCableCounts = new ArrayList>(); + mMidiEventMultiSchedulers = new ArrayList>(); mThreads = new ArrayList(); for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) { ArrayList inputEndpoints = new ArrayList(); ArrayList outputEndpoints = new ArrayList(); + ArrayList inputEndpointCableCounts = new ArrayList(); + ArrayList outputEndpointCableCounts = new ArrayList(); + ArrayList midiEventMultiSchedulers = + new ArrayList(); UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex); for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints(); endpointIndex++) { @@ -328,8 +341,13 @@ public final class UsbDirectMidiDevice implements Closeable { // 0 is output, 1 << 7 is input. if (endpoint.getDirection() == 0) { outputEndpoints.add(endpoint.toAndroid(mParser)); + outputEndpointCableCounts.add(getNumJacks(endpoint)); + MidiEventMultiScheduler scheduler = + new MidiEventMultiScheduler(getNumJacks(endpoint)); + midiEventMultiSchedulers.add(scheduler); } else { inputEndpoints.add(endpoint.toAndroid(mParser)); + inputEndpointCableCounts.add(getNumJacks(endpoint)); } } if (!outputEndpoints.isEmpty() || !inputEndpoints.isEmpty()) { @@ -341,40 +359,69 @@ public final class UsbDirectMidiDevice implements Closeable { mUsbDeviceConnections.add(connection); mInputUsbEndpoints.add(inputEndpoints); mOutputUsbEndpoints.add(outputEndpoints); + mInputUsbEndpointCableCounts.add(inputEndpointCableCounts); + mOutputUsbEndpointCableCounts.add(outputEndpointCableCounts); + mMidiEventMultiSchedulers.add(midiEventMultiSchedulers); } } - mEventSchedulers = new MidiEventScheduler[mNumOutputs]; - - for (int i = 0; i < mNumOutputs; i++) { - MidiEventScheduler scheduler = new MidiEventScheduler(); - mEventSchedulers[i] = scheduler; - mMidiInputPortReceivers[i].setReceiver(scheduler.getReceiver()); + // Set up event schedulers + int outputIndex = 0; + for (int connectionIndex = 0; connectionIndex < mMidiEventMultiSchedulers.size(); + connectionIndex++) { + for (int endpointIndex = 0; + endpointIndex < mMidiEventMultiSchedulers.get(connectionIndex).size(); + endpointIndex++) { + int cableCount = + mOutputUsbEndpointCableCounts.get(connectionIndex).get(endpointIndex); + MidiEventMultiScheduler multiScheduler = + mMidiEventMultiSchedulers.get(connectionIndex).get(endpointIndex); + for (int cableNumber = 0; cableNumber < cableCount; cableNumber++) { + MidiEventScheduler scheduler = multiScheduler.getEventScheduler(cableNumber); + mMidiInputPortReceivers[outputIndex].setReceiver(scheduler.getReceiver()); + outputIndex++; + } + } } final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers(); // Create input thread for each input port of the physical device - int portNumber = 0; + int portStartNumber = 0; for (int connectionIndex = 0; connectionIndex < mInputUsbEndpoints.size(); connectionIndex++) { for (int endpointIndex = 0; endpointIndex < mInputUsbEndpoints.get(connectionIndex).size(); endpointIndex++) { + // Each USB endpoint maps to one or more outputReceivers. USB MIDI data from an + // endpoint should be sent to the appropriate outputReceiver. A new thread is + // created and waits for incoming USB data. Once the data is received, it is added + // to the packet converter. The packet converter acts as an inverse multiplexer. + // With a for loop, data is pulled per cable and sent to the correct output + // receiver. The first byte of each legacy MIDI 1.0 USB message indicates which + // cable the data should be used and is how the reverse multiplexer directs data. + // For MIDI UMP endpoints, a multiplexer is not needed as we are just swapping + // the endianness of the packets. final UsbDeviceConnection connectionFinal = mUsbDeviceConnections.get(connectionIndex); final UsbEndpoint endpointFinal = mInputUsbEndpoints.get(connectionIndex).get(endpointIndex); - final int portFinal = portNumber; + final int portStartFinal = portStartNumber; + final int cableCountFinal = + mInputUsbEndpointCableCounts.get(connectionIndex).get(endpointIndex); - Thread newThread = new Thread("UsbDirectMidiDevice input thread " + portFinal) { + Thread newThread = new Thread("UsbDirectMidiDevice input thread " + + portStartFinal) { @Override public void run() { final UsbRequest request = new UsbRequest(); + final UsbMidiPacketConverter packetConverter = new UsbMidiPacketConverter(); + packetConverter.createDecoders(cableCountFinal); try { request.initialize(connectionFinal, endpointFinal); byte[] inputBuffer = new byte[endpointFinal.getMaxPacketSize()]; - while (true) { + boolean keepGoing = true; + while (keepGoing) { if (Thread.currentThread().interrupted()) { Log.w(TAG, "input thread interrupted"); break; @@ -404,42 +451,56 @@ public final class UsbDirectMidiDevice implements Closeable { logByteArray("Input before conversion ", inputBuffer, 0, bytesRead); } + + // Add packets into the packet decoder. + if (!mIsUniversalMidiDevice) { + packetConverter.decodeMidiPackets(inputBuffer, bytesRead); + } + byte[] convertedArray; - if (mIsUniversalMidiDevice) { - // For USB, each 32 bit word of a UMP is - // sent with the least significant byte first. - convertedArray = swapEndiannessPerWord(inputBuffer, - bytesRead); - } else { - if (mUsbMidiPacketConverter == null) { - Log.w(TAG, "mUsbMidiPacketConverter is null"); + for (int cableNumber = 0; cableNumber < cableCountFinal; + cableNumber++) { + if (mIsUniversalMidiDevice) { + // For USB, each 32 bit word of a UMP is + // sent with the least significant byte first. + convertedArray = swapEndiannessPerWord(inputBuffer, + bytesRead); + } else { + convertedArray = + packetConverter.pullDecodedMidiPackets( + cableNumber); + } + + if (DEBUG) { + logByteArray("Input " + cableNumber + + " after conversion ", convertedArray, 0, + convertedArray.length); + } + + if (convertedArray.length == 0) { + continue; + } + + if ((outputReceivers == null) + || (outputReceivers[portStartFinal + cableNumber] + == null)) { + Log.w(TAG, "outputReceivers is null"); + keepGoing = false; break; } - convertedArray = - mUsbMidiPacketConverter.usbMidiToRawMidi( - inputBuffer, bytesRead); - } + outputReceivers[portStartFinal + cableNumber].send( + convertedArray, 0, convertedArray.length, + timestamp); - if (DEBUG) { - logByteArray("Input after conversion ", convertedArray, - 0, convertedArray.length); - } - - if ((outputReceivers == null) - || (outputReceivers[portFinal] == null)) { - Log.w(TAG, "outputReceivers is null"); - break; - } - outputReceivers[portFinal].send(convertedArray, 0, - convertedArray.length, timestamp); - - // Boost power if there seems to be a voice message. - // For legacy devices, boost when message is more than size 1. - // For UMP devices, boost for channel voice messages. - if ((mPowerBoostSetter != null && convertedArray.length > 1) - && (!mIsUniversalMidiDevice - || isChannelVoiceMessage(convertedArray))) { - mPowerBoostSetter.boostPower(); + // Boost power if there seems to be a voice message. + // For legacy devices, boost if message length > 1. + // For UMP devices, boost for channel voice messages. + if ((mPowerBoostSetter != null + && convertedArray.length > 1) + && (!mIsUniversalMidiDevice + || isChannelVoiceMessage(convertedArray))) { + mPowerBoostSetter.boostPower(); + } } } } @@ -455,64 +516,93 @@ public final class UsbDirectMidiDevice implements Closeable { }; newThread.start(); mThreads.add(newThread); - portNumber++; + portStartNumber += cableCountFinal; } } // Create output thread for each output port of the physical device - portNumber = 0; + portStartNumber = 0; for (int connectionIndex = 0; connectionIndex < mOutputUsbEndpoints.size(); connectionIndex++) { for (int endpointIndex = 0; endpointIndex < mOutputUsbEndpoints.get(connectionIndex).size(); endpointIndex++) { + // Each USB endpoint maps to one or more MIDI ports. Each port has an event + // scheduler that is used to pull incoming raw MIDI bytes from Android apps. + // With a MidiEventMultiScheduler, data can be pulled if any of the schedulers + // have new incoming data. This data is then packaged as USB MIDI packets before + // getting sent through USB. One thread will be created per endpoint that pulls + // data from all relevant event schedulers. Raw MIDI from the event schedulers + // will be converted to the correct USB MIDI format before getting sent through + // USB. + final UsbDeviceConnection connectionFinal = mUsbDeviceConnections.get(connectionIndex); final UsbEndpoint endpointFinal = mOutputUsbEndpoints.get(connectionIndex).get(endpointIndex); - final int portFinal = portNumber; - final MidiEventScheduler eventSchedulerFinal = mEventSchedulers[portFinal]; + final int portStartFinal = portStartNumber; + final int cableCountFinal = + mOutputUsbEndpointCableCounts.get(connectionIndex).get(endpointIndex); + final MidiEventMultiScheduler multiSchedulerFinal = + mMidiEventMultiSchedulers.get(connectionIndex).get(endpointIndex); - Thread newThread = new Thread("UsbDirectMidiDevice output thread " + portFinal) { + Thread newThread = new Thread("UsbDirectMidiDevice output write thread " + + portStartFinal) { @Override public void run() { try { - while (true) { - if (Thread.currentThread().interrupted()) { - Log.w(TAG, "output thread interrupted"); + final ByteArrayOutputStream midi2ByteStream = + new ByteArrayOutputStream(); + final UsbMidiPacketConverter packetConverter = + new UsbMidiPacketConverter(); + packetConverter.createEncoders(cableCountFinal); + boolean isInterrupted = false; + while (!isInterrupted) { + boolean wasSuccessful = multiSchedulerFinal.waitNextEvent(); + if (!wasSuccessful) { + Log.d(TAG, "output thread closed"); break; } - MidiEvent event; - try { - event = (MidiEvent) eventSchedulerFinal.waitNextEvent(); - } catch (InterruptedException e) { - Log.w(TAG, "event scheduler interrupted"); - break; - } - if (event == null) { - Log.w(TAG, "event is null"); - break; - } - - if (DEBUG) { - logByteArray("Output before conversion ", event.data, 0, - event.count); - } - - byte[] convertedArray; - if (mIsUniversalMidiDevice) { - // For USB, each 32 bit word of a UMP is - // sent with the least significant byte first. - convertedArray = swapEndiannessPerWord(event.data, - event.count); - } else { - if (mUsbMidiPacketConverter == null) { - Log.w(TAG, "mUsbMidiPacketConverter is null"); - break; + long now = System.nanoTime(); + for (int cableNumber = 0; cableNumber < cableCountFinal; + cableNumber++) { + MidiEventScheduler eventScheduler = + multiSchedulerFinal.getEventScheduler(cableNumber); + MidiEvent event = + (MidiEvent) eventScheduler.getNextEvent(now); + while (event != null) { + if (DEBUG) { + logByteArray("Output before conversion ", + event.data, 0, event.count); + } + if (mIsUniversalMidiDevice) { + // For USB, each 32 bit word of a UMP is + // sent with the least significant byte first. + byte[] convertedArray = swapEndiannessPerWord( + event.data, event.count); + midi2ByteStream.write(convertedArray, 0, + convertedArray.length); + } else { + packetConverter.encodeMidiPackets(event.data, + event.count, cableNumber); + } + eventScheduler.addEventToPool(event); + event = (MidiEvent) eventScheduler.getNextEvent(now); } + } + + if (Thread.currentThread().interrupted()) { + Log.d(TAG, "output thread interrupted"); + break; + } + + byte[] convertedArray = new byte[0]; + if (mIsUniversalMidiDevice) { + convertedArray = midi2ByteStream.toByteArray(); + midi2ByteStream.reset(); + } else { convertedArray = - mUsbMidiPacketConverter.rawMidiToUsbMidi( - event.data, event.count, portFinal); + packetConverter.pullEncodedMidiPackets(); } if (DEBUG) { @@ -520,7 +610,6 @@ public final class UsbDirectMidiDevice implements Closeable { convertedArray.length); } - boolean isInterrupted = false; // Split the packet into multiple if they are greater than the // endpoint's max packet size. for (int curPacketStart = 0; @@ -558,11 +647,9 @@ public final class UsbDirectMidiDevice implements Closeable { } } } - if (isInterrupted == true) { - break; - } - eventSchedulerFinal.addEventToPool(event); } + } catch (InterruptedException e) { + Log.w(TAG, "output thread: ", e); } catch (NullPointerException e) { Log.e(TAG, "output thread: ", e); } @@ -571,7 +658,7 @@ public final class UsbDirectMidiDevice implements Closeable { }; newThread.start(); mThreads.add(newThread); - portNumber++; + portStartNumber += cableCountFinal; } } @@ -667,11 +754,21 @@ public final class UsbDirectMidiDevice implements Closeable { } mThreads = null; - for (int i = 0; i < mEventSchedulers.length; i++) { + for (int i = 0; i < mMidiInputPortReceivers.length; i++) { mMidiInputPortReceivers[i].setReceiver(null); - mEventSchedulers[i].close(); } - mEventSchedulers = null; + + for (int connectionIndex = 0; connectionIndex < mMidiEventMultiSchedulers.size(); + connectionIndex++) { + for (int endpointIndex = 0; + endpointIndex < mMidiEventMultiSchedulers.get(connectionIndex).size(); + endpointIndex++) { + MidiEventMultiScheduler multiScheduler = + mMidiEventMultiSchedulers.get(connectionIndex).get(endpointIndex); + multiScheduler.close(); + } + } + mMidiEventMultiSchedulers = null; for (UsbDeviceConnection connection : mUsbDeviceConnections) { connection.close(); @@ -679,8 +776,8 @@ public final class UsbDirectMidiDevice implements Closeable { mUsbDeviceConnections = null; mInputUsbEndpoints = null; mOutputUsbEndpoints = null; - - mUsbMidiPacketConverter = null; + mInputUsbEndpointCableCounts = null; + mOutputUsbEndpointCableCounts = null; mIsOpen = false; } @@ -788,4 +885,19 @@ public final class UsbDirectMidiDevice implements Closeable { return messageType == MESSAGE_TYPE_MIDI_1_CHANNEL_VOICE || messageType == MESSAGE_TYPE_MIDI_2_CHANNEL_VOICE; } + + // Returns the number of jacks for MIDI 1.0 endpoints. + // For MIDI 2.0 endpoints, this concept does not exist and each endpoint should be treated as + // one port. + private int getNumJacks(UsbEndpointDescriptor usbEndpointDescriptor) { + UsbDescriptor classSpecificEndpointDescriptor = + usbEndpointDescriptor.getClassSpecificEndpointDescriptor(); + if (classSpecificEndpointDescriptor != null + && (classSpecificEndpointDescriptor instanceof UsbACMidi10Endpoint)) { + UsbACMidi10Endpoint midiEndpoint = + (UsbACMidi10Endpoint) classSpecificEndpointDescriptor; + return midiEndpoint.getNumJacks(); + } + return 1; + } } diff --git a/services/usb/java/com/android/server/usb/UsbMidiPacketConverter.java b/services/usb/java/com/android/server/usb/UsbMidiPacketConverter.java index 56bb23681741f..65d7a41cf1ec0 100644 --- a/services/usb/java/com/android/server/usb/UsbMidiPacketConverter.java +++ b/services/usb/java/com/android/server/usb/UsbMidiPacketConverter.java @@ -16,12 +16,17 @@ package com.android.server.usb; +import android.util.Log; + import java.io.ByteArrayOutputStream; /** - * Converts between MIDI packets and USB MIDI 1.0 packets. + * Converts between raw MIDI packets and USB MIDI 1.0 packets. + * This is NOT thread-safe. Please handle locking outside this function for multiple threads. + * For data mapping to an invalid cable number, this converter will use the first cable. */ public class UsbMidiPacketConverter { + private static final String TAG = "UsbMidiPacketConverter"; // Refer to Table 4-1 in USB MIDI 1.0 spec. private static final int[] PAYLOAD_SIZE = new int[]{ @@ -74,54 +79,133 @@ public class UsbMidiPacketConverter { private static final byte SYSEX_START_EXCLUSIVE = (byte) 0xF0; private static final byte SYSEX_END_EXCLUSIVE = (byte) 0xF7; - private UsbMidiDecoder mUsbMidiDecoder = new UsbMidiDecoder(); private UsbMidiEncoder[] mUsbMidiEncoders; + private ByteArrayOutputStream mEncoderOutputStream = new ByteArrayOutputStream(); - public UsbMidiPacketConverter(int numEncoders) { - mUsbMidiEncoders = new UsbMidiEncoder[numEncoders]; - for (int i = 0; i < numEncoders; i++) { - mUsbMidiEncoders[i] = new UsbMidiEncoder(); - } - } + private UsbMidiDecoder mUsbMidiDecoder; /** - * Converts a USB MIDI array into a raw MIDI array. + * Creates encoders. * - * @param usbMidiBytes the USB MIDI bytes to convert - * @param size the size of usbMidiBytes - * @return byte array of raw MIDI packets + * createEncoders() must be called before raw MIDI can be converted to USB MIDI. + * + * @param size the number of encoders to create */ - public byte[] usbMidiToRawMidi(byte[] usbMidiBytes, int size) { - return mUsbMidiDecoder.decode(usbMidiBytes, size); + public void createEncoders(int size) { + mUsbMidiEncoders = new UsbMidiEncoder[size]; + for (int i = 0; i < size; i++) { + mUsbMidiEncoders[i] = new UsbMidiEncoder(i); + } } /** * Converts a raw MIDI array into a USB MIDI array. * + * Call pullEncodedMidiPackets to retrieve the byte array. + * * @param midiBytes the raw MIDI bytes to convert * @param size the size of usbMidiBytes * @param encoderId which encoder to use + */ + public void encodeMidiPackets(byte[] midiBytes, int size, int encoderId) { + // Use the first encoder if the encoderId is invalid. + if (encoderId >= mUsbMidiEncoders.length) { + Log.w(TAG, "encoderId " + encoderId + " invalid"); + encoderId = 0; + } + byte[] encodedPacket = mUsbMidiEncoders[encoderId].encode(midiBytes, size); + mEncoderOutputStream.write(encodedPacket, 0, encodedPacket.length); + } + + /** + * Returns the encoded MIDI packets from encodeMidiPackets + * * @return byte array of USB MIDI packets */ - public byte[] rawMidiToUsbMidi(byte[] midiBytes, int size, int encoderId) { - return mUsbMidiEncoders[encoderId].encode(midiBytes, size); + public byte[] pullEncodedMidiPackets() { + byte[] output = mEncoderOutputStream.toByteArray(); + mEncoderOutputStream.reset(); + return output; + } + + /** + * Creates decoders. + * + * createDecoders() must be called before USB MIDI can be converted to raw MIDI. + * + * @param size the number of decoders to create + */ + public void createDecoders(int size) { + mUsbMidiDecoder = new UsbMidiDecoder(size); + } + + /** + * Converts a USB MIDI array into a multiple MIDI arrays, one per cable. + * + * Call pullDecodedMidiPackets to retrieve the byte array. + * + * @param usbMidiBytes the USB MIDI bytes to convert + * @param size the size of usbMidiBytes + */ + public void decodeMidiPackets(byte[] usbMidiBytes, int size) { + mUsbMidiDecoder.decode(usbMidiBytes, size); + } + + /** + * Returns the decoded MIDI packets from decodeMidiPackets + * + * @param cableNumber the cable to pull data from + * @return byte array of raw MIDI packets + */ + public byte[] pullDecodedMidiPackets(int cableNumber) { + return mUsbMidiDecoder.pullBytes(cableNumber); } private class UsbMidiDecoder { + int mNumJacks; + ByteArrayOutputStream[] mDecodedByteArrays; + + UsbMidiDecoder(int numJacks) { + mNumJacks = numJacks; + mDecodedByteArrays = new ByteArrayOutputStream[numJacks]; + for (int i = 0; i < numJacks; i++) { + mDecodedByteArrays[i] = new ByteArrayOutputStream(); + } + } + // Decodes the data from USB MIDI to raw MIDI. // Each valid 4 byte input maps to a 1-3 byte output. // Reference the USB MIDI 1.0 spec for more info. - public byte[] decode(byte[] usbMidiBytes, int size) { + public void decode(byte[] usbMidiBytes, int size) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + if (size % 4 != 0) { + Log.w(TAG, "size " + size + " not multiple of 4"); + } for (int i = 0; i + 3 < size; i += 4) { + int cableNumber = (usbMidiBytes[i] >> 4) & 0x0f; int codeIndex = usbMidiBytes[i] & 0x0f; int numPayloadBytes = PAYLOAD_SIZE[codeIndex]; if (numPayloadBytes < 0) { continue; } - outputStream.write(usbMidiBytes, i + 1, numPayloadBytes); + // Use the first cable if the cable number is invalid. + if (cableNumber >= mNumJacks) { + Log.w(TAG, "cableNumber " + cableNumber + " invalid"); + cableNumber = 0; + } + mDecodedByteArrays[cableNumber].write(usbMidiBytes, i + 1, numPayloadBytes); } - return outputStream.toByteArray(); + } + + public byte[] pullBytes(int cableNumber) { + // Use the first cable if the cable number is invalid. + if (cableNumber >= mNumJacks) { + Log.w(TAG, "cableNumber " + cableNumber + " invalid"); + cableNumber = 0; + } + byte[] output = mDecodedByteArrays[cableNumber].toByteArray(); + mDecodedByteArrays[cableNumber].reset(); + return output; } } @@ -135,6 +219,13 @@ public class UsbMidiPacketConverter { private byte[] mEmptyBytes = new byte[3]; // Used to fill out extra data + private byte mShiftedCableNumber; + + UsbMidiEncoder(int cableNumber) { + // Jack Id is always the left nibble of every byte so shift this now. + mShiftedCableNumber = (byte) (cableNumber << 4); + } + // Encodes the data from raw MIDI to USB MIDI. // Each valid 1-3 byte input maps to a 4 byte output. // Reference the USB MIDI 1.0 spec for more info. @@ -153,7 +244,8 @@ public class UsbMidiPacketConverter { midiBytes[curLocation]; mNumStoredSystemExclusiveBytes++; if (mNumStoredSystemExclusiveBytes == 3) { - outputStream.write(CODE_INDEX_NUMBER_SYSEX_STARTS_OR_CONTINUES); + outputStream.write(CODE_INDEX_NUMBER_SYSEX_STARTS_OR_CONTINUES + | mShiftedCableNumber); outputStream.write(mStoredSystemExclusiveBytes, 0, 3); mNumStoredSystemExclusiveBytes = 0; } @@ -179,7 +271,7 @@ public class UsbMidiPacketConverter { byte codeIndexNumber = (byte) ((midiBytes[curLocation] >> 4) & 0x0f); int channelMessageSize = PAYLOAD_SIZE[codeIndexNumber]; if (curLocation + channelMessageSize <= size) { - outputStream.write(codeIndexNumber); + outputStream.write(codeIndexNumber | mShiftedCableNumber); outputStream.write(midiBytes, curLocation, channelMessageSize); // Fill in the rest of the bytes with 0. outputStream.write(mEmptyBytes, 0, 3 - channelMessageSize); @@ -197,8 +289,8 @@ public class UsbMidiPacketConverter { curLocation++; } else if (midiBytes[curLocation] == SYSEX_END_EXCLUSIVE) { // 1 byte is 0x05, 2 bytes is 0x06, and 3 bytes is 0x07 - outputStream.write(CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE - + mNumStoredSystemExclusiveBytes); + outputStream.write((CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE + + mNumStoredSystemExclusiveBytes) | mShiftedCableNumber); mStoredSystemExclusiveBytes[mNumStoredSystemExclusiveBytes] = midiBytes[curLocation]; mNumStoredSystemExclusiveBytes++; @@ -218,7 +310,7 @@ public class UsbMidiPacketConverter { } else { int systemMessageSize = PAYLOAD_SIZE[codeIndexNumber]; if (curLocation + systemMessageSize <= size) { - outputStream.write(codeIndexNumber); + outputStream.write(codeIndexNumber | mShiftedCableNumber); outputStream.write(midiBytes, curLocation, systemMessageSize); // Fill in the rest of the bytes with 0. outputStream.write(mEmptyBytes, 0, 3 - systemMessageSize); @@ -236,7 +328,7 @@ public class UsbMidiPacketConverter { } private void writeSingleByte(ByteArrayOutputStream outputStream, byte byteToWrite) { - outputStream.write(CODE_INDEX_NUMBER_SINGLE_BYTE); + outputStream.write(CODE_INDEX_NUMBER_SINGLE_BYTE | mShiftedCableNumber); outputStream.write(byteToWrite); outputStream.write(0); outputStream.write(0); diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java index 1f448acac5e88..117a3d9f374e7 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java @@ -118,7 +118,7 @@ public class UsbEndpointDescriptor extends UsbDescriptor { mClassSpecificEndpointDescriptor = descriptor; } - UsbDescriptor getClassSpecificEndpointDescriptor() { + public UsbDescriptor getClassSpecificEndpointDescriptor() { return mClassSpecificEndpointDescriptor; }