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

Commit d1b16fe2 authored by Mike Lockwood's avatar Mike Lockwood
Browse files

Move MIDI utilities for internal use to com.android.internal.midi package

Change-Id: I7393ae1d4bca61667fb6ee809a7aa22c5c48de56
parent fc2d615a
Loading
Loading
Loading
Loading
+236 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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;

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

/**
 * Store arbitrary timestamped events using a Long timestamp.
 * Only one Thread can write into the buffer.
 * And only one Thread can read from the buffer.
 */
public class EventScheduler {
    private static final long NANOS_PER_MILLI = 1000000;

    private final Object lock = new Object();
    private SortedMap<Long, FastEventQueue> mEventBuffer;
    private FastEventQueue mEventPool = null;
    private int mMaxPoolSize = 200;

    public 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 {
        // One thread takes from the beginning of the list.
        volatile SchedulableEvent mFirst;
        // A second thread returns events to the end of the list.
        volatile SchedulableEvent mLast;
        volatile long mEventsAdded;
        volatile long mEventsRemoved;

        FastEventQueue(SchedulableEvent event) {
            mFirst = event;
            mLast = mFirst;
            mEventsAdded = 1;
            mEventsRemoved = 0;
        }

        int size() {
            return (int)(mEventsAdded - mEventsRemoved);
        }

        /**
         * Do not call this unless there is more than one event
         * in the list.
         * @return first event in the list
         */
        public SchedulableEvent remove() {
            // Take first event.
            mEventsRemoved++;
            SchedulableEvent event = mFirst;
            mFirst = event.mNext;
            return event;
        }

        /**
         * @param event
         */
        public void add(SchedulableEvent event) {
            event.mNext = null;
            mLast.mNext = event;
            mLast = event;
            mEventsAdded++;
        }
    }

    /**
     * Base class for events that can be stored in the EventScheduler.
     */
    public static class SchedulableEvent {
        private long mTimestamp;
        private SchedulableEvent mNext = null;

        /**
         * @param timestamp
         */
        public SchedulableEvent(long timestamp) {
            mTimestamp = timestamp;
        }

        /**
         * @return timestamp
         */
        public long getTimestamp() {
            return mTimestamp;
        }

        /**
         * The timestamp should not be modified when the event is in the
         * scheduling buffer.
         */
        public void setTimestamp(long timestamp) {
            mTimestamp = timestamp;
        }
    }

    /**
     * Get an event from the pool.
     * Always leave at least one event in the pool.
     * @return event or null
     */
    public SchedulableEvent removeEventfromPool() {
        SchedulableEvent event = null;
        if (mEventPool != null && (mEventPool.size() > 1)) {
            event = mEventPool.remove();
        }
        return event;
    }

    /**
     * Return events to a pool so they can be reused.
     *
     * @param event
     */
    public void addEventToPool(SchedulableEvent event) {
        if (mEventPool == null) {
            mEventPool = new FastEventQueue(event);
        // If we already have enough items in the pool then just
        // drop the event. This prevents unbounded memory leaks.
        } else if (mEventPool.size() < mMaxPoolSize) {
            mEventPool.add(event);
        }
    }

    /**
     * Add an event to the scheduler. Events with the same time will be
     * processed in order.
     *
     * @param event
     */
    public void add(SchedulableEvent event) {
        synchronized (lock) {
            FastEventQueue list = mEventBuffer.get(event.getTimestamp());
            if (list == null) {
                long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
                        : mEventBuffer.firstKey();
                list = new FastEventQueue(event);
                mEventBuffer.put(event.getTimestamp(), list);
                // 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) {
                    lock.notify();
                }
            } else {
                list.add(event);
            }
        }
    }

    private SchedulableEvent removeNextEventLocked(long lowestTime) {
        SchedulableEvent event;
        FastEventQueue list = mEventBuffer.get(lowestTime);
        // Remove list from tree if this is the last node.
        if ((list.size() == 1)) {
            mEventBuffer.remove(lowestTime);
        }
        event = list.remove();
        return event;
    }

    /**
     * Check to see if any scheduled events are ready to be processed.
     *
     * @param timestamp
     * @return next event or null if none ready
     */
    public SchedulableEvent getNextEvent(long time) {
        SchedulableEvent event = null;
        synchronized (lock) {
            if (!mEventBuffer.isEmpty()) {
                long lowestTime = mEventBuffer.firstKey();
                // Is it time for this list to be processed?
                if (lowestTime <= time) {
                    event = removeNextEventLocked(lowestTime);
                }
            }
        }
        // Log.i(TAG, "getNextEvent: event = " + event);
        return event;
    }

    /**
     * Return the next available event or wait until there is an event ready to
     * be processed. This method assumes that the timestamps are in nanoseconds
     * and that the current time is System.nanoTime().
     *
     * @return event
     * @throws InterruptedException
     */
    public SchedulableEvent waitNextEvent() throws InterruptedException {
        SchedulableEvent event = null;
        while (true) {
            long millisToWait = Integer.MAX_VALUE;
            synchronized (lock) {
                if (!mEventBuffer.isEmpty()) {
                    long now = System.nanoTime();
                    long lowestTime = mEventBuffer.firstKey();
                    // Is it time for the earliest list to be processed?
                    if (lowestTime <= now) {
                        event = removeNextEventLocked(lowestTime);
                        break;
                    } else {
                        // Figure out how long to sleep until next event.
                        long nanosToWait = lowestTime - now;
                        // Add 1 millisecond so we don't wake up before it is
                        // ready.
                        millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI);
                        // Clip 64-bit value to 32-bit max.
                        if (millisToWait > Integer.MAX_VALUE) {
                            millisToWait = Integer.MAX_VALUE;
                        }
                    }
                }
                lock.wait((int) millisToWait);
            }
        }
        return event;
    }
}
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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;

