From ce8791c18e278f31abc343292d9bba6059efa9ec Mon Sep 17 00:00:00 2001 From: Robert Wu Date: Thu, 9 Sep 2021 23:22:18 +0000 Subject: [PATCH] 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 --- .../jni/com_android_server_UsbMidiDevice.cpp | 119 ++++++-------- .../android/server/usb/UsbAlsaManager.java | 7 +- .../com/android/server/usb/UsbMidiDevice.java | 148 +++++++++--------- .../usb/descriptors/UsbDescriptorParser.java | 39 +++++ 4 files changed, 174 insertions(+), 139 deletions(-) diff --git a/services/core/jni/com_android_server_UsbMidiDevice.cpp b/services/core/jni/com_android_server_UsbMidiDevice.cpp index d6b5bed173ebe..c8e769878b8c5 100644 --- a/services/core/jni/com_android_server_UsbMidiDevice.cpp +++ b/services/core/jni/com_android_server_UsbMidiDevice.cpp @@ -24,14 +24,14 @@ #include "android_runtime/AndroidRuntime.h" #include "android_runtime/Log.h" -#include -#include #include -#include -#include +#include #include -#include #include +#include +#include +#include +#include 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 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 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 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) diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java index 22629dd526088..0aa62c53e269f 100644 --- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java +++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java @@ -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; diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java index f47636ed8ea4a..b61e93b36e58f 100644 --- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java @@ -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); } diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java index 43d5bf323abbd..7250a071835d8 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java @@ -656,6 +656,45 @@ public final class UsbDescriptorParser { return false; } + private int calculateNumMidiPorts(boolean isOutput) { + int count = 0; + ArrayList 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 */