/* * 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 com.android.internal.midi.MidiConstants; import com.android.internal.midi.MidiFramer; import java.io.IOException; /** * 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; private boolean mWritePending; private final Object mLock = new Object(); // 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) { 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 > mAccumulationBuffer.length) { // 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 == mAccumulationBuffer.length) { // write out our data if there is no more room // if necessary, block until previous packet is sent flushLocked(true); appendHeader(milliTimestamp); } int copy = mAccumulationBuffer.length - 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 > mAccumulationBuffer.length) { // 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]; } @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) { mPacketReceiver.writePacket(mAccumulationBuffer, mAccumulatedBytes); mAccumulatedBytes = 0; mPacketTimestamp = 0; mRunningStatus = 0; mWritePending = true; } } }