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

Commit 750a3496 authored by Robert Wu's avatar Robert Wu
Browse files

Rewrite MIDI 1.0 USB Host mode in Java

ALSA has multiple bugs related to MIDI 1.0. This CL removes
Android's dependency on ALSA for MIDI 1.0 USB Host mode. MIDI to
USB MIDI conversion is done in UsbMidiPacketConverter.java

ALSA is still used for peripheral mode.

Bug: 217392573
Test: Ran local unit tests with UsbMidiPacketConverter
Test: MidiScope/MidiKeyboard with multiple MIDI keyboards
Change-Id: I0b86f9bd26e4c9a50576f704deb6b53fc9a5c130
parent 4c6239b1
Loading
Loading
Loading
Loading
+108 −0
Original line number Original line Diff line number Diff line
@@ -62,6 +62,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Iterator;
import java.util.List;
import java.util.List;


@@ -114,6 +115,19 @@ public class MidiService extends IMidiManager.Stub {


    private final PackageManager mPackageManager;
    private final PackageManager mPackageManager;


    private static final String MIDI_LEGACY_STRING = "MIDI 1.0";
    private static final String MIDI_UNIVERSAL_STRING = "MIDI 2.0";

    // Used to lock mUsbMidiLegacyDeviceOpenCount and mUsbMidiUniversalDeviceInUse.
    private final Object mUsbMidiLock = new Object();

    // Number of times a USB MIDI 1.0 device has opened, based on the device name.
    private final HashMap<String, Integer> mUsbMidiLegacyDeviceOpenCount =
            new HashMap<String, Integer>();

    // Whether a USB MIDI device has opened, based on the device name.
    private final HashSet<String> mUsbMidiUniversalDeviceInUse = new HashSet<String>();

    // UID of BluetoothMidiService
    // UID of BluetoothMidiService
    private int mBluetoothServiceUid;
    private int mBluetoothServiceUid;


@@ -246,6 +260,12 @@ public class MidiService extends IMidiManager.Stub {
            }
            }


            for (DeviceConnection connection : mDeviceConnections.values()) {
            for (DeviceConnection connection : mDeviceConnections.values()) {
                if (connection.getDevice().getDeviceInfo().getType()
                        == MidiDeviceInfo.TYPE_USB) {
                    synchronized (mUsbMidiLock) {
                        removeUsbMidiDeviceLocked(connection.getDevice().getDeviceInfo());
                    }
                }
                connection.getDevice().removeDeviceConnection(connection);
                connection.getDevice().removeDeviceConnection(connection);
            }
            }
        }
        }
