Added in API level 23

android.media.midi

Provides classes for sending and receiving messages using the standard MIDI event protocol over USB, Bluetooth LE, and virtual (inter-app) transports.

Overview

The Android MIDI package allows users to:

  • Connect a MIDI keyboard to Android to play a synthesizer or drive music apps.
  • Connect alternative MIDI controllers to Android.
  • Drive external MIDI synths from Android.
  • Drive external peripherals, lights, show control, etc from Android.
  • Generate music dynamically from games or music creation apps.
  • Generate MIDI messages in one app and send them to a second app.
  • Use an Android device running in peripheral mode as a multitouch controller connected to a laptop.

The API features include:

  • Enumeration of currently available devices. Information includes name, vendor, capabilities, etc.
  • Provide notification when MIDI devices are plugged in or unplugged.
  • Support efficient transmission of single or multiple short 1-3 byte MIDI messages.
  • Support transmission of arbitrary length data for SysEx, etc.
  • Timestamps to avoid jitter.
  • Support creation of virtual MIDI devices that can be connected to other devices. An example might be a synthesizer app that can be controlled by a composing app.
  • Support direct connection or “patching” of devices for lower latency.

Transports Supported

The API is “transport agnostic”. But there are several transports currently supported:

  • USB
  • software routing
  • BTLE

Android MIDI Terminology

Terminology

A Device is a MIDI capable object that has zero or more InputPorts and OutputPorts.

An InputPort has 16 channels and can receive MIDI messages from an OutputPort or an app.

An OutputPort has 16 channels and can send MIDI messages to an InputPort or an app.

MidiService is a centralized process that keeps track of all devices and brokers communication between them.

MidiManager is a class that the application or a device manager calls to communicate with the MidiService.

Writing a MIDI Application

Declare Feature in Manifest

An app that requires the MIDI API should declare that in the AndroidManifest.xml file. Then the app will not appear in the Play Store for old devices that do not support the MIDI API.

 <uses-feature android:name="android.software.midi" android:required="true"/>
 

Check for Feature Support

An app can also check at run-time whether the MIDI feature is supported on a platform. This is particularly useful during development when you install apps directly on a device.

 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI)) {
     // do MIDI stuff
 }
 

The MidiManager

The primary class for accessing the MIDI package is through the MidiManager.

 MidiManager m = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
 

Get List of Already Plugged In Entities

When an app starts, it can get a list of all the available MIDI devices. This information can be presented to a user, allowing them to choose a device.

 MidiDeviceInfo[] infos = m.getDevices();
 

Notification of MIDI Devices HotPlug Events

The application can request notification when, for example, keyboards are plugged in or unplugged.

 m.registerDeviceCallback(new MidiManager.DeviceCallback() {
     public void onDeviceAdded( MidiDeviceInfo info ) {
       ...
     }
     public void onDeviceRemoved( MidiDeviceInfo info ) {
       ...
     }
   });
 

Device and Port Information

You can query the number of input and output ports.

 int numInputs = info.getInputPortCount();
 int numOutputs = info.getOutputPortCount();
 

Note that “input” and “output” directions reflect the point of view of the MIDI device itself, not your app. For example, to send MIDI notes to a synthesizer, open the synth's INPUT port. To receive notes from a keyboard, open the keyboard's OUTPUT port.

The MidiDeviceInfo has a bundle of properties.

 Bundle properties = info.getProperties();
 String manufacturer = properties
       .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER);
 

Other properties include PROPERTY_PRODUCT, PROPERTY_NAME, PROPERTY_SERIAL_NUMBER

You can get the names and types of the ports from a PortInfo object. The type will be either TYPE_INPUT or TYPE_OUTPUT.

 MidiDeviceInfo.PortInfo[] portInfos = info.getPorts();
 String portName = portInfos[0].getName();
 if (portInfos[0].getType() == MidiDeviceInfo.PortInfo.TYPE_INPUT) {
     ...
 }
 

Open a MIDI Device

