/* * 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.bluetoothmidiservice; import android.media.midi.MidiReceiver; import android.util.Log; import com.android.internal.midi.MidiConstants; import com.android.internal.midi.MidiFramer; import java.io.IOException; import java.util.ArrayDeque; import java.util.Queue; /** * This class accumulates MIDI messages to form a MIDI packet. */ public class BluetoothPacketEncoder extends PacketEncoder { private static final String TAG = "BluetoothPacketEncoder"; private static final long MILLISECOND_NANOS = 1000000L; // mask for generating 13 bit timestamps private static final int MILLISECOND_MASK = 0x1FFF; private final PacketReceiver mPacketReceiver; // buffer for accumulating messages to write private final byte[] mAccumulationBuffer; // number of bytes currently in mAccumulationBuffer private int mAccumulatedBytes; // timestamp for first message in current packet private int mPacketTimestamp; // current running status, or zero if none private byte mRunningStatus; // max size of a packet private int mMaxPacketSize; private boolean mWritePending; private final Object mLock = new Object(); private Queue mFailedToSendQueue = new ArrayDeque(); // This receives normalized data from mMidiFramer and accumulates it into a packet buffer private final MidiReceiver mFramedDataReceiver = new MidiReceiver() { @Override public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { synchronized (mLock) { flushFailedToSendQueueLocked(); int milliTimestamp = (int)(timestamp / MILLISECOND_NANOS) & MILLISECOND_MASK; byte status = msg[offset]; boolean isSysExStart = (status == MidiConstants.STATUS_SYSTEM_EXCLUSIVE); // Because of the MidiFramer, if it is not a status byte then it // must be a continuation. boolean isSysExContinuation = ((status & 0x80) == 0); int bytesNeeded; if (isSysExStart || isSysExContinuation) { // SysEx messages can be split into multiple packets bytesNeeded = 1; } else { bytesNeeded = count; } // Status bytes must be preceded by a timestamp boolean needsTimestamp = (status != mRunningStatus) || (milliTimestamp != mPacketTimestamp); if (isSysExStart) { // SysEx start byte must be preceded by a timestamp needsTimestamp = true; } else if (isSysExContinuation) { // SysEx continuation packets must not have timestamp byte needsTimestamp = false; } if (needsTimestamp) bytesNeeded++; // add one for timestamp byte if (status == mRunningStatus) bytesNeeded--; // subtract one for status byte if (mAccumulatedBytes + bytesNeeded > mMaxPacketSize) { // write out our data if there is no more room // if necessary, block until previous packet is sent flushLocked(true); } // write the header if necessary if (appendHeader(milliTimestamp)) { needsTimestamp = !isSysExContinuation; } // write new timestamp byte if necessary if (needsTimestamp) { // timestamp byte with bits 0 - 6 of timestamp mAccumulationBuffer[mAccumulatedBytes++] = (byte)(0x80 | (milliTimestamp & 0x7F)); mPacketTimestamp = milliTimestamp; } if (isSysExStart || isSysExContinuation) { // MidiFramer will end the packet with SysEx End if there is one in the buffer boolean hasSysExEnd = (msg[offset + count - 1] == MidiConstants.STATUS_END_SYSEX); int remaining = (hasSysExEnd ? count - 1 : count); while (remaining > 0) { if (mAccumulatedBytes == mMaxPacketSize) { // write out our data if there is no more room // if necessary, block until previous packet is sent flushLocked(true); appendHeader(milliTimestamp); } int copy = mMaxPacketSize - mAccumulatedBytes; if (copy > remaining) copy = remaining; System.arraycopy(msg, offset, mAccumulationBuffer, mAccumulatedBytes, copy); mAccumulatedBytes += copy; offset += copy; remaining -= copy; } if (hasSysExEnd) { // SysEx End command must be preceeded by a timestamp byte if (mAccumulatedBytes + 2 > mMaxPacketSize) { // write out our data if there is no more room // if necessary, block until previous packet is sent flushLocked(true); appendHeader(milliTimestamp); } mAccumulationBuffer[mAccumulatedBytes++] = (byte)(0x80 | (milliTimestamp & 0x7F)); mAccumulationBuffer[mAccumulatedBytes++] = MidiConstants.STATUS_END_SYSEX; } } else { // Non-SysEx message if (status != mRunningStatus) { mAccumulationBuffer[mAccumulatedBytes++] = status; if (MidiConstants.allowRunningStatus(status)) { mRunningStatus = status; } else if (MidiConstants.cancelsRunningStatus(status)) { mRunningStatus = 0; } } // now copy data bytes int dataLength = count - 1; System.arraycopy(msg, offset + 1, mAccumulationBuffer, mAccumulatedBytes, dataLength); mAccumulatedBytes += dataLength; } // write the packet if possible, but do not block flushLocked(false); } } }; private boolean appendHeader(int milliTimestamp) { // write header if we are starting a new packet if (mAccumulatedBytes == 0) { // header byte with timestamp bits 7 - 12 mAccumulationBuffer[mAccumulatedBytes++] = (byte)(0x80 | ((milliTimestamp >> 7) & 0x3F)); mPacketTimestamp = milliTimestamp; return true; } else { return false; } } // MidiFramer for normalizing incoming data private final MidiFramer mMidiFramer = new MidiFramer(mFramedDataReceiver); public BluetoothPacketEncoder(PacketReceiver packetReceiver, int maxPacketSize) { mPacketReceiver = packetReceiver; mAccumulationBuffer = new byte[maxPacketSize]; setMaxPacketSize(maxPacketSize); } /** * Dynamically sets the maximum packet size */ public void setMaxPacketSize(int maxPacketSize) { synchronized (mLock) { mMaxPacketSize = Math.min(maxPacketSize, mAccumulationBuffer.length); } } @Override public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { // normalize the data by passing it through a MidiFramer first mMidiFramer.send(msg, offset, count, timestamp); } @Override public void writeComplete() { synchronized (mLock) { mWritePending = false; flushLocked(false); mLock.notify(); } } private void flushLocked(boolean canBlock) { if (mWritePending && !canBlock) { return; } while (mWritePending && mAccumulatedBytes > 0) { try { mLock.wait(); } catch (InterruptedException e) { // try again continue; } } if (mAccumulatedBytes > 0) { boolean wasSendSuccessful = mPacketReceiver.writePacket(mAccumulationBuffer, mAccumulatedBytes); if (!wasSendSuccessful) { byte[] failedBuffer = new byte[mAccumulatedBytes]; System.arraycopy(mAccumulationBuffer, 0, failedBuffer, 0, mAccumulatedBytes); mFailedToSendQueue.add(failedBuffer); Log.d(TAG, "Enqueued data into failed queue."); } mAccumulatedBytes = 0; mPacketTimestamp = 0; mRunningStatus = 0; mWritePending = wasSendSuccessful; } } private void flushFailedToSendQueueLocked() { while (!mFailedToSendQueue.isEmpty()) { while (mWritePending) { try { mLock.wait(); } catch (InterruptedException e) { // try again continue; } } byte[] currentBuffer = mFailedToSendQueue.element(); boolean wasSendSuccessful = mPacketReceiver.writePacket(currentBuffer, currentBuffer.length); mWritePending = wasSendSuccessful; if (wasSendSuccessful) { mFailedToSendQueue.remove(); Log.d(TAG, "Dequeued data from failed queue."); } else { return; } } } }