@@ -698,6 +718,7 @@ public class MidiService extends IMidiManager.Stub {
        synchronized (mDevicesByInfo) {
        synchronized (mDevicesByInfo) {
            for (Device device : mDevicesByInfo.values()) {
            for (Device device : mDevicesByInfo.values()) {
                if (device.isUidAllowed(uid)) {
                if (device.isUidAllowed(uid)) {
                    deviceInfos.add(device.getDeviceInfo());
                    // UMP devices have protocols that are not PROTOCOL_UNKNOWN
                    // UMP devices have protocols that are not PROTOCOL_UNKNOWN
                    if (transport == MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS) {
                    if (transport == MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS) {
                        if (device.getDeviceInfo().getDefaultProtocol()
                        if (device.getDeviceInfo().getDefaultProtocol()
@@ -734,6 +755,15 @@ public class MidiService extends IMidiManager.Stub {
            }
            }
        }
        }


        if (deviceInfo.getType() == MidiDeviceInfo.TYPE_USB) {
            synchronized (mUsbMidiLock) {
                if (isUsbMidiDeviceInUseLocked(deviceInfo)) {
                    throw new IllegalArgumentException("device already in use: " + deviceInfo);
                }
                addUsbMidiDeviceLocked(deviceInfo);
            }
        }

        // clear calling identity so bindService does not fail
        // clear calling identity so bindService does not fail
        final long identity = Binder.clearCallingIdentity();
        final long identity = Binder.clearCallingIdentity();
        try {
        try {
@@ -1145,4 +1175,82 @@ public class MidiService extends IMidiManager.Stub {
        }
        }
        pw.decreaseIndent();
        pw.decreaseIndent();
    }
    }

    // hold mUsbMidiLock before calling this
    private boolean isUsbMidiDeviceInUseLocked(MidiDeviceInfo info) {
        String name = info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME);
        if (name.length() < MIDI_LEGACY_STRING.length()) {
            return false;
        }
        String deviceName = extractUsbDeviceName(name);
        String tagName = extractUsbDeviceTag(name);

        // Only one MIDI 2.0 device can be used at once.
        // Multiple MIDI 1.0 devices can be used at once.
        if (mUsbMidiUniversalDeviceInUse.contains(deviceName)
                || ((tagName).equals(MIDI_UNIVERSAL_STRING)
                && (mUsbMidiLegacyDeviceOpenCount.containsKey(deviceName)))) {
            return true;
        }
        return false;
    }

    // hold mUsbMidiLock before calling this
    void addUsbMidiDeviceLocked(MidiDeviceInfo info) {
        String name = info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME);
        if (name.length() < MIDI_LEGACY_STRING.length()) {
            return;
        }
        String deviceName = extractUsbDeviceName(name);
        String tagName = extractUsbDeviceTag(name);

        if ((tagName).equals(MIDI_UNIVERSAL_STRING)) {
            mUsbMidiUniversalDeviceInUse.add(deviceName);
        } else if ((tagName).equals(MIDI_LEGACY_STRING)) {
            int count = mUsbMidiLegacyDeviceOpenCount.getOrDefault(deviceName, 0) + 1;
            mUsbMidiLegacyDeviceOpenCount.put(deviceName, count);
        }
    }

    // hold mUsbMidiLock before calling this
    void removeUsbMidiDeviceLocked(MidiDeviceInfo info) {
        String name = info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME);
        if (name.length() < MIDI_LEGACY_STRING.length()) {
            return;
        }
        String deviceName = extractUsbDeviceName(name);
        String tagName = extractUsbDeviceTag(name);

        if ((tagName).equals(MIDI_UNIVERSAL_STRING)) {
            mUsbMidiUniversalDeviceInUse.remove(deviceName);
        } else if ((tagName).equals(MIDI_LEGACY_STRING)) {
            if (mUsbMidiLegacyDeviceOpenCount.containsKey(deviceName)) {
                int count = mUsbMidiLegacyDeviceOpenCount.get(deviceName);
                if (count > 1) {
                    mUsbMidiLegacyDeviceOpenCount.put(deviceName, count - 1);
                } else {
                    mUsbMidiLegacyDeviceOpenCount.remove(deviceName);
                }
            }
        }
    }

    // The USB property name is in the form "manufacturer product#Id MIDI 1.0".
    // This is defined in UsbDirectMidiDevice.java.
    // This function extracts out the "manufacturer product#Id " part.
    // Two devices would have the same device name if they had the following property name:
    // "manufacturer product#Id MIDI 1.0"
    // "manufacturer product#Id MIDI 2.0"
    // Note that MIDI_LEGACY_STRING and MIDI_UNIVERSAL_STRING are the same length.
    String extractUsbDeviceName(String propertyName) {
        return propertyName.substring(0, propertyName.length() - MIDI_LEGACY_STRING.length());
    }

    // The USB property name is in the form "manufacturer product#Id MIDI 1.0".
    // This is defined in UsbDirectMidiDevice.java.
    // This function extracts the "MIDI 1.0" part.
    // Note that MIDI_LEGACY_STRING and MIDI_UNIVERSAL_STRING are the same length.
    String extractUsbDeviceTag(String propertyName) {
        return propertyName.substring(propertyName.length() - MIDI_LEGACY_STRING.length());
    }
}
}
+0 −59
Original line number Original line Diff line number Diff line
@@ -36,7 +36,6 @@ import libcore.io.IoUtils;


import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.List;


/**
/**
@@ -107,12 +106,6 @@ public final class UsbAlsaManager {
        return false;
        return false;
    }
    }


    /**
     * List of connected MIDI devices
     */
    private final HashMap<String, UsbMidiDevice>
            mMidiDevices = new HashMap<String, UsbMidiDevice>();

    // UsbMidiDevice for USB peripheral mode (gadget) device
    // UsbMidiDevice for USB peripheral mode (gadget) device
    private UsbMidiDevice mPeripheralMidiDevice = null;
    private UsbMidiDevice mPeripheralMidiDevice = null;


@@ -255,45 +248,6 @@ public final class UsbAlsaManager {
            }
            }
        }
        }


        // look for MIDI devices
        boolean hasMidi = parser.hasMIDIInterface();
        int midiNumInputs = parser.calculateNumLegacyMidiInputs();
        int midiNumOutputs = parser.calculateNumLegacyMidiOutputs();
        if (DEBUG) {
            Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature);
            Slog.d(TAG, "midiNumInputs: " + midiNumInputs + " midiNumOutputs:" + midiNumOutputs);
        }
        if (hasMidi && mHasMidiFeature) {
            int device = 0;
            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);

            UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
                    cardRec.getCardNum(), 0 /*device*/, midiNumInputs, midiNumOutputs);
            if (usbMidiDevice != null) {
                mMidiDevices.put(deviceAddress, usbMidiDevice);
            }
        }

        logDevices("deviceAdded()");
        logDevices("deviceAdded()");


        if (DEBUG) {
        if (DEBUG) {
@@ -314,13 +268,6 @@ public final class UsbAlsaManager {
            selectDefaultDevice(); // if there any external devices left, select one of them
            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: " + usbMidiDevice);
            IoUtils.closeQuietly(usbMidiDevice);
        }

        logDevices("usbDeviceRemoved()");
        logDevices("usbDeviceRemoved()");


    }
    }
