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;
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<Long, FastEventQueue> mEventBuffer;
private FastEventQueue mEventPool = null;
protected volatile SortedMap<Long, FastEventQueue> mEventBuffer;
protected FastEventQueue mEventPool = null;
private int mMaxPoolSize = 200;
private boolean mClosed;
@@ -38,9 +37,13 @@ public class 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 {
/**
* 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<Long, FastEventQueue>();
}
/**
* 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;
}
}

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.
*/
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) {

View File

@@ -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<UsbDeviceConnection> mUsbDeviceConnections;
// Array of endpoints by device connection.
private ArrayList<ArrayList<UsbEndpoint>> mInputUsbEndpoints;
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 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<UsbDeviceConnection>();
mInputUsbEndpoints = 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>();
for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) {
ArrayList<UsbEndpoint> inputEndpoints = 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);
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;
}
}

View File

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

View File

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