/**
 * MIDI related constants and static methods.
 */
public class MidiConstants {
    public static final byte STATUS_COMMAND_MASK = (byte) 0xF0;
    public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F;

    // Channel voice messages.
    public static final byte STATUS_NOTE_OFF = (byte) 0x80;
    public static final byte STATUS_NOTE_ON = (byte) 0x90;
    public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0;
    public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0;
    public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0;
    public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0;
    public static final byte STATUS_PITCH_BEND = (byte) 0xE0;

    // System Common Messages.
    public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0;
    public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1;
    public static final byte STATUS_SONG_POSITION = (byte) 0xF2;
    public static final byte STATUS_SONG_SELECT = (byte) 0xF3;
    public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6;
    public static final byte STATUS_END_SYSEX = (byte) 0xF7;

    // System Real-Time Messages
    public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8;
    public static final byte STATUS_START = (byte) 0xFA;
    public static final byte STATUS_CONTINUE = (byte) 0xFB;
    public static final byte STATUS_STOP = (byte) 0xFC;
    public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE;
    public static final byte STATUS_RESET = (byte) 0xFF;

    /** Number of bytes in a message nc from 8c to Ec */
    public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 };

    /** Number of bytes in a message Fn from F0 to FF */
    public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, 1, 1 };

    /********************************************************************/

    public static int getBytesPerMessage(int command) {
        if ((command < 0x80) || (command > 0xFF)) {
            return 0;
        } else if (command >= 0xF0) {
            return SYSTEM_BYTE_LENGTHS[command & 0x0F];
        } else {
            return CHANNEL_BYTE_LENGTHS[(command >> 4) - 8];
        }
    }

    /**
     * @param msg
     * @param offset
     * @param count
     * @return true if the entire message is ActiveSensing commands
     */
    public static boolean isAllActiveSensing(byte[] msg, int offset,
            int count) {
        // Count bytes that are not active sensing.
        int goodBytes = 0;
        for (int i = 0; i < count; i++) {
            byte b = msg[offset + i];
            if (b != MidiConstants.STATUS_ACTIVE_SENSING) {
                goodBytes++;
            }
        }
        return (goodBytes == 0);
    }
}
+12 −10
Original line number Diff line number Diff line
@@ -14,19 +14,20 @@
 * limitations under the License.
 */

package android.media.midi;
package com.android.internal.midi;

import android.media.midi.MidiReceiver;
import android.media.midi.MidiSender;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
 * This class subclasses {@link MidiReceiver} and dispatches any data it receives
 * Utility class for dispatching MIDI data to a list of {@link android.media.midi.MidiReceiver}s.
 * This class subclasses {@link android.media.midi.MidiReceiver} and dispatches any data it receives
 * to its receiver list. Any receivers that throw an exception upon receiving data will
 * be automatically removed from the receiver list, but no IOException will be returned
 * from the dispatcher's {@link #onReceive} in that case.
 *
 * @hide
 * from the dispatcher's {@link android.media.midi.MidiReceiver#onReceive} in that case.
 */
