Use USB API to get # of i/o ports for MIDI with USB.

Currently, the number of MIDI devices is determined using an ALSA ioctl.
This method is flaky on devices that don't simply have one input port
and one output port. This change allows us to query the number of MIDI
devices through USB instead of through an ALSA ioctl. USB MIDI should
now function correctly even with varying number of ports.

Bug: 28269065
Test: MIDI apps work for Alesis Recital and on M-Audio KeyStudio.
Android device also works as a peripheral to another Android device.

Change-Id: Id19748fbe1c8aa4913cf4925e0fec9316617409e
This commit is contained in:
Robert Wu
2021-09-09 23:22:18 +00:00
parent 4cffc46ad1
commit ce8791c18e
4 changed files with 174 additions and 139 deletions

View File

@@ -24,14 +24,14 @@
#include "android_runtime/AndroidRuntime.h"
#include "android_runtime/Log.h"
#include <stdio.h>
#include <errno.h>
#include <asm/byteorder.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sound/asound.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
namespace android
{
@@ -39,78 +39,62 @@ namespace android
static jclass sFileDescriptorClass;
static jfieldID sPipeFDField;
static jint
android_server_UsbMidiDevice_get_subdevice_count(JNIEnv *env, jobject /* thiz */,
jint card, jint device)
{
char path[100];
int fd;
const int kMaxRetries = 10;
const int kSleepMicroseconds = 2000;
snprintf(path, sizeof(path), "/dev/snd/controlC%d", card);
// This control device may not have been created yet. So we should
// try to open it several times to prevent intermittent failure
// from a race condition.
int retryCounter = 0;
while ((fd = open(path, O_RDWR)) < 0) {
if (++retryCounter > kMaxRetries) {
ALOGE("timed out after %d tries, could not open %s", retryCounter, path);
return 0;
} else {
ALOGW("attempt #%d, could not open %s", retryCounter, path);
// Increase the sleep interval each time.
// 10 retries will total 2 * sum(1..10) = 110 milliseconds.
// Typically the device should be ready in 5-10 milliseconds.
usleep(kSleepMicroseconds * retryCounter);
}
}
struct snd_rawmidi_info info;
memset(&info, 0, sizeof(info));
info.device = device;
int ret = ioctl(fd, SNDRV_CTL_IOCTL_RAWMIDI_INFO, &info);
close(fd);
if (ret < 0) {
ALOGE("SNDRV_CTL_IOCTL_RAWMIDI_INFO failed, errno: %d path: %s", errno, path);
return -1;
}
ALOGD("subdevices_count: %d", info.subdevices_count);
return info.subdevices_count;
}
static jobjectArray
android_server_UsbMidiDevice_open(JNIEnv *env, jobject thiz, jint card, jint device,
jint subdevice_count)
{
// This function returns an array of integers, each representing a file descriptor.
// The will be in the order of inputs then outputs.
// The last input fd will be for a file descriptor that simply allows Os.poll() to keep working.
// For example, if numInputs is 2 and numOutputs is 1, the resulting fds are as follows:
// 1. Input O_RDONLY file descriptor
// 2. Special input file descriptor to block the input thread
// 3. Output O_WRONLY file descriptor
static jobjectArray android_server_UsbMidiDevice_open(JNIEnv *env, jobject thiz, jint card,
jint device, jint numInputs,
jint numOutputs) {
char path[100];
int fd;
snprintf(path, sizeof(path), "/dev/snd/midiC%dD%d", card, device);
// allocate one extra file descriptor for close pipe
jobjectArray fds = env->NewObjectArray(subdevice_count + 1, sFileDescriptorClass, NULL);
ALOGD("Opening %d inputs and %d outputs", numInputs, numOutputs);
jobjectArray fds = env->NewObjectArray(numInputs + numOutputs, sFileDescriptorClass, NULL);
if (!fds) {
return NULL;
}
// to support multiple subdevices we open the same file multiple times
for (int i = 0; i < subdevice_count; i++) {
int fd = open(path, O_RDWR);
// open the path for the read pipes. The last one is special and used to
// unblock Os.poll()
for (int i = 0; i < numInputs - 1; i++) {
fd = open(path, O_RDONLY);
if (fd < 0) {
ALOGE("open failed on %s for index %d", path, i);
goto release_fds;
}
ScopedLocalRef<jobject> jifd(env, jniCreateFileDescriptor(env, fd));
if (jifd.get() == NULL) {
close(fd);
goto release_fds;
}
env->SetObjectArrayElement(fds, i, jifd.get());
}
// create a pipe to use for unblocking our input thread
{
// open the path for the write pipes
for (int i = 0; i < numOutputs; i++) {
fd = open(path, O_WRONLY);
if (fd < 0) {
ALOGE("open failed on %s for index %d", path, i);
goto release_fds;
}
ScopedLocalRef<jobject> jifd(env, jniCreateFileDescriptor(env, fd));
if (jifd.get() == NULL) {
close(fd);
goto release_fds;
}
env->SetObjectArrayElement(fds, i + numInputs, jifd.get());
}
// create a pipe to use for unblocking our input thread. The caller should
// set numInputs as 0 when there are zero real input threads.
if (numInputs > 0) {
int pipeFD[2];
if (pipe(pipeFD) == -1) {
ALOGE("pipe() failed, errno = %d", errno);
@@ -123,20 +107,21 @@ android_server_UsbMidiDevice_open(JNIEnv *env, jobject thiz, jint card, jint dev
close(pipeFD[1]);
goto release_fds;
}
env->SetObjectArrayElement(fds, subdevice_count, jifd.get());
// store as last input file descriptor
env->SetObjectArrayElement(fds, numInputs - 1, jifd.get());
// store our end of the pipe in mPipeFD
env->SetIntField(thiz, sPipeFDField, pipeFD[1]);
}
return fds;
release_fds:
for (int i = 0; i < subdevice_count + 1; ++i) {
for (int i = 0; i < numInputs + numOutputs; ++i) {
ScopedLocalRef<jobject> jifd(env, env->GetObjectArrayElement(fds, i));
if (jifd.get() == NULL) {
break;
if (jifd.get() != NULL) {
int fd = jniGetFDFromFileDescriptor(env, jifd.get());
close(fd);
}
int fd = jniGetFDFromFileDescriptor(env, jifd.get());
close(fd);
}
return NULL;
}
@@ -158,9 +143,9 @@ android_server_UsbMidiDevice_close(JNIEnv *env, jobject thiz, jobjectArray fds)
}
static JNINativeMethod method_table[] = {
{ "nativeGetSubdeviceCount", "(II)I", (void*)android_server_UsbMidiDevice_get_subdevice_count },
{ "nativeOpen", "(III)[Ljava/io/FileDescriptor;", (void*)android_server_UsbMidiDevice_open },
{ "nativeClose", "([Ljava/io/FileDescriptor;)V", (void*)android_server_UsbMidiDevice_close },
{"nativeOpen", "(IIII)[Ljava/io/FileDescriptor;",
(void *)android_server_UsbMidiDevice_open},
{"nativeClose", "([Ljava/io/FileDescriptor;)V", (void *)android_server_UsbMidiDevice_close},
};
int register_android_server_UsbMidiDevice(JNIEnv *env)

View File

@@ -257,6 +257,8 @@ public final class UsbAlsaManager {
// look for MIDI devices
boolean hasMidi = parser.hasMIDIInterface();
int midiNumInputs = parser.calculateNumMidiInputs();
int midiNumOutputs = parser.calculateNumMidiOutputs();
if (DEBUG) {
Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature);
}
@@ -285,7 +287,7 @@ public final class UsbAlsaManager {
properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
cardRec.getCardNum(), 0 /*device*/);
cardRec.getCardNum(), 0 /*device*/, midiNumInputs, midiNumOutputs);
if (usbMidiDevice != null) {
mMidiDevices.put(deviceAddress, usbMidiDevice);
}
@@ -338,7 +340,8 @@ public final class UsbAlsaManager {
com.android.internal.R.string.usb_midi_peripheral_product_name));
properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card);
properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device);
mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device);
mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device,
1 /* numInputs */, 1 /* numOutputs */);
} else if (!enabled && mPeripheralMidiDevice != null) {
IoUtils.closeQuietly(mPeripheralMidiDevice);
mPeripheralMidiDevice = null;

View File

@@ -48,8 +48,10 @@ public final class UsbMidiDevice implements Closeable {
private final int mAlsaCard;
private final int mAlsaDevice;
private final int mSubdeviceCount;
private final InputReceiverProxy[] mInputPortReceivers;
// USB outputs are MIDI inputs
private final InputReceiverProxy[] mMidiInputPortReceivers;
private final int mNumInputs;
private final int mNumOutputs;
private MidiDeviceServer mServer;
@@ -139,15 +141,14 @@ public final class UsbMidiDevice implements Closeable {
}
}
public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) {
// FIXME - support devices with different number of input and output ports
int subDeviceCount = nativeGetSubdeviceCount(card, device);
if (subDeviceCount <= 0) {
Log.e(TAG, "nativeGetSubdeviceCount failed");
return null;
}
UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, subDeviceCount);
/**
* Creates an UsbMidiDevice based on the input parameters. Read/Write streams
* will be created individually as some devices don't have the same number of
* inputs and outputs.
*/
public static UsbMidiDevice create(Context context, Bundle properties, int card,
int device, int numInputs, int numOutputs) {
UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, numInputs, numOutputs);
if (!midiDevice.register(context, properties)) {
IoUtils.closeQuietly(midiDevice);
Log.e(TAG, "createDeviceServer failed");
@@ -156,100 +157,107 @@ public final class UsbMidiDevice implements Closeable {
return midiDevice;
}
private UsbMidiDevice(int card, int device, int subdeviceCount) {
private UsbMidiDevice(int card, int device, int numInputs, int numOutputs) {
mAlsaCard = card;
mAlsaDevice = device;
mSubdeviceCount = subdeviceCount;
mNumInputs = numInputs;
mNumOutputs = numOutputs;
// FIXME - support devices with different number of input and output ports
int inputPortCount = subdeviceCount;
mInputPortReceivers = new InputReceiverProxy[inputPortCount];
for (int port = 0; port < inputPortCount; port++) {
mInputPortReceivers[port] = new InputReceiverProxy();
// Create MIDI port receivers based on the number of output ports. The
// output of USB is the input of MIDI.
mMidiInputPortReceivers = new InputReceiverProxy[numOutputs];
for (int port = 0; port < numOutputs; port++) {
mMidiInputPortReceivers[port] = new InputReceiverProxy();
}
}
private boolean openLocked() {
// FIXME - support devices with different number of input and output ports
FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice, mSubdeviceCount);
int inputStreamCount = mNumInputs;
// Create an extra stream for unblocking Os.poll()
if (inputStreamCount > 0) {
inputStreamCount++;
}
int outputStreamCount = mNumOutputs;
// The resulting file descriptors will be O_RDONLY following by O_WRONLY
FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice,
inputStreamCount, outputStreamCount);
if (fileDescriptors == null) {
Log.e(TAG, "nativeOpen failed");
return false;
}
mFileDescriptors = fileDescriptors;
int inputStreamCount = fileDescriptors.length;
// last file descriptor returned from nativeOpen() is only used for unblocking Os.poll()
// in our input thread
int outputStreamCount = fileDescriptors.length - 1;
mPollFDs = new StructPollfd[inputStreamCount];
mInputStreams = new FileInputStream[inputStreamCount];
for (int i = 0; i < inputStreamCount; i++) {
FileDescriptor fd = fileDescriptors[i];
StructPollfd pollfd = new StructPollfd();
pollfd.fd = fd;
pollfd.events = (short)OsConstants.POLLIN;
pollfd.events = (short) OsConstants.POLLIN;
mPollFDs[i] = pollfd;
mInputStreams[i] = new FileInputStream(fd);
}
mOutputStreams = new FileOutputStream[outputStreamCount];
mEventSchedulers = new MidiEventScheduler[outputStreamCount];
for (int i = 0; i < outputStreamCount; i++) {
mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]);
int curOutputStream = 0;
for (int i = 0; i < outputStreamCount; i++) {
mOutputStreams[i] = new FileOutputStream(fileDescriptors[inputStreamCount + i]);
MidiEventScheduler scheduler = new MidiEventScheduler();
mEventSchedulers[i] = scheduler;
mInputPortReceivers[i].setReceiver(scheduler.getReceiver());
mMidiInputPortReceivers[i].setReceiver(scheduler.getReceiver());
}
final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
// Create input thread which will read from all output ports of the physical device
new Thread("UsbMidiDevice input thread") {
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
try {
while (true) {
// Record time of event immediately after waking.
long timestamp = System.nanoTime();
synchronized (mLock) {
if (!mIsOpen) break;
if (inputStreamCount > 0) {
// Create input thread which will read from all output ports of the physical device
new Thread("UsbMidiDevice input thread") {
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
try {
while (true) {
// Record time of event immediately after waking.
long timestamp = System.nanoTime();
synchronized (mLock) {
if (!mIsOpen) break;
// look for a readable FileDescriptor
for (int index = 0; index < mPollFDs.length; index++) {
StructPollfd pfd = mPollFDs[index];
if ((pfd.revents & (OsConstants.POLLERR
| OsConstants.POLLHUP)) != 0) {
break;
} else if ((pfd.revents & OsConstants.POLLIN) != 0) {
// clear readable flag
pfd.revents = 0;
if (index == mInputStreams.length - 1) {
// last file descriptor is used only for unblocking Os.poll()
// look for a readable FileDescriptor
for (int index = 0; index < mPollFDs.length; index++) {
StructPollfd pfd = mPollFDs[index];
if ((pfd.revents & (OsConstants.POLLERR
| OsConstants.POLLHUP)) != 0) {
break;
}
} else if ((pfd.revents & OsConstants.POLLIN) != 0) {
// clear readable flag
pfd.revents = 0;
if (index == mInputStreams.length - 1) {
// last fd is used only for unblocking Os.poll()
break;
}
int count = mInputStreams[index].read(buffer);
outputReceivers[index].send(buffer, 0, count, timestamp);
int count = mInputStreams[index].read(buffer);
outputReceivers[index].send(buffer, 0, count, timestamp);
}
}
}
}
// wait until we have a readable port or we are signalled to close
Os.poll(mPollFDs, -1 /* infinite timeout */);
}
} catch (IOException e) {
Log.d(TAG, "reader thread exiting");
} catch (ErrnoException e) {
Log.d(TAG, "reader thread exiting");
// wait until we have a readable port or we are signalled to close
Os.poll(mPollFDs, -1 /* infinite timeout */);
}
} catch (IOException e) {
Log.d(TAG, "reader thread exiting");
} catch (ErrnoException e) {
Log.d(TAG, "reader thread exiting");
}
Log.d(TAG, "input thread exit");
}
Log.d(TAG, "input thread exit");
}
}.start();
}.start();
}
// Create output thread for each input port of the physical device
for (int port = 0; port < outputStreamCount; port++) {
@@ -294,7 +302,7 @@ public final class UsbMidiDevice implements Closeable {
return false;
}
mServer = midiManager.createDeviceServer(mInputPortReceivers, mSubdeviceCount,
mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs,
null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback);
if (mServer == null) {
return false;
@@ -318,7 +326,7 @@ public final class UsbMidiDevice implements Closeable {
private void closeLocked() {
for (int i = 0; i < mEventSchedulers.length; i++) {
mInputPortReceivers[i].setReceiver(null);
mMidiInputPortReceivers[i].setReceiver(null);
mEventSchedulers[i].close();
}
mEventSchedulers = null;
@@ -354,7 +362,7 @@ public final class UsbMidiDevice implements Closeable {
dump.end(token);
}
private static native int nativeGetSubdeviceCount(int card, int device);
private native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount);
private native FileDescriptor[] nativeOpen(int card, int device, int numInputs,
int numOutputs);
private native void nativeClose(FileDescriptor[] fileDescriptors);
}

View File

@@ -656,6 +656,45 @@ public final class UsbDescriptorParser {
return false;
}
private int calculateNumMidiPorts(boolean isOutput) {
int count = 0;
ArrayList<UsbDescriptor> descriptors =
getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO);
for (UsbDescriptor descriptor : descriptors) {
// ensure that this isn't an unrecognized interface descriptor
if (descriptor instanceof UsbInterfaceDescriptor) {
UsbInterfaceDescriptor interfaceDescr = (UsbInterfaceDescriptor) descriptor;
if (interfaceDescr.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) {
for (int i = 0; i < interfaceDescr.getNumEndpoints(); i++) {
UsbEndpointDescriptor endpoint = interfaceDescr.getEndpointDescriptor(i);
// 0 is output, 1 << 7 is input.
if ((endpoint.getDirection() == 0) == isOutput) {
count++;
}
}
}
} else {
Log.w(TAG, "Undefined Audio Class Interface l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
return count;
}
/**
* @hide
*/
public int calculateNumMidiInputs() {
return calculateNumMidiPorts(false /*isOutput*/);
}
/**
* @hide
*/
public int calculateNumMidiOutputs() {
return calculateNumMidiPorts(true /*isOutput*/);
}
/**
* @hide
*/