blob: 65d7a41cf1ec0cc565eae8a81343eff86c57263c [file] [log] [blame]
/*
* 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 android.util.Log;
import java.io.ByteArrayOutputStream;
/**
* Converts between raw MIDI packets and USB MIDI 1.0 packets.
* This is NOT thread-safe. Please handle locking outside this function for multiple threads.
* For data mapping to an invalid cable number, this converter will use the first cable.
*/
public class UsbMidiPacketConverter {
private static final String TAG = "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[] mUsbMidiEncoders;
private ByteArrayOutputStream mEncoderOutputStream = new ByteArrayOutputStream();
private UsbMidiDecoder mUsbMidiDecoder;
/**
* Creates encoders.
*
* createEncoders() must be called before raw MIDI can be converted to USB MIDI.
*
* @param size the number of encoders to create
*/
public void createEncoders(int size) {
mUsbMidiEncoders = new UsbMidiEncoder[size];
for (int i = 0; i < size; i++) {
mUsbMidiEncoders[i] = new UsbMidiEncoder(i);
}
}
/**
* Converts a raw MIDI array into a USB MIDI array.
*
* Call pullEncodedMidiPackets to retrieve the byte array.
*
* @param midiBytes the raw MIDI bytes to convert
* @param size the size of usbMidiBytes
* @param encoderId which encoder to use
*/
public void encodeMidiPackets(byte[] midiBytes, int size, int encoderId) {
// Use the first encoder if the encoderId is invalid.
if (encoderId >= mUsbMidiEncoders.length) {
Log.w(TAG, "encoderId " + encoderId + " invalid");
encoderId = 0;
}
byte[] encodedPacket = mUsbMidiEncoders[encoderId].encode(midiBytes, size);
mEncoderOutputStream.write(encodedPacket, 0, encodedPacket.length);
}
/**
* Returns the encoded MIDI packets from encodeMidiPackets
*
* @return byte array of USB MIDI packets
*/
public byte[] pullEncodedMidiPackets() {
byte[] output = mEncoderOutputStream.toByteArray();
mEncoderOutputStream.reset();
return output;
}
/**
* Creates decoders.
*
* createDecoders() must be called before USB MIDI can be converted to raw MIDI.
*
* @param size the number of decoders to create
*/
public void createDecoders(int size) {
mUsbMidiDecoder = new UsbMidiDecoder(size);
}
/**
* Converts a USB MIDI array into a multiple MIDI arrays, one per cable.
*
* Call pullDecodedMidiPackets to retrieve the byte array.
*
* @param usbMidiBytes the USB MIDI bytes to convert
* @param size the size of usbMidiBytes
*/
public void decodeMidiPackets(byte[] usbMidiBytes, int size) {
mUsbMidiDecoder.decode(usbMidiBytes, size);
}
/**
* Returns the decoded MIDI packets from decodeMidiPackets
*
* @param cableNumber the cable to pull data from
* @return byte array of raw MIDI packets
*/
public byte[] pullDecodedMidiPackets(int cableNumber) {
return mUsbMidiDecoder.pullBytes(cableNumber);
}
private class UsbMidiDecoder {
int mNumJacks;
ByteArrayOutputStream[] mDecodedByteArrays;
UsbMidiDecoder(int numJacks) {
mNumJacks = numJacks;
mDecodedByteArrays = new ByteArrayOutputStream[numJacks];
for (int i = 0; i < numJacks; i++) {
mDecodedByteArrays[i] = new ByteArrayOutputStream();
}
}
// 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 void decode(byte[] usbMidiBytes, int size) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
if (size % 4 != 0) {
Log.w(TAG, "size " + size + " not multiple of 4");
}
for (int i = 0; i + 3 < size; i += 4) {
int cableNumber = (usbMidiBytes[i] >> 4) & 0x0f;
int codeIndex = usbMidiBytes[i] & 0x0f;
int numPayloadBytes = PAYLOAD_SIZE[codeIndex];
if (numPayloadBytes < 0) {
continue;
}
// Use the first cable if the cable number is invalid.
if (cableNumber >= mNumJacks) {
Log.w(TAG, "cableNumber " + cableNumber + " invalid");
cableNumber = 0;
}
mDecodedByteArrays[cableNumber].write(usbMidiBytes, i + 1, numPayloadBytes);
}
}
public byte[] pullBytes(int cableNumber) {
// Use the first cable if the cable number is invalid.
if (cableNumber >= mNumJacks) {
Log.w(TAG, "cableNumber " + cableNumber + " invalid");
cableNumber = 0;
}
byte[] output = mDecodedByteArrays[cableNumber].toByteArray();
mDecodedByteArrays[cableNumber].reset();
return output;
}
}
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
private byte mShiftedCableNumber;
UsbMidiEncoder(int cableNumber) {
// Jack Id is always the left nibble of every byte so shift this now.
mShiftedCableNumber = (byte) (cableNumber << 4);
}
// 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
| mShiftedCableNumber);
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 | mShiftedCableNumber);
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) | mShiftedCableNumber);
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 | mShiftedCableNumber);
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 | mShiftedCableNumber);
outputStream.write(byteToWrite);
outputStream.write(0);
outputStream.write(0);
}
}
}