Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ce8791c1 authored by Robert Wu's avatar Robert Wu
Browse files

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
parent 4cffc46a
Loading
Loading
Loading
Loading
+52 −67
Original line number Diff line number Diff line
@@ -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)
{
// 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;
    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)
{
    char    path[100];

    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,21 +107,22 @@ 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);
        }
    }
    return NULL;
}

@@ -158,8 +143,8 @@ 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 },
        {"nativeOpen", "(IIII)[Ljava/io/FileDescriptor;",
         (void *)android_server_UsbMidiDevice_open},
        {"nativeClose", "([Ljava/io/FileDescriptor;)V", (void *)android_server_UsbMidiDevice_close},
};

+5 −2
Original line number Diff line number Diff line
@@ -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;
+80 −72
Original line number Diff line number Diff line
@@ -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,35 +157,40 @@ 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();
@@ -196,16 +202,18 @@ public final class UsbMidiDevice implements Closeable {

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

        if (inputStreamCount > 0) {
            // Create input thread which will read from all output ports of the physical device
            new Thread("UsbMidiDevice input thread") {
                @Override
@@ -227,9 +235,8 @@ public final class UsbMidiDevice implements Closeable {
                                    } 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()
                                            // last fd is used only for unblocking Os.poll()
                                            break;
                                        }

@@ -250,6 +257,7 @@ public final class UsbMidiDevice implements Closeable {
                    Log.d(TAG, "input thread exit");
                }
            }.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);
}
+39 −0
Original line number Diff line number Diff line
@@ -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
     */