To access a MIDI device you need to open it first. The open is asynchronous so you need to provide a callback for completion. You can specify an optional Handler if you want the callback to occur on a specific Thread.

 m.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
     @Override
     public void onDeviceOpened(MidiDevice device) {
         if (device == null) {
             Log.e(TAG, "could not open device " + info);
         } else {
             ...
         }
     }, new Handler(Looper.getMainLooper())
     );
 

Open a MIDI Input Port

If you want to send a message to a MIDI Device then you need to open an “input” port with exclusive access.

 MidiInputPort inputPort = device.openInputPort(index);
 

Send a NoteOn

MIDI messages are sent as byte arrays. Here we encode a NoteOn message.

 byte[] buffer = new byte[32];
 int numBytes = 0;
 int channel = 3; // MIDI channels 1-16 are encoded as 0-15.
 buffer[numBytes++] = (byte)(0x90 + (channel - 1)); // note on
 buffer[numBytes++] = (byte)60; // pitch is middle C
 buffer[numBytes++] = (byte)127; // max velocity
 int offset = 0;
 // post is non-blocking
 inputPort.send(buffer, offset, numBytes);
 

Sometimes it is convenient to send MIDI messages with a timestamp. By scheduling events in the future we can mask scheduling jitter. Android MIDI timestamps are based on the monotonic nanosecond system timer. This is consistent with the other audio and input timers.

Here we send a message with a timestamp 2 seconds in the future.

 final long NANOS_PER_SECOND = 1000000000L;
 long now = System.nanoTime();
 long future = now + (2 * NANOS_PER_SECOND);
 inputPort.send(buffer, offset, numBytes, future);
 

If you want to cancel events that you have scheduled in the future then call flush().

 inputPort.flush(); // discard events
 

If there were any MIDI NoteOff message left in the buffer then they will be discarded and you may get stuck notes. So we recommend sending “all notes off” after doing a flush.

Receive a Note

To receive MIDI data from a device you need to extend MidiReceiver. Then connect your receiver to an output port of the device.

 class MyReceiver extends MidiReceiver {
     public void onSend(byte[] data, int offset,
             int count, long timestamp) throws IOException {
         // parse MIDI or whatever
     }
 }
 MidiOutputPort outputPort = device.openOutputPort(index);
 outputPort.connect(new MyReceiver());
 

The data that arrives is not validated or aligned in any particular way. It is raw MIDI data and can contain multiple messages or partial messages. It might contain System Real-Time messages, which can be interleaved inside other messages.

Using MIDI Over Bluetooth LE

MIDI devices can be connected to Android using Bluetooth LE.

Before using the device, the app must scan for available BTLE devices and then allow the user to connect. See the Android developer website for an example program.

Request Location Permission for BTLE

Applications that scan for Bluetooth devices must request permission in the manifest file. This LOCATION permission is required because it may be possible to guess the location of an Android device by seeing which BTLE devices are nearby.

 <uses-permission android:name="android.permission.BLUETOOTH"/>
 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
 

Apps must also request location permission from the user at run-time. See the documentation for Activity.requestPermissions() for details and an example.

Scan for MIDI Devices

The app will only want to see MIDI devices and not mice or other non-MIDI devices. So construct a ScanFilter using the UUID for standard MIDI over BTLE.

 MIDI over BTLE UUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
 

Open a MIDI Bluetooth Device

See the documentation for android.bluetooth.le.BluetoothLeScanner.startScan() method for details. When the user selects a MIDI/BTLE device then you can open it using the MidiManager.

 m.openBluetoothDevice(bluetoothDevice, callback, handler);
 

Once the MIDI/BTLE device has been opened by one app then it will also become available to other apps using the MIDI device discovery calls described above.

Creating a MIDI Virtual Device Service

An app can provide a MIDI Service that can be used by other apps. For example, an app can provide a custom synthesizer that other apps can send messages to. The service must be guarded with permission "android.permission.BIND_MIDI_DEVICE_SERVICE".

Manifest Files

An app declares that it will function as a MIDI server in the AndroidManifest.xml file.

 <service android:name="MySynthDeviceService"
   android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
   <intent-filter>
     <action android:name="android.media.midi.MidiDeviceService" />
   </intent-filter>
   <meta-data android:name="android.media.midi.MidiDeviceService"
       android:resource="@xml/synth_device_info" />
 </service>
 

