blob: 3ef9f4c4a7381cb72105f0646c9f5d58eeaacaf4 [file] [log] [blame]
/*
* 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<byte[]> mFailedToSendQueue = new ArrayDeque<byte[]>();
// 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;
}
}
}
}