@@ -376,12 +323,6 @@ public final class UsbAlsaManager {
            usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES);
            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);
        dump.end(token);
    }
    }


+157 −56

File changed and moved.

Preview size limit exceeded, changes collapsed.

+51 −10
Original line number Original line Diff line number Diff line
@@ -50,9 +50,12 @@ import com.android.server.usb.descriptors.tree.UsbDescriptorsTree;
import libcore.io.IoUtils;
import libcore.io.IoUtils;


import java.text.SimpleDateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Date;
import java.util.HashMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.LinkedList;
import java.util.Random;


/**
/**
 * UsbHostManager manages USB state in host mode.
 * UsbHostManager manages USB state in host mode.
@@ -94,10 +97,12 @@ public class UsbHostManager {
    private final ArrayMap<String, ConnectionRecord> mConnected = new ArrayMap<>();
    private final ArrayMap<String, ConnectionRecord> mConnected = new ArrayMap<>();


    /**
    /**
     * List of connected MIDI devices
     * List of connected MIDI devices. Key on deviceAddress.
     */
     */
    private final HashMap<String, UsbUniversalMidiDevice>
    private final HashMap<String, ArrayList<UsbDirectMidiDevice>>
            mMidiDevices = new HashMap<String, UsbUniversalMidiDevice>();
            mMidiDevices = new HashMap<String, ArrayList<UsbDirectMidiDevice>>();
    private final HashSet<String> mMidiUniqueCodes = new HashSet<String>();
    private final Random mRandom = new Random();
    private final boolean mHasMidiFeature;
    private final boolean mHasMidiFeature;


    /*
    /*
@@ -425,15 +430,35 @@ public class UsbHostManager {
                mUsbAlsaManager.usbDeviceAdded(deviceAddress, newDevice, parser);
                mUsbAlsaManager.usbDeviceAdded(deviceAddress, newDevice, parser);


                if (mHasMidiFeature) {
                if (mHasMidiFeature) {
                    // Use a 3 digit code to associate MIDI devices with one another.
                    // Each MIDI device already has mId for uniqueness. mId is generated
                    // sequentially. For clarity, this code is not generated sequentially.
                    String uniqueUsbDeviceIdentifier = generateNewUsbDeviceIdentifier();

                    ArrayList<UsbDirectMidiDevice> midiDevices =
                            new ArrayList<UsbDirectMidiDevice>();
                    if (parser.containsUniversalMidiDeviceEndpoint()) {
                    if (parser.containsUniversalMidiDeviceEndpoint()) {
                        UsbUniversalMidiDevice midiDevice = UsbUniversalMidiDevice.create(mContext,
                        UsbDirectMidiDevice midiDevice = UsbDirectMidiDevice.create(mContext,
                                newDevice, parser);
                                newDevice, parser, true, uniqueUsbDeviceIdentifier);
                        if (midiDevice != null) {
                        if (midiDevice != null) {
                            mMidiDevices.put(deviceAddress, midiDevice);
                            midiDevices.add(midiDevice);
                        } else {
                        } else {
                            Slog.e(TAG, "Universal Midi Device is null.");
                            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.");
                        }
                    }

                    if (!midiDevices.isEmpty()) {
                        mMidiDevices.put(deviceAddress, midiDevices);
                    }
                }
                }


                // Tracking
                // Tracking
@@ -471,11 +496,14 @@ public class UsbHostManager {
                mPermissionManager.usbDeviceRemoved(device);
                mPermissionManager.usbDeviceRemoved(device);


                // MIDI
                // MIDI
                UsbUniversalMidiDevice midiDevice = mMidiDevices.remove(deviceAddress);
                ArrayList<UsbDirectMidiDevice> midiDevices =
                        mMidiDevices.remove(deviceAddress);
                for (UsbDirectMidiDevice midiDevice : midiDevices) {
                    if (midiDevice != null) {
                    if (midiDevice != null) {
                    Slog.i(TAG, "USB Universal MIDI Device Removed: " + deviceAddress);
                        Slog.i(TAG, "USB MIDI Device Removed: " + deviceAddress);
                        IoUtils.closeQuietly(midiDevice);
                        IoUtils.closeQuietly(midiDevice);
                    }
                    }
                }


                getCurrentUserSettings().usbDeviceRemoved(device);
                getCurrentUserSettings().usbDeviceRemoved(device);
                ConnectionRecord current = mConnected.get(deviceAddress);
                ConnectionRecord current = mConnected.get(deviceAddress);
@@ -608,6 +636,19 @@ public class UsbHostManager {
        return true;
        return true;
    }
    }


    // Generate a 3 digit code.
    private String generateNewUsbDeviceIdentifier() {
        String code;
        do {
            code = "";
            for (int i = 0; i < 3; i++) {
                code += mRandom.nextInt(10);
            }
        } while (mMidiUniqueCodes.contains(code));
        mMidiUniqueCodes.add(code);
        return code;
    }

    private native void monitorUsbHostBus();
    private native void monitorUsbHostBus();
    private native ParcelFileDescriptor nativeOpenDevice(String deviceAddress);
    private native ParcelFileDescriptor nativeOpenDevice(String deviceAddress);
}
}
+237 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.usb;

import java.io.ByteArrayOutputStream;

/**
 * Converts between MIDI packets and USB MIDI 1.0 packets.
 */
