From 6ad1d8ccbfad3d8a9a0273ac6eaef4cf84d1fb45 Mon Sep 17 00:00:00 2001 From: Robert Wu Date: Thu, 5 Jan 2023 19:04:36 +0000 Subject: [PATCH] Use ALSA for USB MIDI 1.0 devices In Android T, we added USB MIDI 2.0 support. As part of the change, we changed USB MIDI 1.0 devices to also not use ALSA. Users have reported bugs where audio stops working after MIDI is used. The root cause of this is because ALSA disconnects the sound card if one part of the sound card is removed. Android disconnects the ALSA MIDI interface as a direct USB connection is made. As long as ALSA is used for audio, ALSA needs to be used for MIDI. Thus, Android will go back to using ALSA for MIDI 1.0 USB devices. Refer to ag/16835029 for the change where ALSA is no longer used for host mode. This change is a partial revert of it. Bug: 251243033 Bug: 259365701 Test: MIDI Scope on Korg device Test: MIDI Keyboard and Scope loopback on 2x2 MIDI interface Test: MIDI Scope on MIDI 2.0 device Test: OboeTester Tap to Tone with MIDI and Audio Change-Id: Ia6a6021619e05dc5b483d4644d7fd152d89e1d46 (cherry picked from commit 768236ceecc2d81ea82671b8dc6c2031f356e391) --- .../android/server/usb/UsbAlsaManager.java | 70 ++++++++++++++++ .../android/server/usb/UsbHostManager.java | 21 +++-- .../usb/descriptors/UsbConfigDescriptor.java | 4 + .../usb/descriptors/UsbDescriptorParser.java | 84 ++++++++++++++++++- .../descriptors/UsbEndpointDescriptor.java | 10 +++ 5 files changed, 180 insertions(+), 9 deletions(-) diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java index 42a5af7fdce36..17c354a8a80e8 100644 --- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java +++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java @@ -36,6 +36,7 @@ import libcore.io.IoUtils; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -106,6 +107,12 @@ public final class UsbAlsaManager { return false; } + /** + * List of connected MIDI devices + */ + private final HashMap + mMidiDevices = new HashMap(); + // UsbMidiDevice for USB peripheral mode (gadget) device private UsbMidiDevice mPeripheralMidiDevice = null; @@ -249,6 +256,8 @@ public final class UsbAlsaManager { } } + addMidiDevice(deviceAddress, usbDevice, parser, cardRec); + logDevices("deviceAdded()"); if (DEBUG) { @@ -256,6 +265,54 @@ public final class UsbAlsaManager { } } + private void addMidiDevice(String deviceAddress, UsbDevice usbDevice, + UsbDescriptorParser parser, AlsaCardsParser.AlsaCardRecord cardRec) { + boolean hasMidi = parser.hasMIDIInterface(); + // UsbHostManager will create UsbDirectMidiDevices instead if MIDI 2 is supported. + boolean hasMidi2 = parser.containsUniversalMidiDeviceEndpoint(); + if (DEBUG) { + Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature); + Slog.d(TAG, "hasMidi2: " + hasMidi2); + } + if (mHasMidiFeature && hasMidi && !hasMidi2) { + Bundle properties = new Bundle(); + String manufacturer = usbDevice.getManufacturerName(); + String product = usbDevice.getProductName(); + String version = usbDevice.getVersion(); + String name; + if (manufacturer == null || manufacturer.isEmpty()) { + name = product; + } else if (product == null || product.isEmpty()) { + name = manufacturer; + } else { + name = manufacturer + " " + product; + } + properties.putString(MidiDeviceInfo.PROPERTY_NAME, name); + properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer); + properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product); + properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version); + properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER, + usbDevice.getSerialNumber()); + properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, cardRec.getCardNum()); + properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, 0 /*deviceNum*/); + properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice); + + int numLegacyMidiInputs = parser.calculateNumLegacyMidiInputs(); + int numLegacyMidiOutputs = parser.calculateNumLegacyMidiOutputs(); + if (DEBUG) { + Slog.d(TAG, "numLegacyMidiInputs: " + numLegacyMidiInputs); + Slog.d(TAG, "numLegacyMidiOutputs:" + numLegacyMidiOutputs); + } + + UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties, + cardRec.getCardNum(), 0 /*device*/, numLegacyMidiInputs, + numLegacyMidiOutputs); + if (usbMidiDevice != null) { + mMidiDevices.put(deviceAddress, usbMidiDevice); + } + } + } + /* package */ synchronized void usbDeviceRemoved(String deviceAddress/*UsbDevice usbDevice*/) { if (DEBUG) { Slog.d(TAG, "deviceRemoved(" + deviceAddress + ")"); @@ -269,6 +326,13 @@ public final class UsbAlsaManager { selectDefaultDevice(); // if there any external devices left, select one of them } + // MIDI + UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress); + if (usbMidiDevice != null) { + Slog.i(TAG, "USB MIDI Device Removed: " + deviceAddress); + IoUtils.closeQuietly(usbMidiDevice); + } + logDevices("usbDeviceRemoved()"); } @@ -324,6 +388,12 @@ public final class UsbAlsaManager { usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES); } + for (String deviceAddr : mMidiDevices.keySet()) { + // A UsbMidiDevice does not have a handle to the UsbDevice anymore + mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices", + UsbAlsaManagerProto.MIDI_DEVICES); + } + dump.end(token); } diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java index f3892768921ce..b3eb285527962 100644 --- a/services/usb/java/com/android/server/usb/UsbHostManager.java +++ b/services/usb/java/com/android/server/usb/UsbHostManager.java @@ -444,14 +444,19 @@ public class UsbHostManager { } else { Slog.e(TAG, "Universal Midi Device is null."); } - } - if (parser.containsLegacyMidiDeviceEndpoint()) { - UsbDirectMidiDevice midiDevice = UsbDirectMidiDevice.create(mContext, - newDevice, parser, false, uniqueUsbDeviceIdentifier); - if (midiDevice != null) { - midiDevices.add(midiDevice); - } else { - Slog.e(TAG, "Legacy Midi Device is null."); + + // Use UsbDirectMidiDevice only if this supports MIDI 2.0 as well. + // ALSA removes the audio sound card if MIDI interfaces are removed. + // This means that as long as ALSA is used for audio, MIDI 1.0 USB + // devices should use the ALSA path for MIDI. + if (parser.containsLegacyMidiDeviceEndpoint()) { + midiDevice = UsbDirectMidiDevice.create(mContext, + newDevice, parser, false, uniqueUsbDeviceIdentifier); + if (midiDevice != null) { + midiDevices.add(midiDevice); + } else { + Slog.e(TAG, "Legacy Midi Device is null."); + } } } diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java index 3f2d8c8ef47c8..c6ea22856dc77 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java @@ -79,6 +79,10 @@ public final class UsbConfigDescriptor extends UsbDescriptor { mInterfaceDescriptors.add(interfaceDesc); } + ArrayList getInterfaceDescriptors() { + return mInterfaceDescriptors; + } + private boolean isAudioInterface(UsbInterfaceDescriptor descriptor) { return descriptor.getUsbClass() == UsbDescriptor.CLASSID_AUDIO && descriptor.getUsbSubclass() == UsbDescriptor.AUDIO_AUDIOSTREAMING; 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 cd6ea681db07a..626ce89271589 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java @@ -40,6 +40,7 @@ public final class UsbDescriptorParser { private UsbDeviceDescriptor mDeviceDescriptor; private UsbConfigDescriptor mCurConfigDescriptor; private UsbInterfaceDescriptor mCurInterfaceDescriptor; + private UsbEndpointDescriptor mCurEndpointDescriptor; // The AudioClass spec implemented by the AudioClass Interfaces // This may well be different than the overall USB Spec. @@ -165,7 +166,7 @@ public final class UsbDescriptorParser { break; case UsbDescriptor.DESCRIPTORTYPE_ENDPOINT: - descriptor = new UsbEndpointDescriptor(length, type); + descriptor = mCurEndpointDescriptor = new UsbEndpointDescriptor(length, type); if (mCurInterfaceDescriptor != null) { mCurInterfaceDescriptor.addEndpointDescriptor( (UsbEndpointDescriptor) descriptor); @@ -265,6 +266,9 @@ public final class UsbDescriptorParser { + Integer.toHexString(subClass)); break; } + if (mCurEndpointDescriptor != null && descriptor != null) { + mCurEndpointDescriptor.setClassSpecificEndpointDescriptor(descriptor); + } } break; @@ -795,6 +799,84 @@ public final class UsbDescriptorParser { return count; } + /** + * @hide + */ + private int calculateNumLegacyMidiPorts(boolean isOutput) { + // Only look at the first config. + UsbConfigDescriptor configDescriptor = null; + for (UsbDescriptor descriptor : mDescriptors) { + if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_CONFIG) { + if (descriptor instanceof UsbConfigDescriptor) { + configDescriptor = (UsbConfigDescriptor) descriptor; + break; + } else { + Log.w(TAG, "Unrecognized Config l: " + descriptor.getLength() + + " t:0x" + Integer.toHexString(descriptor.getType())); + } + } + } + if (configDescriptor == null) { + Log.w(TAG, "Config not found"); + return 0; + } + + ArrayList legacyMidiInterfaceDescriptors = + new ArrayList(); + for (UsbInterfaceDescriptor interfaceDescriptor + : configDescriptor.getInterfaceDescriptors()) { + if (interfaceDescriptor.getUsbClass() == UsbDescriptor.CLASSID_AUDIO) { + if (interfaceDescriptor.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) { + UsbDescriptor midiHeaderDescriptor = + interfaceDescriptor.getMidiHeaderInterfaceDescriptor(); + if (midiHeaderDescriptor != null) { + if (midiHeaderDescriptor instanceof UsbMSMidiHeader) { + UsbMSMidiHeader midiHeader = + (UsbMSMidiHeader) midiHeaderDescriptor; + if (midiHeader.getMidiStreamingClass() == MS_MIDI_1_0) { + legacyMidiInterfaceDescriptors.add(interfaceDescriptor); + } + } + } + } + } + } + + int count = 0; + for (UsbInterfaceDescriptor interfaceDescriptor : legacyMidiInterfaceDescriptors) { + for (int i = 0; i < interfaceDescriptor.getNumEndpoints(); i++) { + UsbEndpointDescriptor endpoint = + interfaceDescriptor.getEndpointDescriptor(i); + // 0 is output, 1 << 7 is input. + if ((endpoint.getDirection() == 0) == isOutput) { + UsbDescriptor classSpecificEndpointDescriptor = + endpoint.getClassSpecificEndpointDescriptor(); + if (classSpecificEndpointDescriptor != null + && (classSpecificEndpointDescriptor instanceof UsbACMidi10Endpoint)) { + UsbACMidi10Endpoint midiEndpoint = + (UsbACMidi10Endpoint) classSpecificEndpointDescriptor; + count += midiEndpoint.getNumJacks(); + } + } + } + } + return count; + } + + /** + * @hide + */ + public int calculateNumLegacyMidiInputs() { + return calculateNumLegacyMidiPorts(false /*isOutput*/); + } + + /** + * @hide + */ + public int calculateNumLegacyMidiOutputs() { + return calculateNumLegacyMidiPorts(true /*isOutput*/); + } + /** * @hide */ diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java index ab07ce7fdb7ab..1f448acac5e88 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java @@ -79,6 +79,8 @@ public class UsbEndpointDescriptor extends UsbDescriptor { private byte mRefresh; private byte mSyncAddress; + private UsbDescriptor mClassSpecificEndpointDescriptor; + public UsbEndpointDescriptor(int length, byte type) { super(length, type); mHierarchyLevel = 4; @@ -112,6 +114,14 @@ public class UsbEndpointDescriptor extends UsbDescriptor { return mEndpointAddress & UsbEndpointDescriptor.MASK_ENDPOINT_DIRECTION; } + void setClassSpecificEndpointDescriptor(UsbDescriptor descriptor) { + mClassSpecificEndpointDescriptor = descriptor; + } + + UsbDescriptor getClassSpecificEndpointDescriptor() { + return mClassSpecificEndpointDescriptor; + } + /** * Returns a UsbEndpoint that this UsbEndpointDescriptor is describing. */