More robust USB MIDI keyboard handling

This patch implements better USB MIDI handling, including listening for
detach, and handling the case where it's connected while the activity is
active.
master
Raph Levien 11 years ago
parent 5a20308469
commit 9746d9626e
  1. 3
      android/AndroidManifest.xml
  2. 143
      android/src/com/levien/synthesizer/android/ui/PianoActivity2.java
  3. 93
      android/src/com/levien/synthesizer/android/usb/UsbMidiDevice.java

@ -14,7 +14,8 @@
<activity <activity
android:name="com.levien.synthesizer.android.ui.PianoActivity2" android:name="com.levien.synthesizer.android.ui.PianoActivity2"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="landscape"> android:screenOrientation="landscape"
android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

@ -23,11 +23,12 @@ import java.util.HashMap;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.hardware.usb.UsbConstants; import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager; import android.hardware.usb.UsbManager;
import android.media.AudioManager; import android.media.AudioManager;
@ -48,6 +49,7 @@ import android.widget.TextView;
import com.levien.synthesizer.R; import com.levien.synthesizer.R;
import com.levien.synthesizer.android.AndroidGlue; import com.levien.synthesizer.android.AndroidGlue;
import com.levien.synthesizer.android.stats.JitterStats; import com.levien.synthesizer.android.stats.JitterStats;
import com.levien.synthesizer.android.usb.UsbMidiDevice;
import com.levien.synthesizer.android.widgets.knob.KnobListener; import com.levien.synthesizer.android.widgets.knob.KnobListener;
import com.levien.synthesizer.android.widgets.knob.KnobView; import com.levien.synthesizer.android.widgets.knob.KnobView;
import com.levien.synthesizer.android.widgets.piano.PianoView; import com.levien.synthesizer.android.widgets.piano.PianoView;
@ -60,6 +62,7 @@ import com.levien.synthesizer.android.widgets.piano.PianoView;
public class PianoActivity2 extends Activity { public class PianoActivity2 extends Activity {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Log.d("synth", "activity onCreate");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@ -125,9 +128,8 @@ public class PianoActivity2 extends Activity {
}); });
piano_.bindTo(androidGlue_); piano_.bindTo(androidGlue_);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { // This is now done in onResume
tryConnectUsb(); //tryConnectUsb();
}
final boolean doStats = false; final boolean doStats = false;
@ -178,94 +180,129 @@ public class PianoActivity2 extends Activity {
@Override @Override
protected void onDestroy() { protected void onDestroy() {
Log.d("synth", "activity onDestroy");
androidGlue_.shutdown(); androidGlue_.shutdown();
super.onDestroy(); super.onDestroy();
} }
@Override @Override
protected void onPause() { protected void onPause() {
Log.d("synth", "activity onPause");
androidGlue_.setPlayState(false); androidGlue_.setPlayState(false);
unregisterReceiver(usbReceiver_);
setMidiInterface(null, null);
super.onPause(); super.onPause();
} }
@Override @Override
protected void onResume() { protected void onResume() {
Log.d("synth", "activity onResume");
androidGlue_.setPlayState(true); androidGlue_.setPlayState(true);
tryConnectUsb();
super.onResume(); super.onResume();
} }
@Override
protected void onNewIntent(Intent intent) {
Log.d("synth", "activity onNewIntent " + intent);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private UsbEndpoint getInputEndpoint(UsbInterface usbIf) { static private UsbInterface findMidiInterface(UsbDevice device) {
int nEndpoints = usbIf.getEndpointCount(); int count = device.getInterfaceCount();
for (int i = 0; i < nEndpoints; i++) { for (int i = 0; i < count; i++) {
UsbEndpoint endpoint = usbIf.getEndpoint(i); UsbInterface usbIf = device.getInterface(i);
if (endpoint.getDirection() == UsbConstants.USB_DIR_IN && if (usbIf.getInterfaceClass() == 1 && usbIf.getInterfaceSubclass() == 3) {
endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { return usbIf;
return endpoint;
} }
} }
return null; return null;
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private void startUsbThread(final UsbDeviceConnection connection, final UsbEndpoint endpoint) { private boolean setMidiInterface(UsbDevice device, UsbInterface intf) {
Thread thread = new Thread(new Runnable() { if (usbMidiConnection_ != null) {
public void run() { if (usbMidiDevice_ != null) {
byte[] buf = new byte[endpoint.getMaxPacketSize()]; usbMidiDevice_.stop();
while (true) { usbMidiDevice_ = null;
int nBytes = connection.bulkTransfer(endpoint, buf, buf.length, 10000);
for (int i = 0; i < nBytes; i += 4) {
int codeIndexNumber = buf[i] & 0xf;
int payloadBytes = 0;
if (codeIndexNumber == 8 || codeIndexNumber == 9 || codeIndexNumber == 11 ||
codeIndexNumber == 14) {
payloadBytes = 3;
} else if (codeIndexNumber == 12) {
payloadBytes = 2;
} }
if (payloadBytes > 0) { if (usbMidiInterface_ != null) {
byte[] newBuf = new byte[payloadBytes]; usbMidiConnection_.releaseInterface(usbMidiInterface_);
System.arraycopy(buf, i + 1, newBuf, 0, payloadBytes);
androidGlue_.onMessage(newBuf);
} }
usbMidiConnection_.close();
usbMidiDeviceName_ = null;
usbMidiConnection_ = null;
} }
if (device != null && intf != null) {
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection != null) {
if (connection.claimInterface(intf, true)) {
usbMidiDeviceName_ = device.getDeviceName();
usbMidiConnection_ = connection;
usbMidiInterface_ = intf;
usbMidiDevice_ = new UsbMidiDevice(this, usbMidiConnection_, intf);
usbMidiDevice_.start();
return true;
} else {
Log.e("synth", "failed to claim USB interface");
connection.close();
} }
} else {
Log.e("synth", "open device failed");
} }
});
thread.start();
} }
return false;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private void tryConnectUsb() { private void tryConnectUsb() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) {
return;
}
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
TextView label = (TextView)findViewById(R.id.status);
Log.i("synth", "USB device count=" + deviceList.size()); Log.i("synth", "USB device count=" + deviceList.size());
for (UsbDevice device : deviceList.values()) { for (UsbDevice device : deviceList.values()) {
//label.setText("ic:" + device.getInterfaceCount());
Log.i("synth", "usb name=" + device.toString() + " #if=" + device.getInterfaceCount()); Log.i("synth", "usb name=" + device.toString() + " #if=" + device.getInterfaceCount());
if (device.getInterfaceCount() < 2) { UsbInterface usbIf = findMidiInterface(device);
continue; if (setMidiInterface(device, usbIf)) {
break;
} }
UsbInterface usbIf = device.getInterface(1);
if (usbIf.getInterfaceClass() != 1 || usbIf.getInterfaceSubclass() != 3) {
continue;
} }
UsbDeviceConnection connection = usbManager.openDevice(device); IntentFilter filter = new IntentFilter();
if (connection != null) { // Attach intent is handled in manifest, don't want to get it twice.
connection.claimInterface(usbIf, true); //filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
UsbEndpoint endpoint = getInputEndpoint(usbIf); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
//label.setText(endpoint.toString()); registerReceiver(usbReceiver_, filter);
Log.i("synth", "MIDI keyboard detected, starting USB thread");
startUsbThread(connection, endpoint);
} else {
if (label != null) {
Log.i("synth", "error opening USB MIDI device");
label.setText("error opening device");
} }
BroadcastReceiver usbReceiver_ = new BroadcastReceiver() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
Log.i("synth", "broadcast receiver: attach");
UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
UsbInterface intf = findMidiInterface(device);
if (intf != null) {
setMidiInterface(device, intf);
}
} else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
Log.i("synth", "broadcast receiver: detach");
UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
String deviceName = device.getDeviceName();
if (usbMidiDeviceName_ != null && usbMidiDeviceName_.equals(deviceName)) {
setMidiInterface(null, null);
} }
break;
} }
} }
};
public void sendMidiBytes(byte[] buf) {
// TODO: in future we'll want to reflect MIDI to UI (knobs turn, keys press)
androidGlue_.sendMidi(buf);
}
class AudioParams { class AudioParams {
AudioParams(int sr, int bs) { AudioParams(int sr, int bs) {
@ -301,4 +338,8 @@ public class PianoActivity2 extends Activity {
private Handler statusHandler_; private Handler statusHandler_;
private Runnable statusRunnable_; private Runnable statusRunnable_;
private JitterStats jitterStats_; private JitterStats jitterStats_;
private UsbDeviceConnection usbMidiConnection_;
private UsbMidiDevice usbMidiDevice_;
private UsbInterface usbMidiInterface_;
private String usbMidiDeviceName_;
} }

@ -0,0 +1,93 @@
// TODO copyright
// Class representing a USB MIDI keyboard
package com.levien.synthesizer.android.usb;
import android.annotation.TargetApi;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.os.Build;
import android.util.Log;
import com.levien.synthesizer.android.ui.PianoActivity2;
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public class UsbMidiDevice {
private final PianoActivity2 mActivity;
private final UsbDeviceConnection mDeviceConnection;
private final UsbEndpoint mEndpoint;
private final WaiterThread mWaiterThread = new WaiterThread();
public UsbMidiDevice(PianoActivity2 activity, UsbDeviceConnection connection, UsbInterface intf) {
mActivity = activity;
mDeviceConnection = connection;
mEndpoint = getInputEndpoint(intf);
}
private UsbEndpoint getInputEndpoint(UsbInterface usbIf) {
Log.d("synth", "interface:" + usbIf.toString());
int nEndpoints = usbIf.getEndpointCount();
for (int i = 0; i < nEndpoints; i++) {
UsbEndpoint endpoint = usbIf.getEndpoint(i);
if (endpoint.getDirection() == UsbConstants.USB_DIR_IN &&
endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
return endpoint;
}
}
return null;
}
public void start() {
Log.d("synth", "midi USB waiter thread starting");
mWaiterThread.start();
}
public void stop() {
synchronized (mWaiterThread) {
mWaiterThread.mStop = true;
}
}
private class WaiterThread extends Thread {
public boolean mStop;
public void run() {
byte[] buf = new byte[mEndpoint.getMaxPacketSize()];
while (true) {
synchronized (this) {
if (mStop) {
Log.d("synth", "midi USB waiter thread shutting down");
return;
}
}
final int TIMEOUT = 1000;
int nBytes = mDeviceConnection.bulkTransfer(mEndpoint, buf, buf.length, TIMEOUT);
if (nBytes < 0) {
Log.e("synth", "bulkTransfer error " + nBytes);
// break;
}
for (int i = 0; i < nBytes; i += 4) {
int codeIndexNumber = buf[i] & 0xf;
int payloadBytes = 0;
if (codeIndexNumber == 8 || codeIndexNumber == 9 || codeIndexNumber == 11 ||
codeIndexNumber == 14) {
payloadBytes = 3;
} else if (codeIndexNumber == 12) {
payloadBytes = 2;
}
if (payloadBytes > 0) {
byte[] newBuf = new byte[payloadBytes];
System.arraycopy(buf, i + 1, newBuf, 0, payloadBytes);
Log.d("synth", "sending midi");
mActivity.sendMidiBytes(newBuf);
}
}
}
}
}
}
Loading…
Cancel
Save