public class UsbMidiPacketConverter {

    // Refer to Table 4-1 in USB MIDI 1.0 spec.
    private static final int[] PAYLOAD_SIZE = new int[]{
            /* 0x00 */ -1, // Miscellaneous function codes. Reserved for future extensions.
            /* 0x01 */ -1, // Cable events. Reserved for future expansion.
            /* 0x02 */  2, // Two-byte System Common messages like MTC, SongSelect, etc
            /* 0x03 */  3, // Three-byte System Common messages like SPP, etc.
            /* 0x04 */  3, // SysEx starts or continues
            /* 0x05 */  1, // Single-byte System Common Message or single-byte SysEx ends.
            /* 0x06 */  2, // SysEx ends with following two bytes.
            /* 0x07 */  3, // SysEx ends with following three bytes.
            /* 0x08 */  3, // Note-off
            /* 0x09 */  3, // Note-on
            /* 0x0a */  3, // Poly-KeyPress
            /* 0x0b */  3, // Control Change
            /* 0x0c */  2, // Program Change
            /* 0x0d */  2, // Channel Pressure
            /* 0x0e */  3, // PitchBend Change
            /* 0x0f */  1  // Single Byte
    };

    // Each System MIDI message is a certain size. These can be mapped to a
    // Code Index number defined in Table 4-1 of USB MIDI 1.0.
    private static final int[] CODE_INDEX_NUMBER_FROM_SYSTEM_TYPE = new int[]{
            /* 0x00 */ -1, // Start of Exclusive. Special case.
            /* 0x01 */  2, // MIDI Time Code. Two byte message
            /* 0x02 */  3, // Song Point Pointer. Three byte message
            /* 0x03 */  2, // Song Select. Two byte message
            /* 0x04 */ -1, // Undefined MIDI System Common
            /* 0x05 */ -1, // Undefined MIDI System Common
            /* 0x06 */  5, // Tune Request. One byte message
            /* 0x07 */ -1, // End of Exclusive. Special case.
            /* 0x08 */  5, // Timing clock. One byte message
            /* 0x09 */ -1, // Undefined MIDI System Real-time
            /* 0x0a */  5, // Start. One byte message
            /* 0x0b */  5, // Continue. One byte message
            /* 0x0c */  5, // Stop. One byte message
            /* 0x0d */ -1, // Undefined MIDI System Real-time
            /* 0x0e */  5, // Active Sensing. One byte message
            /* 0x0f */  5  // System Reset. One byte message
    };

