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

This commit is contained in:
TreeHugger Robot
2021-09-28 18:49:52 +00:00
committed by Android (Google) Code Review
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
*/