public final class MidiDispatcher extends MidiReceiver {

@@ -35,7 +36,7 @@ public final class MidiDispatcher extends MidiReceiver {

    private final MidiSender mSender = new MidiSender() {
        /**
         * Called to connect a {@link MidiReceiver} to the sender
         * Called to connect a {@link android.media.midi.MidiReceiver} to the sender
         *
         * @param receiver the receiver to connect
         */
@@ -44,7 +45,7 @@ public final class MidiDispatcher extends MidiReceiver {
        }

        /**
         * Called to disconnect a {@link MidiReceiver} from the sender
         * Called to disconnect a {@link android.media.midi.MidiReceiver} from the sender
         *
         * @param receiver the receiver to disconnect
         */
@@ -54,7 +55,7 @@ public final class MidiDispatcher extends MidiReceiver {
    };

    /**
     * Returns the number of {@link MidiReceiver}s this dispatcher contains.
     * Returns the number of {@link android.media.midi.MidiReceiver}s this dispatcher contains.
     * @return the number of receivers
     */
    public int getReceiverCount() {
@@ -62,7 +63,8 @@ public final class MidiDispatcher extends MidiReceiver {
    }

    /**
     * Returns a {@link MidiSender} which is used to add and remove {@link MidiReceiver}s
     * Returns a {@link android.media.midi.MidiSender} which is used to add and remove
     * {@link android.media.midi.MidiReceiver}s
     * to the dispatcher's receiver list.
     * @return the dispatcher's MidiSender
     */
+119 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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;

import android.media.midi.MidiReceiver;

import java.io.IOException;

/**
 * Add MIDI Events to an EventScheduler
 */
public class MidiEventScheduler extends EventScheduler {
    private static final String TAG = "MidiEventScheduler";
    // Maintain a pool of scheduled events to reduce memory allocation.
    // This pool increases performance by about 14%.
    private final static int POOL_EVENT_SIZE = 16;
    private MidiReceiver mReceiver = new SchedulingReceiver();

    private class SchedulingReceiver extends MidiReceiver
    {
        /**
         * Store these bytes in the EventScheduler to be delivered at the specified
         * time.
         */
        @Override
        public void onReceive(byte[] msg, int offset, int count, long timestamp)
                throws IOException {
            MidiEvent event = createScheduledEvent(msg, offset, count, timestamp);
            if (event != null) {
                add(event);
            }
        }
    }

    public static class MidiEvent extends SchedulableEvent {
        public int count = 0;
        public byte[] data;

        private MidiEvent(int count) {
            super(0);
            data = new byte[count];
        }

        private MidiEvent(byte[] msg, int offset, int count, long timestamp) {
            super(timestamp);
            data = new byte[count];
            System.arraycopy(msg, offset, data, 0, count);
            this.count = count;
        }

        @Override
        public String toString() {
            String text = "Event: ";
            for (int i = 0; i < count; i++) {
                text += data[i] + ", ";
            }
            return text;
        }
    }

    /**
     * Create an event that contains the message.
     */
    private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
            long timestamp) {
        MidiEvent event;
        if (count > POOL_EVENT_SIZE) {
            event = new MidiEvent(msg, offset, count, timestamp);
        } else {
            event = (MidiEvent) removeEventfromPool();
            if (event == null) {
                event = new MidiEvent(POOL_EVENT_SIZE);
            }
            System.arraycopy(msg, offset, event.data, 0, count);
            event.count = count;
            event.setTimestamp(timestamp);
        }
        return event;
    }

    /**
     * Return events to a pool so they can be reused.
     *
     * @param event
     */
    @Override
    public void addEventToPool(SchedulableEvent event) {
        // Make sure the event is suitable for the pool.
        if (event instanceof MidiEvent) {
            MidiEvent midiEvent = (MidiEvent) event;
            if (midiEvent.data.length == POOL_EVENT_SIZE) {
                super.addEventToPool(event);
            }
        }
    }

    /**
     * This MidiReceiver will write date to the scheduling buffer.
     * @return the MidiReceiver
     */
    public MidiReceiver getReceiver() {
        return mReceiver;
    }

}
+92 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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;

import android.media.midi.MidiReceiver;

import java.io.IOException;

/**
 * Convert stream of bytes to discrete messages.
 *
 * Parses the incoming bytes and then posts individual messages to the receiver
 * specified in the constructor. Short messages of 1-3 bytes will be complete.
 * System Exclusive messages may be posted in pieces.
 *
 * Resolves Running Status and
 * interleaved System Real-Time messages.
 */
public class MidiFramer extends MidiReceiver {

    public String TAG = "MidiFramer";
    private MidiReceiver mReceiver;
    private byte[] mBuffer = new byte[3];
    private int mCount;
    private int mRunningStatus;
    private int mNeeded;

    public MidiFramer(MidiReceiver receiver) {
        mReceiver = receiver;
    }

    public static String formatMidiData(byte[] data, int offset, int count) {
        String text = "MIDI+" + offset + " : ";
        for (int i = 0; i < count; i++) {
            text += String.format("0x%02X, ", data[offset + i]);
        }
        return text;
    }

    /*
     * @see android.midi.MidiReceiver#onPost(byte[], int, int, long)
     */
    @Override
    public void onReceive(byte[] data, int offset, int count, long timestamp)
            throws IOException {
        // Log.i(TAG, formatMidiData(data, offset, count));
        for (int i = 0; i < count; i++) {
            int b = data[offset] & 0xFF;
            if (b >= 0x80) { // status byte?
                if (b < 0xF0) { // channel message?
                    mRunningStatus = (byte) b;
                    mCount = 1;
                    mNeeded = MidiConstants.getBytesPerMessage(b) - 1;
                } else if (b < 0xF8) { // system common?
                    mBuffer[0] = (byte) b;
                    mRunningStatus = 0;
                    mCount = 1;
                    mNeeded = MidiConstants.getBytesPerMessage(b) - 1;
                } else { // real-time?
                    // Single byte message interleaved with other data.
                    mReceiver.sendWithTimestamp(data, offset, 1, timestamp);
                }
            } else { // data byte
                mBuffer[mCount++] = (byte) b;
                if (--mNeeded == 0) {
                    if (mRunningStatus != 0) {
                        mBuffer[0] = (byte) mRunningStatus;
                    }
                    mReceiver.sendWithTimestamp(mBuffer, 0, mCount, timestamp);
                    mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1;
                    mCount = 1;
                }
            }
            ++offset;
        }
    }

}
Loading