    // These code index numbers also come from Table 4-1 in USB MIDI 1.0 spec.
    private static final byte CODE_INDEX_NUMBER_SYSEX_STARTS_OR_CONTINUES = 0x4;
    private static final byte CODE_INDEX_NUMBER_SINGLE_BYTE = 0xF;
    private static final byte CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE = (byte) 0x5;

    // System messages are defined in MIDI.
    private static final byte FIRST_SYSTEM_MESSAGE_VALUE = (byte) 0xF0;
    private static final byte SYSEX_START_EXCLUSIVE = (byte) 0xF0;
    private static final byte SYSEX_END_EXCLUSIVE = (byte) 0xF7;

    private UsbMidiEncoder mUsbMidiEncoder = new UsbMidiEncoder();
    private UsbMidiDecoder mUsbMidiDecoder = new UsbMidiDecoder();

    /**
     * Converts a USB MIDI array into a raw MIDI array.
     *
     * @param usbMidiBytes the USB MIDI bytes to convert
     * @param size the size of usbMidiBytes
     * @return byte array of raw MIDI packets
     */
    public byte[] usbMidiToRawMidi(byte[] usbMidiBytes, int size) {
        return mUsbMidiDecoder.decode(usbMidiBytes, size);
    }

    /**
     * Converts a raw MIDI array into a USB MIDI array.
     *
     * @param midiBytes the raw MIDI bytes to convert
     * @param size the size of usbMidiBytes
     * @return byte array of USB MIDI packets
     */
    public byte[] rawMidiToUsbMidi(byte[] midiBytes, int size) {
        return mUsbMidiEncoder.encode(midiBytes, size);
    }

    private class UsbMidiDecoder {
        // Decodes the data from USB MIDI to raw MIDI.
        // Each valid 4 byte input maps to a 1-3 byte output.
        // Reference the USB MIDI 1.0 spec for more info.
        public byte[] decode(byte[] usbMidiBytes, int size) {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            for (int i = 0; i + 3 < size; i += 4) {
                int codeIndex = usbMidiBytes[i] & 0x0f;
                int numPayloadBytes = PAYLOAD_SIZE[codeIndex];
                if (numPayloadBytes < 0) {
                    continue;
                }
                outputStream.write(usbMidiBytes, i + 1, numPayloadBytes);
            }
            return outputStream.toByteArray();
        }
    }

    private class UsbMidiEncoder {
        // In order to facilitate large scale transfers, SysEx can be sent in multiple packets.
        // If encode() is called without an SysEx end, we must continue SysEx for the next packet.
        // All other packets should be 3 bytes or less and must be not be broken between packets.
        private byte[] mStoredSystemExclusiveBytes = new byte[3];
        private int mNumStoredSystemExclusiveBytes = 0;
        private boolean mHasSystemExclusiveStarted = false;

        private byte[] mEmptyBytes = new byte[3]; // Used to fill out extra data

