Merge "USB MIDI: Multijack MIDI 1.0 for MIDI 2.0 devices" into udc-dev

This commit is contained in:
TreeHugger Robot
2023-02-28 23:26:47 +00:00
committed by Android (Google) Code Review
6 changed files with 500 additions and 147 deletions

View File

@@ -16,7 +16,6 @@
package com.android.internal.midi; package com.android.internal.midi;
import java.util.Iterator;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; import java.util.TreeMap;
@@ -26,11 +25,11 @@ import java.util.TreeMap;
* And only one Thread can read from the buffer. * And only one Thread can read from the buffer.
*/ */
public class EventScheduler { 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(); private final Object mLock = new Object();
volatile private SortedMap<Long, FastEventQueue> mEventBuffer; protected volatile SortedMap<Long, FastEventQueue> mEventBuffer;
private FastEventQueue mEventPool = null; protected FastEventQueue mEventPool = null;
private int mMaxPoolSize = 200; private int mMaxPoolSize = 200;
private boolean mClosed; private boolean mClosed;
@@ -38,9 +37,13 @@ public class EventScheduler {
mEventBuffer = new TreeMap<Long, FastEventQueue>(); mEventBuffer = new TreeMap<Long, FastEventQueue>();
} }
// If we keep at least one node in the list then it can be atomic /**
// and non-blocking. * Class for a fast event queue.
private class FastEventQueue { *
* 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. // One thread takes from the beginning of the list.
volatile SchedulableEvent mFirst; volatile SchedulableEvent mFirst;
// A second thread returns events to the end of the list. // A second thread returns events to the end of the list.
@@ -48,7 +51,7 @@ public class EventScheduler {
volatile long mEventsAdded; volatile long mEventsAdded;
volatile long mEventsRemoved; volatile long mEventsRemoved;
FastEventQueue(SchedulableEvent event) { public FastEventQueue(SchedulableEvent event) {
mFirst = event; mFirst = event;
mLast = mFirst; mLast = mFirst;
mEventsAdded = 1; mEventsAdded = 1;
@@ -149,7 +152,8 @@ public class EventScheduler {
* @param event * @param event
*/ */
public void add(SchedulableEvent event) { public void add(SchedulableEvent event) {
synchronized (mLock) { Object lock = getLock();
synchronized (lock) {
FastEventQueue list = mEventBuffer.get(event.getTimestamp()); FastEventQueue list = mEventBuffer.get(event.getTimestamp());
if (list == null) { if (list == null) {
long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE 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 // If the event we added is earlier than the previous earliest
// event then notify any threads waiting for the next event. // event then notify any threads waiting for the next event.
if (event.getTimestamp() < lowestTime) { if (event.getTimestamp() < lowestTime) {
mLock.notify(); lock.notify();
} }
} else { } else {
list.add(event); list.add(event);
@@ -167,7 +171,7 @@ public class EventScheduler {
} }
} }
private SchedulableEvent removeNextEventLocked(long lowestTime) { protected SchedulableEvent removeNextEventLocked(long lowestTime) {
SchedulableEvent event; SchedulableEvent event;
FastEventQueue list = mEventBuffer.get(lowestTime); FastEventQueue list = mEventBuffer.get(lowestTime);
// Remove list from tree if this is the last node. // Remove list from tree if this is the last node.
@@ -186,7 +190,8 @@ public class EventScheduler {
*/ */
public SchedulableEvent getNextEvent(long time) { public SchedulableEvent getNextEvent(long time) {
SchedulableEvent event = null; SchedulableEvent event = null;
synchronized (mLock) { Object lock = getLock();
synchronized (lock) {
if (!mEventBuffer.isEmpty()) { if (!mEventBuffer.isEmpty()) {
long lowestTime = mEventBuffer.firstKey(); long lowestTime = mEventBuffer.firstKey();
// Is it time for this list to be processed? // Is it time for this list to be processed?
@@ -209,7 +214,8 @@ public class EventScheduler {
*/ */
public SchedulableEvent waitNextEvent() throws InterruptedException { public SchedulableEvent waitNextEvent() throws InterruptedException {
SchedulableEvent event = null; SchedulableEvent event = null;
synchronized (mLock) { Object lock = getLock();
synchronized (lock) {
while (!mClosed) { while (!mClosed) {
long millisToWait = Integer.MAX_VALUE; long millisToWait = Integer.MAX_VALUE;
if (!mEventBuffer.isEmpty()) { if (!mEventBuffer.isEmpty()) {
@@ -231,7 +237,7 @@ public class EventScheduler {
} }
} }
} }
mLock.wait((int) millisToWait); lock.wait((int) millisToWait);
} }
} }
return event; return event;
@@ -242,10 +248,25 @@ public class EventScheduler {
mEventBuffer = new TreeMap<Long, FastEventQueue>(); mEventBuffer = new TreeMap<Long, FastEventQueue>();
} }
/**
* Stops the EventScheduler.
* The subscriber calling waitNextEvent() will get one final SchedulableEvent returning null.
*/
public void close() { public void close() {
synchronized (mLock) { Object lock = getLock();
synchronized (lock) {
mClosed = true; 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;
}
} }

View File

@@ -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();
}
}
}

View File

@@ -79,7 +79,7 @@ public class MidiEventScheduler extends EventScheduler {
/** /**
* Create an event that contains the message. * 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) { long timestamp) {
MidiEvent event; MidiEvent event;
if (count > POOL_EVENT_SIZE) { if (count > POOL_EVENT_SIZE) {

View File

@@ -19,7 +19,6 @@ package com.android.server.usb;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.content.Context; import android.content.Context;
import android.hardware.usb.UsbConfiguration; import android.hardware.usb.UsbConfiguration;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbEndpoint;
@@ -35,9 +34,12 @@ import android.os.Bundle;
import android.service.usb.UsbDirectMidiDeviceProto; import android.service.usb.UsbDirectMidiDeviceProto;
import android.util.Log; import android.util.Log;
import com.android.internal.midi.MidiEventMultiScheduler;
import com.android.internal.midi.MidiEventScheduler; import com.android.internal.midi.MidiEventScheduler;
import com.android.internal.midi.MidiEventScheduler.MidiEvent; import com.android.internal.midi.MidiEventScheduler.MidiEvent;
import com.android.internal.util.dump.DualDumpOutputStream; 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.UsbDescriptorParser;
import com.android.server.usb.descriptors.UsbEndpointDescriptor; import com.android.server.usb.descriptors.UsbEndpointDescriptor;
import com.android.server.usb.descriptors.UsbInterfaceDescriptor; import com.android.server.usb.descriptors.UsbInterfaceDescriptor;
@@ -45,6 +47,7 @@ import com.android.server.usb.descriptors.UsbMidiBlockParser;
import libcore.io.IoUtils; import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -56,7 +59,7 @@ import java.util.ArrayList;
*/ */
public final class UsbDirectMidiDevice implements Closeable { public final class UsbDirectMidiDevice implements Closeable {
private static final String TAG = "UsbDirectMidiDevice"; private static final String TAG = "UsbDirectMidiDevice";
private static final boolean DEBUG = false; private static final boolean DEBUG = true;
private Context mContext; private Context mContext;
private String mName; private String mName;
@@ -74,9 +77,6 @@ public final class UsbDirectMidiDevice implements Closeable {
private MidiDeviceServer mServer; private MidiDeviceServer mServer;
// event schedulers for each input port of the physical device
private MidiEventScheduler[] mEventSchedulers;
// Timeout for sending a packet to a device. // Timeout for sending a packet to a device.
// If bulkTransfer times out, retry sending the packet up to 20 times. // If bulkTransfer times out, retry sending the packet up to 20 times.
private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50; 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 static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200;
private ArrayList<UsbDeviceConnection> mUsbDeviceConnections; private ArrayList<UsbDeviceConnection> mUsbDeviceConnections;
// Array of endpoints by device connection.
private ArrayList<ArrayList<UsbEndpoint>> mInputUsbEndpoints; private ArrayList<ArrayList<UsbEndpoint>> mInputUsbEndpoints;
private ArrayList<ArrayList<UsbEndpoint>> mOutputUsbEndpoints; private ArrayList<ArrayList<UsbEndpoint>> 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<ArrayList<Integer>> mInputUsbEndpointCableCounts;
private ArrayList<ArrayList<Integer>> mOutputUsbEndpointCableCounts;
// Array of event schedulers by device connection.
// Each number here maps to an entry in mOutputUsbEndpoints.
private ArrayList<ArrayList<MidiEventMultiScheduler>> mMidiEventMultiSchedulers;
private ArrayList<Thread> mThreads; private ArrayList<Thread> mThreads;
private UsbMidiBlockParser mMidiBlockParser = new UsbMidiBlockParser(); private UsbMidiBlockParser mMidiBlockParser = new UsbMidiBlockParser();
@@ -97,8 +110,6 @@ public final class UsbDirectMidiDevice implements Closeable {
private boolean mIsOpen; private boolean mIsOpen;
private boolean mServerAvailable; private boolean mServerAvailable;
private UsbMidiPacketConverter mUsbMidiPacketConverter;
private PowerBoostSetter mPowerBoostSetter = null; private PowerBoostSetter mPowerBoostSetter = null;
private static final byte MESSAGE_TYPE_MIDI_1_CHANNEL_VOICE = 0x02; private static final byte MESSAGE_TYPE_MIDI_1_CHANNEL_VOICE = 0x02;
@@ -238,9 +249,9 @@ public final class UsbDirectMidiDevice implements Closeable {
interfaceDescriptor.getEndpointDescriptor(endpointIndex); interfaceDescriptor.getEndpointDescriptor(endpointIndex);
// 0 is output, 1 << 7 is input. // 0 is output, 1 << 7 is input.
if (endpoint.getDirection() == 0) { if (endpoint.getDirection() == 0) {
numOutputs++; numOutputs += getNumJacks(endpoint);
} else { } else {
numInputs++; numInputs += getNumJacks(endpoint);
} }
} }
} }
@@ -307,19 +318,21 @@ public final class UsbDirectMidiDevice implements Closeable {
Log.d(TAG, "openLocked()"); Log.d(TAG, "openLocked()");
UsbManager manager = mContext.getSystemService(UsbManager.class); 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<UsbDeviceConnection>(); mUsbDeviceConnections = new ArrayList<UsbDeviceConnection>();
mInputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>(); mInputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>();
mOutputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>(); mOutputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>();
mInputUsbEndpointCableCounts = new ArrayList<ArrayList<Integer>>();
mOutputUsbEndpointCableCounts = new ArrayList<ArrayList<Integer>>();
mMidiEventMultiSchedulers = new ArrayList<ArrayList<MidiEventMultiScheduler>>();
mThreads = new ArrayList<Thread>(); mThreads = new ArrayList<Thread>();
for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) { for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) {
ArrayList<UsbEndpoint> inputEndpoints = new ArrayList<UsbEndpoint>(); ArrayList<UsbEndpoint> inputEndpoints = new ArrayList<UsbEndpoint>();
ArrayList<UsbEndpoint> outputEndpoints = new ArrayList<UsbEndpoint>(); ArrayList<UsbEndpoint> outputEndpoints = new ArrayList<UsbEndpoint>();
ArrayList<Integer> inputEndpointCableCounts = new ArrayList<Integer>();
ArrayList<Integer> outputEndpointCableCounts = new ArrayList<Integer>();
ArrayList<MidiEventMultiScheduler> midiEventMultiSchedulers =
new ArrayList<MidiEventMultiScheduler>();
UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex); UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex);
for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints(); for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints();
endpointIndex++) { endpointIndex++) {
@@ -328,8 +341,13 @@ public final class UsbDirectMidiDevice implements Closeable {
// 0 is output, 1 << 7 is input. // 0 is output, 1 << 7 is input.
if (endpoint.getDirection() == 0) { if (endpoint.getDirection() == 0) {
outputEndpoints.add(endpoint.toAndroid(mParser)); outputEndpoints.add(endpoint.toAndroid(mParser));
outputEndpointCableCounts.add(getNumJacks(endpoint));
MidiEventMultiScheduler scheduler =
new MidiEventMultiScheduler(getNumJacks(endpoint));
midiEventMultiSchedulers.add(scheduler);
} else { } else {
inputEndpoints.add(endpoint.toAndroid(mParser)); inputEndpoints.add(endpoint.toAndroid(mParser));
inputEndpointCableCounts.add(getNumJacks(endpoint));
} }
} }
if (!outputEndpoints.isEmpty() || !inputEndpoints.isEmpty()) { if (!outputEndpoints.isEmpty() || !inputEndpoints.isEmpty()) {
@@ -341,40 +359,69 @@ public final class UsbDirectMidiDevice implements Closeable {
mUsbDeviceConnections.add(connection); mUsbDeviceConnections.add(connection);
mInputUsbEndpoints.add(inputEndpoints); mInputUsbEndpoints.add(inputEndpoints);
mOutputUsbEndpoints.add(outputEndpoints); mOutputUsbEndpoints.add(outputEndpoints);
mInputUsbEndpointCableCounts.add(inputEndpointCableCounts);
mOutputUsbEndpointCableCounts.add(outputEndpointCableCounts);
mMidiEventMultiSchedulers.add(midiEventMultiSchedulers);
} }
} }
mEventSchedulers = new MidiEventScheduler[mNumOutputs]; // Set up event schedulers
int outputIndex = 0;
for (int i = 0; i < mNumOutputs; i++) { for (int connectionIndex = 0; connectionIndex < mMidiEventMultiSchedulers.size();
MidiEventScheduler scheduler = new MidiEventScheduler(); connectionIndex++) {
mEventSchedulers[i] = scheduler; for (int endpointIndex = 0;
mMidiInputPortReceivers[i].setReceiver(scheduler.getReceiver()); 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(); final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
// Create input thread for each input port of the physical device // Create input thread for each input port of the physical device
int portNumber = 0; int portStartNumber = 0;
for (int connectionIndex = 0; connectionIndex < mInputUsbEndpoints.size(); for (int connectionIndex = 0; connectionIndex < mInputUsbEndpoints.size();
connectionIndex++) { connectionIndex++) {
for (int endpointIndex = 0; for (int endpointIndex = 0;
endpointIndex < mInputUsbEndpoints.get(connectionIndex).size(); endpointIndex < mInputUsbEndpoints.get(connectionIndex).size();
endpointIndex++) { 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 = final UsbDeviceConnection connectionFinal =
mUsbDeviceConnections.get(connectionIndex); mUsbDeviceConnections.get(connectionIndex);
final UsbEndpoint endpointFinal = final UsbEndpoint endpointFinal =
mInputUsbEndpoints.get(connectionIndex).get(endpointIndex); 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 @Override
public void run() { public void run() {
final UsbRequest request = new UsbRequest(); final UsbRequest request = new UsbRequest();
final UsbMidiPacketConverter packetConverter = new UsbMidiPacketConverter();
packetConverter.createDecoders(cableCountFinal);
try { try {
request.initialize(connectionFinal, endpointFinal); request.initialize(connectionFinal, endpointFinal);
byte[] inputBuffer = new byte[endpointFinal.getMaxPacketSize()]; byte[] inputBuffer = new byte[endpointFinal.getMaxPacketSize()];
while (true) { boolean keepGoing = true;
while (keepGoing) {
if (Thread.currentThread().interrupted()) { if (Thread.currentThread().interrupted()) {
Log.w(TAG, "input thread interrupted"); Log.w(TAG, "input thread interrupted");
break; break;
@@ -404,45 +451,59 @@ public final class UsbDirectMidiDevice implements Closeable {
logByteArray("Input before conversion ", inputBuffer, logByteArray("Input before conversion ", inputBuffer,
0, bytesRead); 0, bytesRead);
} }
// Add packets into the packet decoder.
if (!mIsUniversalMidiDevice) {
packetConverter.decodeMidiPackets(inputBuffer, bytesRead);
}
byte[] convertedArray; byte[] convertedArray;
for (int cableNumber = 0; cableNumber < cableCountFinal;
cableNumber++) {
if (mIsUniversalMidiDevice) { if (mIsUniversalMidiDevice) {
// For USB, each 32 bit word of a UMP is // For USB, each 32 bit word of a UMP is
// sent with the least significant byte first. // sent with the least significant byte first.
convertedArray = swapEndiannessPerWord(inputBuffer, convertedArray = swapEndiannessPerWord(inputBuffer,
bytesRead); bytesRead);
} else { } else {
if (mUsbMidiPacketConverter == null) {
Log.w(TAG, "mUsbMidiPacketConverter is null");
break;
}
convertedArray = convertedArray =
mUsbMidiPacketConverter.usbMidiToRawMidi( packetConverter.pullDecodedMidiPackets(
inputBuffer, bytesRead); cableNumber);
} }
if (DEBUG) { if (DEBUG) {
logByteArray("Input after conversion ", convertedArray, logByteArray("Input " + cableNumber
0, convertedArray.length); + " after conversion ", convertedArray, 0,
convertedArray.length);
}
if (convertedArray.length == 0) {
continue;
} }
if ((outputReceivers == null) if ((outputReceivers == null)
|| (outputReceivers[portFinal] == null)) { || (outputReceivers[portStartFinal + cableNumber]
== null)) {
Log.w(TAG, "outputReceivers is null"); Log.w(TAG, "outputReceivers is null");
keepGoing = false;
break; break;
} }
outputReceivers[portFinal].send(convertedArray, 0, outputReceivers[portStartFinal + cableNumber].send(
convertedArray.length, timestamp); convertedArray, 0, convertedArray.length,
timestamp);
// Boost power if there seems to be a voice message. // Boost power if there seems to be a voice message.
// For legacy devices, boost when message is more than size 1. // For legacy devices, boost if message length > 1.
// For UMP devices, boost for channel voice messages. // For UMP devices, boost for channel voice messages.
if ((mPowerBoostSetter != null && convertedArray.length > 1) if ((mPowerBoostSetter != null
&& convertedArray.length > 1)
&& (!mIsUniversalMidiDevice && (!mIsUniversalMidiDevice
|| isChannelVoiceMessage(convertedArray))) { || isChannelVoiceMessage(convertedArray))) {
mPowerBoostSetter.boostPower(); mPowerBoostSetter.boostPower();
} }
} }
} }
}
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, "reader thread exiting"); Log.d(TAG, "reader thread exiting");
} catch (NullPointerException e) { } catch (NullPointerException e) {
@@ -455,64 +516,93 @@ public final class UsbDirectMidiDevice implements Closeable {
}; };
newThread.start(); newThread.start();
mThreads.add(newThread); mThreads.add(newThread);
portNumber++; portStartNumber += cableCountFinal;
} }
} }
// Create output thread for each output port of the physical device // Create output thread for each output port of the physical device
portNumber = 0; portStartNumber = 0;
for (int connectionIndex = 0; connectionIndex < mOutputUsbEndpoints.size(); for (int connectionIndex = 0; connectionIndex < mOutputUsbEndpoints.size();
connectionIndex++) { connectionIndex++) {
for (int endpointIndex = 0; for (int endpointIndex = 0;
endpointIndex < mOutputUsbEndpoints.get(connectionIndex).size(); endpointIndex < mOutputUsbEndpoints.get(connectionIndex).size();
endpointIndex++) { 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 = final UsbDeviceConnection connectionFinal =
mUsbDeviceConnections.get(connectionIndex); mUsbDeviceConnections.get(connectionIndex);
final UsbEndpoint endpointFinal = final UsbEndpoint endpointFinal =
mOutputUsbEndpoints.get(connectionIndex).get(endpointIndex); mOutputUsbEndpoints.get(connectionIndex).get(endpointIndex);
final int portFinal = portNumber; final int portStartFinal = portStartNumber;
final MidiEventScheduler eventSchedulerFinal = mEventSchedulers[portFinal]; 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 @Override
public void run() { public void run() {
try { try {
while (true) { final ByteArrayOutputStream midi2ByteStream =
if (Thread.currentThread().interrupted()) { new ByteArrayOutputStream();
Log.w(TAG, "output thread interrupted"); 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; break;
} }
MidiEvent event; long now = System.nanoTime();
try { for (int cableNumber = 0; cableNumber < cableCountFinal;
event = (MidiEvent) eventSchedulerFinal.waitNextEvent(); cableNumber++) {
} catch (InterruptedException e) { MidiEventScheduler eventScheduler =
Log.w(TAG, "event scheduler interrupted"); multiSchedulerFinal.getEventScheduler(cableNumber);
break; MidiEvent event =
} (MidiEvent) eventScheduler.getNextEvent(now);
if (event == null) { while (event != null) {
Log.w(TAG, "event is null");
break;
}
if (DEBUG) { if (DEBUG) {
logByteArray("Output before conversion ", event.data, 0, logByteArray("Output before conversion ",
event.count); event.data, 0, event.count);
} }
byte[] convertedArray;
if (mIsUniversalMidiDevice) { if (mIsUniversalMidiDevice) {
// For USB, each 32 bit word of a UMP is // For USB, each 32 bit word of a UMP is
// sent with the least significant byte first. // sent with the least significant byte first.
convertedArray = swapEndiannessPerWord(event.data, byte[] convertedArray = swapEndiannessPerWord(
event.count); event.data, event.count);
midi2ByteStream.write(convertedArray, 0,
convertedArray.length);
} else { } else {
if (mUsbMidiPacketConverter == null) { packetConverter.encodeMidiPackets(event.data,
Log.w(TAG, "mUsbMidiPacketConverter is null"); event.count, cableNumber);
}
eventScheduler.addEventToPool(event);
event = (MidiEvent) eventScheduler.getNextEvent(now);
}
}
if (Thread.currentThread().interrupted()) {
Log.d(TAG, "output thread interrupted");
break; break;
} }
byte[] convertedArray = new byte[0];
if (mIsUniversalMidiDevice) {
convertedArray = midi2ByteStream.toByteArray();
midi2ByteStream.reset();
} else {
convertedArray = convertedArray =
mUsbMidiPacketConverter.rawMidiToUsbMidi( packetConverter.pullEncodedMidiPackets();
event.data, event.count, portFinal);
} }
if (DEBUG) { if (DEBUG) {
@@ -520,7 +610,6 @@ public final class UsbDirectMidiDevice implements Closeable {
convertedArray.length); convertedArray.length);
} }
boolean isInterrupted = false;
// Split the packet into multiple if they are greater than the // Split the packet into multiple if they are greater than the
// endpoint's max packet size. // endpoint's max packet size.
for (int curPacketStart = 0; 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) { } catch (NullPointerException e) {
Log.e(TAG, "output thread: ", e); Log.e(TAG, "output thread: ", e);
} }
@@ -571,7 +658,7 @@ public final class UsbDirectMidiDevice implements Closeable {
}; };
newThread.start(); newThread.start();
mThreads.add(newThread); mThreads.add(newThread);
portNumber++; portStartNumber += cableCountFinal;
} }
} }
@@ -667,11 +754,21 @@ public final class UsbDirectMidiDevice implements Closeable {
} }
mThreads = null; mThreads = null;
for (int i = 0; i < mEventSchedulers.length; i++) { for (int i = 0; i < mMidiInputPortReceivers.length; i++) {
mMidiInputPortReceivers[i].setReceiver(null); 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) { for (UsbDeviceConnection connection : mUsbDeviceConnections) {
connection.close(); connection.close();
@@ -679,8 +776,8 @@ public final class UsbDirectMidiDevice implements Closeable {
mUsbDeviceConnections = null; mUsbDeviceConnections = null;
mInputUsbEndpoints = null; mInputUsbEndpoints = null;
mOutputUsbEndpoints = null; mOutputUsbEndpoints = null;
mInputUsbEndpointCableCounts = null;
mUsbMidiPacketConverter = null; mOutputUsbEndpointCableCounts = null;
mIsOpen = false; mIsOpen = false;
} }
@@ -788,4 +885,19 @@ public final class UsbDirectMidiDevice implements Closeable {
return messageType == MESSAGE_TYPE_MIDI_1_CHANNEL_VOICE return messageType == MESSAGE_TYPE_MIDI_1_CHANNEL_VOICE
|| messageType == MESSAGE_TYPE_MIDI_2_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;
}
} }

View File

@@ -16,12 +16,17 @@
package com.android.server.usb; package com.android.server.usb;
import android.util.Log;
import java.io.ByteArrayOutputStream; 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 { public class UsbMidiPacketConverter {
private static final String TAG = "UsbMidiPacketConverter";
// Refer to Table 4-1 in USB MIDI 1.0 spec. // Refer to Table 4-1 in USB MIDI 1.0 spec.
private static final int[] PAYLOAD_SIZE = new int[]{ 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_START_EXCLUSIVE = (byte) 0xF0;
private static final byte SYSEX_END_EXCLUSIVE = (byte) 0xF7; private static final byte SYSEX_END_EXCLUSIVE = (byte) 0xF7;
private UsbMidiDecoder mUsbMidiDecoder = new UsbMidiDecoder();
private UsbMidiEncoder[] mUsbMidiEncoders; private UsbMidiEncoder[] mUsbMidiEncoders;
private ByteArrayOutputStream mEncoderOutputStream = new ByteArrayOutputStream();
public UsbMidiPacketConverter(int numEncoders) { private UsbMidiDecoder mUsbMidiDecoder;
mUsbMidiEncoders = new UsbMidiEncoder[numEncoders];
for (int i = 0; i < numEncoders; i++) {
mUsbMidiEncoders[i] = new UsbMidiEncoder();
}
}
/** /**
* Converts a USB MIDI array into a raw MIDI array. * Creates encoders.
* *
* @param usbMidiBytes the USB MIDI bytes to convert * createEncoders() must be called before raw MIDI can be converted to USB MIDI.
* @param size the size of usbMidiBytes *
* @return byte array of raw MIDI packets * @param size the number of encoders to create
*/ */
public byte[] usbMidiToRawMidi(byte[] usbMidiBytes, int size) { public void createEncoders(int size) {
return mUsbMidiDecoder.decode(usbMidiBytes, 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. * 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 midiBytes the raw MIDI bytes to convert
* @param size the size of usbMidiBytes * @param size the size of usbMidiBytes
* @param encoderId which encoder to use * @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 * @return byte array of USB MIDI packets
*/ */
public byte[] rawMidiToUsbMidi(byte[] midiBytes, int size, int encoderId) { public byte[] pullEncodedMidiPackets() {
return mUsbMidiEncoders[encoderId].encode(midiBytes, size); 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 { 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. // Decodes the data from USB MIDI to raw MIDI.
// Each valid 4 byte input maps to a 1-3 byte output. // Each valid 4 byte input maps to a 1-3 byte output.
// Reference the USB MIDI 1.0 spec for more info. // 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(); 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) { for (int i = 0; i + 3 < size; i += 4) {
int cableNumber = (usbMidiBytes[i] >> 4) & 0x0f;
int codeIndex = usbMidiBytes[i] & 0x0f; int codeIndex = usbMidiBytes[i] & 0x0f;
int numPayloadBytes = PAYLOAD_SIZE[codeIndex]; int numPayloadBytes = PAYLOAD_SIZE[codeIndex];
if (numPayloadBytes < 0) { if (numPayloadBytes < 0) {
continue; 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;
} }
return outputStream.toByteArray(); mDecodedByteArrays[cableNumber].write(usbMidiBytes, i + 1, numPayloadBytes);
}
}
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[] 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. // Encodes the data from raw MIDI to USB MIDI.
// Each valid 1-3 byte input maps to a 4 byte output. // Each valid 1-3 byte input maps to a 4 byte output.
// Reference the USB MIDI 1.0 spec for more info. // Reference the USB MIDI 1.0 spec for more info.
@@ -153,7 +244,8 @@ public class UsbMidiPacketConverter {
midiBytes[curLocation]; midiBytes[curLocation];
mNumStoredSystemExclusiveBytes++; mNumStoredSystemExclusiveBytes++;
if (mNumStoredSystemExclusiveBytes == 3) { 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); outputStream.write(mStoredSystemExclusiveBytes, 0, 3);
mNumStoredSystemExclusiveBytes = 0; mNumStoredSystemExclusiveBytes = 0;
} }
@@ -179,7 +271,7 @@ public class UsbMidiPacketConverter {
byte codeIndexNumber = (byte) ((midiBytes[curLocation] >> 4) & 0x0f); byte codeIndexNumber = (byte) ((midiBytes[curLocation] >> 4) & 0x0f);
int channelMessageSize = PAYLOAD_SIZE[codeIndexNumber]; int channelMessageSize = PAYLOAD_SIZE[codeIndexNumber];
if (curLocation + channelMessageSize <= size) { if (curLocation + channelMessageSize <= size) {
outputStream.write(codeIndexNumber); outputStream.write(codeIndexNumber | mShiftedCableNumber);
outputStream.write(midiBytes, curLocation, channelMessageSize); outputStream.write(midiBytes, curLocation, channelMessageSize);
// Fill in the rest of the bytes with 0. // Fill in the rest of the bytes with 0.
outputStream.write(mEmptyBytes, 0, 3 - channelMessageSize); outputStream.write(mEmptyBytes, 0, 3 - channelMessageSize);
@@ -197,8 +289,8 @@ public class UsbMidiPacketConverter {
curLocation++; curLocation++;
} else if (midiBytes[curLocation] == SYSEX_END_EXCLUSIVE) { } else if (midiBytes[curLocation] == SYSEX_END_EXCLUSIVE) {
// 1 byte is 0x05, 2 bytes is 0x06, and 3 bytes is 0x07 // 1 byte is 0x05, 2 bytes is 0x06, and 3 bytes is 0x07
outputStream.write(CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE outputStream.write((CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE
+ mNumStoredSystemExclusiveBytes); + mNumStoredSystemExclusiveBytes) | mShiftedCableNumber);
mStoredSystemExclusiveBytes[mNumStoredSystemExclusiveBytes] = mStoredSystemExclusiveBytes[mNumStoredSystemExclusiveBytes] =
midiBytes[curLocation]; midiBytes[curLocation];
mNumStoredSystemExclusiveBytes++; mNumStoredSystemExclusiveBytes++;
@@ -218,7 +310,7 @@ public class UsbMidiPacketConverter {
} else { } else {
int systemMessageSize = PAYLOAD_SIZE[codeIndexNumber]; int systemMessageSize = PAYLOAD_SIZE[codeIndexNumber];
if (curLocation + systemMessageSize <= size) { if (curLocation + systemMessageSize <= size) {
outputStream.write(codeIndexNumber); outputStream.write(codeIndexNumber | mShiftedCableNumber);
outputStream.write(midiBytes, curLocation, systemMessageSize); outputStream.write(midiBytes, curLocation, systemMessageSize);
// Fill in the rest of the bytes with 0. // Fill in the rest of the bytes with 0.
outputStream.write(mEmptyBytes, 0, 3 - systemMessageSize); outputStream.write(mEmptyBytes, 0, 3 - systemMessageSize);
@@ -236,7 +328,7 @@ public class UsbMidiPacketConverter {
} }
private void writeSingleByte(ByteArrayOutputStream outputStream, byte byteToWrite) { 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(byteToWrite);
outputStream.write(0); outputStream.write(0);
outputStream.write(0); outputStream.write(0);

View File

@@ -118,7 +118,7 @@ public class UsbEndpointDescriptor extends UsbDescriptor {
mClassSpecificEndpointDescriptor = descriptor; mClassSpecificEndpointDescriptor = descriptor;
} }
UsbDescriptor getClassSpecificEndpointDescriptor() { public UsbDescriptor getClassSpecificEndpointDescriptor() {
return mClassSpecificEndpointDescriptor; return mClassSpecificEndpointDescriptor;
} }