The details of the resource in this example is stored in “res/xml/synth_device_info.xml”. The port names that you declare in this file will be available from PortInfo.getName().

 <devices>
     <device manufacturer="MyCompany" product="MidiSynthBasic">
         <input-port name="input" />
     </device>
 </devices>
 

Extend MidiDeviceService

You then define your server by extending android.media.midi.MidiDeviceService. Let‘s assume you have a MySynthEngine class that extends MidiReceiver.

 import android.media.midi.MidiDeviceService;
 import android.media.midi.MidiDeviceStatus;
 import android.media.midi.MidiReceiver;

 public class MidiSynthDeviceService extends MidiDeviceService {
     private static final String TAG = "MidiSynthDeviceService";
     private MySynthEngine mSynthEngine = new MySynthEngine();
     private boolean synthStarted = false;

     @Override
     public void onCreate() {
         super.onCreate();
     }

     @Override
     public void onDestroy() {
         mSynthEngine.stop();
         super.onDestroy();
     }

     @Override
     // Declare the receivers associated with your input ports.
     public MidiReceiver[] onGetInputPortReceivers() {
         return new MidiReceiver[] { mSynthEngine };
     }

     /**
      * This will get called when clients connect or disconnect.
      * You can use it to turn on your synth only when needed.
      */
     @Override
     public void onDeviceStatusChanged(MidiDeviceStatus status) {
         if (status.isInputPortOpen(0) && !synthStarted) {
             mSynthEngine.start();
             synthStarted = true;
         } else if (!status.isInputPortOpen(0) && synthStarted){
             mSynthEngine.stop();
             synthStarted = false;
         }
     }
 }
 

Using MIDI 2.0

An app can use MIDI 2.0 over USB starting in Android T. MIDI 2.0 packets are embedded in Universal MIDI Packets, or UMP for short. A MIDI 2.0 USB device should create two interfaces, one endpoint that accepts only MIDI 1.0 packets and one that accepts only UMP packets. For more info about MIDI 2.0 and UMP, please read the MIDI 2.0 and UMP spec. Starting from Android V, apps can also open MIDI 2.0 virtual devices.

MidiManager.getDevices() would simply return the 1.0 interface. This interface should work exactly the same as before. In order to use the new UMP interface, retrieve the device with the following code snippet.

 Collection<MidiDeviceInfo> universalDeviceInfos = midiManager.getDevicesForTransport(
         MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS);
 

UMP packet sizes are always a multiple of 4 bytes. For each set of 4 bytes, they are sent in network order. Compare the following NoteOn code snippet with the NoteOn code snippet above.

 byte[] buffer = new byte[32];
 int numBytes = 0;
 int channel = 3; // MIDI channels 1-16 are encoded as 0-15.
 int group = 0;
 buffer[numBytes++] = (byte)(0x20 + group); // MIDI 1.0 Channel Voice Message
 buffer[numBytes++] = (byte)(0x90 + (channel - 1)); // note on
 buffer[numBytes++] = (byte)60; // pitch is middle C
 buffer[numBytes++] = (byte)127; // max velocity
 int offset = 0;
 // post is non-blocking
 inputPort.send(buffer, offset, numBytes);
 

MIDI 2.0 messages can be sent through UMP after negotiating with the device. This is called MIDI-CI and is documented in the MIDI 2.0 spec. Some USB devices support pre-negotiated MIDI 2.0. For a MidiDeviceInfo, you can query the defaultProtocol.

 int defaultProtocol = info.getDefaultProtocol();
 

To register for callbacks when MIDI 2.0 devices are added or removed, use MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS as the transport.

 midiManager.registerDeviceCallback(
         MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS, executor, callback);
 

Creating a MIDI 2.0 Virtual Device Service

Starting in Android V, an app can provide a MIDI 2.0 Service that can be used by other apps. MIDI 2.0 packets are embedded in Universal MIDI Packets, or UMP for short. The service must be guarded with permission "android.permission.BIND_MIDI_DEVICE_SERVICE".

Manifest Files