        // Encodes the data from raw MIDI to USB MIDI.
        // Each valid 1-3 byte input maps to a 4 byte output.
        // Reference the USB MIDI 1.0 spec for more info.
        // MidiFramer is not needed here as this code handles partial packets.
        // Long SysEx messages split between packets will encode and return a
        // byte stream even if the SysEx end has not been sent.
        // If there are less than 3 remaining data bytes in a SysEx message left,
        // these bytes will be combined with the next set of packets.
        public byte[] encode(byte[] midiBytes, int size) {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            int curLocation = 0;
            while (curLocation < size) {
                if (midiBytes[curLocation] >= 0) { // Data byte
                    if (mHasSystemExclusiveStarted) {
                        mStoredSystemExclusiveBytes[mNumStoredSystemExclusiveBytes] =
                                midiBytes[curLocation];
                        mNumStoredSystemExclusiveBytes++;
                        if (mNumStoredSystemExclusiveBytes == 3) {
                            outputStream.write(CODE_INDEX_NUMBER_SYSEX_STARTS_OR_CONTINUES);
                            outputStream.write(mStoredSystemExclusiveBytes, 0, 3);
                            mNumStoredSystemExclusiveBytes = 0;
                        }
                    } else {
                        writeSingleByte(outputStream, midiBytes[curLocation]);
                    }
                    curLocation++;
                    continue;
                } else if (midiBytes[curLocation] != SYSEX_END_EXCLUSIVE) {
                    // SysEx operation was interrupted. Pass the data directly down.
                    if (mHasSystemExclusiveStarted) {
                        int index = 0;
                        while (index < mNumStoredSystemExclusiveBytes) {
                            writeSingleByte(outputStream, mStoredSystemExclusiveBytes[index]);
                            index++;
                        }
                        mNumStoredSystemExclusiveBytes = 0;
                        mHasSystemExclusiveStarted = false;
                    }
                }

                if (midiBytes[curLocation] < FIRST_SYSTEM_MESSAGE_VALUE) { // Channel message
                    byte codeIndexNumber = (byte) ((midiBytes[curLocation] >> 4) & 0x0f);
                    int channelMessageSize = PAYLOAD_SIZE[codeIndexNumber];
                    if (curLocation + channelMessageSize <= size) {
                        outputStream.write(codeIndexNumber);
                        outputStream.write(midiBytes, curLocation, channelMessageSize);
                        // Fill in the rest of the bytes with 0.
                        outputStream.write(mEmptyBytes, 0, 3 - channelMessageSize);
                        curLocation += channelMessageSize;
                    } else { // The packet is missing data. Use single byte messages.
                        while (curLocation < size) {
                            writeSingleByte(outputStream, midiBytes[curLocation]);
                            curLocation++;
                        }
                    }
                } else if (midiBytes[curLocation] == SYSEX_START_EXCLUSIVE) {
                    mHasSystemExclusiveStarted = true;
                    mStoredSystemExclusiveBytes[0] = midiBytes[curLocation];
                    mNumStoredSystemExclusiveBytes = 1;
                    curLocation++;
                } else if (midiBytes[curLocation] == SYSEX_END_EXCLUSIVE) {
                    // 1 byte is 0x05, 2 bytes is 0x06, and 3 bytes is 0x07
                    outputStream.write(CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE
                            + mNumStoredSystemExclusiveBytes);
                    mStoredSystemExclusiveBytes[mNumStoredSystemExclusiveBytes] =
                            midiBytes[curLocation];
                    mNumStoredSystemExclusiveBytes++;
                    outputStream.write(mStoredSystemExclusiveBytes, 0,
                             mNumStoredSystemExclusiveBytes);
                    // Fill in the rest of the bytes with 0.
                    outputStream.write(mEmptyBytes, 0, 3 - mNumStoredSystemExclusiveBytes);
                    mHasSystemExclusiveStarted = false;
                    mNumStoredSystemExclusiveBytes = 0;
                    curLocation++;
                } else {
                    int systemType = midiBytes[curLocation] & 0x0f;
                    int codeIndexNumber = CODE_INDEX_NUMBER_FROM_SYSTEM_TYPE[systemType];
                    if (codeIndexNumber < 0) { // Unknown type. Use single byte messages.
                        writeSingleByte(outputStream, midiBytes[curLocation]);
                        curLocation++;
                    } else {
                        int systemMessageSize = PAYLOAD_SIZE[codeIndexNumber];
                        if (curLocation + systemMessageSize <= size) {
                            outputStream.write(codeIndexNumber);
                            outputStream.write(midiBytes, curLocation, systemMessageSize);
                            // Fill in the rest of the bytes with 0.
                            outputStream.write(mEmptyBytes, 0, 3 - systemMessageSize);
                            curLocation += systemMessageSize;
                        } else { // The packet is missing data. Use single byte messages.
                            while (curLocation < size) {
                                writeSingleByte(outputStream, midiBytes[curLocation]);
                                curLocation++;
                            }
                        }
                    }
                }
            }
            return outputStream.toByteArray();
        }

        private void writeSingleByte(ByteArrayOutputStream outputStream, byte byteToWrite) {
            outputStream.write(CODE_INDEX_NUMBER_SINGLE_BYTE);
            outputStream.write(byteToWrite);
            outputStream.write(0);
            outputStream.write(0);
        }
    }
}
Loading