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

Commit e9e02d52 authored by Mike Lockwood's avatar Mike Lockwood Committed by Android (Google) Code Review
Browse files

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

parents 27e3514e d1b16fe2
Loading
Loading
Loading
Loading
+236 −0
Original line number Original line 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 Original line 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 Original line Diff line number Diff line
@@ -14,19 +14,20 @@
 * limitations under the License.
 * 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.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArrayList;


/**
/**
 * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
 * Utility class for dispatching MIDI data to a list of {@link android.media.midi.MidiReceiver}s.
 * This class subclasses {@link MidiReceiver} and dispatches any data it receives
 * 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
 * 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
 * be automatically removed from the receiver list, but no IOException will be returned
 * from the dispatcher's {@link #onReceive} in that case.
 * from the dispatcher's {@link android.media.midi.MidiReceiver#onReceive} in that case.
 *
 * @hide
 */
 */
public final class MidiDispatcher extends MidiReceiver {
public final class MidiDispatcher extends MidiReceiver {


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


    private final MidiSender mSender = new MidiSender() {
    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
         * @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
         * @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
     * @return the number of receivers
     */
     */
    public int getReceiverCount() {
    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.
     * to the dispatcher's receiver list.
     * @return the dispatcher's MidiSender
     * @return the dispatcher's MidiSender
     */
     */
+119 −0
Original line number Original line 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 Original line 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