An app declares that it will function as a MIDI server in the AndroidManifest.xml file. Unlike MIDI 1.0 virtual devices, android.media.midi.MidiUmpDeviceService is used

 <service android:name="MidiEchoDeviceService"
   android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
   <intent-filter>
     <action android:name="android.media.midi.MidiUmpDeviceService" />
   </intent-filter>
   <meta-data android:name="android.media.midi.MidiUmpDeviceService"
       android:resource="@xml/echo_device_info" />
 </service>
 

The details of the resource in this example is stored in “res/xml/echo_device_info.xml ”. The port names that you declare in this file will be available from PortInfo.getName(). Unlike MIDI 1.0, MIDI 2.0 ports are bidirectional. If you declare a port in this service, then it automatically creates an input port and an output port with the same name. Clients can use those two ports like the MIDI 1.0 ports.

 <devices>
     <device manufacturer="MyCompany" product="MidiEcho">
         <port name="port1" />
     </device>
 </devices>
 

Extend MidiUmpDeviceService

You then define your server by extending android.media.midi.MidiUmpDeviceService.

 import android.media.midi.MidiDeviceStatus;
 import android.media.midi.MidiReceiver;
 import android.media.midi.MidiUmpDeviceService;

 public class MidiEchoDeviceService extends MidiUmpDeviceService {
     private static final String TAG = "MidiEchoDeviceService";
     // Other apps will write to this port.
     private MidiReceiver mInputReceiver = new MyReceiver();
     // This app will copy the data to this port.
     private MidiReceiver mOutputReceiver;

     @Override
     public void onCreate() {
         super.onCreate();
     }

     @Override
     public void onDestroy() {
         super.onDestroy();
     }

     @Override
     // Declare the receivers associated with your input ports.
     public List onGetInputPortReceivers() {
         return new ArrayList(Collections.singletonList(mInputReceiver));
     }

     /**
      * Sample receiver to echo from the input port to the output port.
      * In this example, we are just echoing the data and not parsing it.
      * You will probably want to convert the bytes to a packet and then interpret the packet.
      * See the MIDI 2.0 spec at the MMA site. Packets are either 4, 8, 12 or 16 bytes.
      */
     class MyReceiver extends MidiReceiver {
         @Override
         public void onSend(byte[] data, int offset, int count, long timestamp)
                 throws IOException {
             if (mOutputReceiver == null) {
                 mOutputReceiver = getOutputPortReceivers().get(0);
             }
             // Copy input to output.
             mOutputReceiver.send(data, offset, count, timestamp);
         }
     }

     /**
      * This will get called when clients connect or disconnect.
      * You can use it to figure out how many devices are connected.
      */
     @Override
     public void onDeviceStatusChanged(MidiDeviceStatus status) {
         // inputOpened = status.isInputPortOpen(0);
         // outputOpenCount = status.getOutputPortOpenCount(0);
     }
 }
 

Interfaces

MidiManager.OnDeviceOpenedListener Listener class used for receiving the results of MidiManager.openDevice(MidiDeviceInfo, OnDeviceOpenedListener, Handler) and MidiManager.openBluetoothDevice(BluetoothDevice, OnDeviceOpenedListener, Handler) 

Classes

MidiDevice This class is used for sending and receiving data to and from a MIDI device Instances of this class are created by MidiManager#openDevice
MidiDevice.MidiConnection This class represents a connection between the output port of one device and the input port of another. 
MidiDeviceInfo This class contains information to describe a MIDI device. 
MidiDeviceInfo.PortInfo Contains information about an input or output port. 
MidiDeviceService A service that implements a virtual MIDI device. 
MidiDeviceStatus This is an immutable class that describes the current status of a MIDI device's ports. 
MidiInputPort This class is used for sending data to a port on a MIDI device 
MidiManager This class is the public application interface to the MIDI service. 
MidiManager.DeviceCallback Callback class used for clients to receive MIDI device added and removed notifications 
MidiOutputPort This class is used for receiving data from a port on a MIDI device 
MidiReceiver Interface for sending and receiving data to and from a MIDI device. 
MidiSender Interface provided by a device to allow attaching MidiReceivers to a MIDI device. 
MidiUmpDeviceService A service that implements a virtual MIDI device for Universal MIDI Packets (UMP).