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

Commit 88e10487 authored by Robert Wu's avatar Robert Wu
Browse files

USB MIDI: Multijack MIDI 1.0 for MIDI 2.0 devices

MIDI 2.0 devices only show one input and output port for its backup
MIDI 1.0 path. This CL reads the jack counts and creates encoders
and decoders accordingly.

UsbMidiPacketConverter has been refactored to use cable numbers for
multiplexing USB MIDI streams.

Bug: 266473128
Test: MidiScope/MidiKeyboard on multiport MIDI device
Test: UmpMidiScope on MIDI 2.0 devices
Change-Id: I58799393ab0bddf7c6a6c63f1ab014accf1ab10e
parent ca9e2463
Loading
Loading
Loading
Loading
+37 −16
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.internal.midi;

import java.util.Iterator;
import java.util.SortedMap;
import java.util.TreeMap;

@@ -26,11 +25,11 @@ import java.util.TreeMap;
 * And only one Thread can read from the buffer.
 */
public class EventScheduler {
    private static final long NANOS_PER_MILLI = 1000000;
    public static final long NANOS_PER_MILLI = 1000000;

    private final Object mLock = new Object();
    volatile private SortedMap<Long, FastEventQueue> mEventBuffer;
    private FastEventQueue mEventPool = null;
    protected volatile SortedMap<Long, FastEventQueue> mEventBuffer;
    protected FastEventQueue mEventPool = null;
    private int mMaxPoolSize = 200;
    private boolean mClosed;

@@ -38,9 +37,13 @@ public class EventScheduler {
        mEventBuffer = new TreeMap<Long, FastEventQueue>();
    }

    // If we keep at least one node in the list then it can be atomic
    // and non-blocking.
    private class FastEventQueue {
    /**
     * Class for a fast event queue.
     *
     * If we keep at least one node in the list then it can be atomic
     * and non-blocking.
     */
    public static class FastEventQueue {
        // One thread takes from the beginning of the list.
        volatile SchedulableEvent mFirst;
        // A second thread returns events to the end of the list.
@@ -48,7 +51,7 @@ public class EventScheduler {
        volatile long mEventsAdded;
        volatile long mEventsRemoved;

        FastEventQueue(SchedulableEvent event) {
        public FastEventQueue(SchedulableEvent event) {
            mFirst = event;
            mLast = mFirst;
            mEventsAdded = 1;
@@ -149,7 +152,8 @@ public class EventScheduler {
     * @param event
     */
    public void add(SchedulableEvent event) {
        synchronized (mLock) {
        Object lock = getLock();
        synchronized (lock) {
            FastEventQueue list = mEventBuffer.get(event.getTimestamp());
            if (list == null) {
                long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
@@ -159,7 +163,7 @@ public class EventScheduler {
                // If the event we added is earlier than the previous earliest
                // event then notify any threads waiting for the next event.
                if (event.getTimestamp() < lowestTime) {
                    mLock.notify();
                    lock.notify();
                }
            } else {
                list.add(event);
@@ -167,7 +171,7 @@ public class EventScheduler {
        }
    }

    private SchedulableEvent removeNextEventLocked(long lowestTime) {
    protected SchedulableEvent removeNextEventLocked(long lowestTime) {
        SchedulableEvent event;
        FastEventQueue list = mEventBuffer.get(lowestTime);
        // Remove list from tree if this is the last node.
@@ -186,7 +190,8 @@ public class EventScheduler {
     */
    public SchedulableEvent getNextEvent(long time) {
        SchedulableEvent event = null;
        synchronized (mLock) {
        Object lock = getLock();
        synchronized (lock) {
            if (!mEventBuffer.isEmpty()) {
                long lowestTime = mEventBuffer.firstKey();
                // Is it time for this list to be processed?
@@ -209,7 +214,8 @@ public class EventScheduler {
     */
    public SchedulableEvent waitNextEvent() throws InterruptedException {
        SchedulableEvent event = null;
        synchronized (mLock) {
        Object lock = getLock();
        synchronized (lock) {
            while (!mClosed) {
                long millisToWait = Integer.MAX_VALUE;
                if (!mEventBuffer.isEmpty()) {
@@ -231,7 +237,7 @@ public class EventScheduler {
                        }
                    }
                }
                mLock.wait((int) millisToWait);
                lock.wait((int) millisToWait);
            }
        }
        return event;
@@ -242,10 +248,25 @@ public class EventScheduler {
        mEventBuffer = new TreeMap<Long, FastEventQueue>();
    }

    /**
     * Stops the EventScheduler.
     * The subscriber calling waitNextEvent() will get one final SchedulableEvent returning null.
     */
    public void close() {
        synchronized (mLock) {
        Object lock = getLock();
        synchronized (lock) {
            mClosed = true;
            mLock.notify();
            lock.notify();
        }
    }

    /**
     * Gets the lock. This doesn't lock it in anyway.
     * Subclasses can override this.
     *
     * @return Object
     */
    protected Object getLock() {
        return mLock;
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.internal.midi;

/**
 * Uses multiple MidiEventSchedulers for waiting for events.
 *
 */
public class MidiEventMultiScheduler {
    private MultiLockMidiEventScheduler[] mMidiEventSchedulers;
    private int mNumEventSchedulers;
    private int mNumClosedSchedulers = 0;
    private final Object mMultiLock = new Object();

    private class MultiLockMidiEventScheduler extends MidiEventScheduler {
        @Override
        public void close() {
            synchronized (mMultiLock) {
                mNumClosedSchedulers++;
            }
            super.close();
        }

        @Override
        protected Object getLock() {
            return mMultiLock;
        }

        public boolean isEventBufferEmptyLocked() {
            return mEventBuffer.isEmpty();
        }

        public long getLowestTimeLocked() {
            return mEventBuffer.firstKey();
        }
    }

    /**
     * MidiEventMultiScheduler constructor
     *
     * @param numSchedulers the number of schedulers to create
     */
    public MidiEventMultiScheduler(int numSchedulers) {
        mNumEventSchedulers = numSchedulers;
        mMidiEventSchedulers = new MultiLockMidiEventScheduler[numSchedulers];
        for (int i = 0; i < numSchedulers; i++) {
            mMidiEventSchedulers[i] = new MultiLockMidiEventScheduler();
        }
    }

    /**
     * Waits for the next MIDI event. This will return true when it receives it.
     * If all MidiEventSchedulers have been closed, this will return false.
     *
     * @return true if a MIDI event is received and false if all schedulers are closed.
     */
    public boolean waitNextEvent() throws InterruptedException {
        synchronized (mMultiLock) {
            while (true) {
                if (mNumClosedSchedulers >= mNumEventSchedulers) {
                    return false;
                }
                long lowestTime = Long.MAX_VALUE;
                long now = System.nanoTime();
                for (MultiLockMidiEventScheduler eventScheduler : mMidiEventSchedulers) {
                    if (!eventScheduler.isEventBufferEmptyLocked()) {
                        lowestTime = Math.min(lowestTime,
                                eventScheduler.getLowestTimeLocked());
                    }
                }
                if (lowestTime <= now) {
                    return true;
                }
                long nanosToWait = lowestTime - now;
                // Add 1 millisecond so we don't wake up before it is
                // ready.
                long millisToWait = 1 + (nanosToWait / EventScheduler.NANOS_PER_MILLI);
                // Clip 64-bit value to 32-bit max.
                if (millisToWait > Integer.MAX_VALUE) {
                    millisToWait = Integer.MAX_VALUE;
                }
                mMultiLock.wait(millisToWait);
            }
        }
    }

    /**
     * Gets the number of MidiEventSchedulers.
     *
     * @return the number of MidiEventSchedulers.
     */
    public int getNumEventSchedulers() {
        return mNumEventSchedulers;
    }

    /**
     * Gets a specific MidiEventScheduler based on the index.
     *
     * @param index the zero indexed index of a MIDI event scheduler
     * @return a MidiEventScheduler
     */
    public MidiEventScheduler getEventScheduler(int index) {
        return mMidiEventSchedulers[index];
    }

    /**
     * Closes all event schedulers.
     */
    public void close() {
        for (MidiEventScheduler eventScheduler : mMidiEventSchedulers) {
            eventScheduler.close();
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -79,7 +79,7 @@ public class MidiEventScheduler extends EventScheduler {
    /**
     * Create an event that contains the message.
     */
    private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
    public MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
            long timestamp) {
        MidiEvent event;
        if (count > POOL_EVENT_SIZE) {
+212 −100

File changed.

Preview size limit exceeded, changes collapsed.

+117 −25
Original line number Diff line number Diff line
@@ -16,12 +16,17 @@

package com.android.server.usb;

import android.util.Log;

import java.io.ByteArrayOutputStream;

/**
 * Converts between MIDI packets and USB MIDI 1.0 packets.
 * 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[]{
@@ -74,54 +79,133 @@ public class UsbMidiPacketConverter {
    private static final byte SYSEX_START_EXCLUSIVE = (byte) 0xF0;
    private static final byte SYSEX_END_EXCLUSIVE = (byte) 0xF7;

    private UsbMidiDecoder mUsbMidiDecoder = new UsbMidiDecoder();
    private UsbMidiEncoder[] mUsbMidiEncoders;
    private ByteArrayOutputStream mEncoderOutputStream = new ByteArrayOutputStream();

    public UsbMidiPacketConverter(int numEncoders) {
        mUsbMidiEncoders = new UsbMidiEncoder[numEncoders];
        for (int i = 0; i < numEncoders; i++) {
            mUsbMidiEncoders[i] = new UsbMidiEncoder();
        }
    }
    private UsbMidiDecoder mUsbMidiDecoder;

    /**
     * Converts a USB MIDI array into a raw MIDI array.
     * Creates encoders.
     *
     * @param usbMidiBytes the USB MIDI bytes to convert
     * @param size the size of usbMidiBytes
     * @return byte array of raw MIDI packets
     * createEncoders() must be called before raw MIDI can be converted to USB MIDI.
     *
     * @param size the number of encoders to create
     */
    public byte[] usbMidiToRawMidi(byte[] usbMidiBytes, int size) {
        return mUsbMidiDecoder.decode(usbMidiBytes, size);
    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[] rawMidiToUsbMidi(byte[] midiBytes, int size, int encoderId) {
        return mUsbMidiEncoders[encoderId].encode(midiBytes, size);
    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 byte[] decode(byte[] usbMidiBytes, int size) {
        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;
                }
                outputStream.write(usbMidiBytes, i + 1, numPayloadBytes);
                // 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);
            }
            return outputStream.toByteArray();
        }

        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;
        }
    }

@@ -135,6 +219,13 @@ public class UsbMidiPacketConverter {

        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.
@@ -153,7 +244,8 @@ public class UsbMidiPacketConverter {
                                midiBytes[curLocation];
                        mNumStoredSystemExclusiveBytes++;
                        if (mNumStoredSystemExclusiveBytes == 3) {
                            outputStream.write(CODE_INDEX_NUMBER_SYSEX_STARTS_OR_CONTINUES);
                            outputStream.write(CODE_INDEX_NUMBER_SYSEX_STARTS_OR_CONTINUES
                                    | mShiftedCableNumber);
                            outputStream.write(mStoredSystemExclusiveBytes, 0, 3);
                            mNumStoredSystemExclusiveBytes = 0;
                        }
@@ -179,7 +271,7 @@ public class UsbMidiPacketConverter {
                    byte codeIndexNumber = (byte) ((midiBytes[curLocation] >> 4) & 0x0f);
                    int channelMessageSize = PAYLOAD_SIZE[codeIndexNumber];
                    if (curLocation + channelMessageSize <= size) {
                        outputStream.write(codeIndexNumber);
                        outputStream.write(codeIndexNumber | mShiftedCableNumber);
                        outputStream.write(midiBytes, curLocation, channelMessageSize);
                        // Fill in the rest of the bytes with 0.
                        outputStream.write(mEmptyBytes, 0, 3 - channelMessageSize);
@@ -197,8 +289,8 @@ public class UsbMidiPacketConverter {
                    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);
                    outputStream.write((CODE_INDEX_NUMBER_SYSEX_END_SINGLE_BYTE
                            + mNumStoredSystemExclusiveBytes) | mShiftedCableNumber);
                    mStoredSystemExclusiveBytes[mNumStoredSystemExclusiveBytes] =
                            midiBytes[curLocation];
                    mNumStoredSystemExclusiveBytes++;
@@ -218,7 +310,7 @@ public class UsbMidiPacketConverter {
                    } else {
                        int systemMessageSize = PAYLOAD_SIZE[codeIndexNumber];
                        if (curLocation + systemMessageSize <= size) {
                            outputStream.write(codeIndexNumber);
                            outputStream.write(codeIndexNumber | mShiftedCableNumber);
                            outputStream.write(midiBytes, curLocation, systemMessageSize);
                            // Fill in the rest of the bytes with 0.
                            outputStream.write(mEmptyBytes, 0, 3 - systemMessageSize);
@@ -236,7 +328,7 @@ public class UsbMidiPacketConverter {
        }

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