diff options
Diffstat (limited to 'media')
12 files changed, 824 insertions, 1 deletions
diff --git a/media/java/android/media/midi/IMidiDeviceServer.aidl b/media/java/android/media/midi/IMidiDeviceServer.aidl index 642078a..96d12fd 100644 --- a/media/java/android/media/midi/IMidiDeviceServer.aidl +++ b/media/java/android/media/midi/IMidiDeviceServer.aidl @@ -16,6 +16,7 @@ package android.media.midi; +import android.media.midi.MidiDeviceInfo; import android.os.ParcelFileDescriptor; /** @hide */ @@ -27,4 +28,6 @@ interface IMidiDeviceServer // connects the input port pfd to the specified output port void connectPorts(IBinder token, in ParcelFileDescriptor pfd, int outputPortNumber); + + MidiDeviceInfo getDeviceInfo(); } diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java index bc85f92..a316a44 100644 --- a/media/java/android/media/midi/MidiDeviceServer.java +++ b/media/java/android/media/midi/MidiDeviceServer.java @@ -252,6 +252,11 @@ public final class MidiDeviceServer implements Closeable { mPortClients.put(token, client); } } + + @Override + public MidiDeviceInfo getDeviceInfo() { + return mDeviceInfo; + } }; /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers, @@ -279,6 +284,10 @@ public final class MidiDeviceServer implements Closeable { return mServer; } + public IBinder asBinder() { + return mServer.asBinder(); + } + /* package */ void setDeviceInfo(MidiDeviceInfo deviceInfo) { if (mDeviceInfo != null) { throw new IllegalStateException("setDeviceInfo should only be called once"); diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java index d62b2dc..0ba1744 100644 --- a/media/java/android/media/midi/MidiManager.java +++ b/media/java/android/media/midi/MidiManager.java @@ -16,6 +16,7 @@ package android.media.midi; +import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -42,6 +43,24 @@ import java.util.HashMap; public final class MidiManager { private static final String TAG = "MidiManager"; + /** + * Intent for starting BluetoothMidiService + * @hide + */ + public static final String BLUETOOTH_MIDI_SERVICE_INTENT = + "android.media.midi.BluetoothMidiService"; + + /** + * BluetoothMidiService package name + */ + private static final String BLUETOOTH_MIDI_SERVICE_PACKAGE = "com.android.bluetoothmidiservice"; + + /** + * BluetoothMidiService class name + */ + private static final String BLUETOOTH_MIDI_SERVICE_CLASS = + "com.android.bluetoothmidiservice.BluetoothMidiService"; + private final Context mContext; private final IMidiManager mService; private final IBinder mToken = new Binder(); @@ -145,6 +164,19 @@ public final class MidiManager { } /** + * Callback class used for receiving the results of {@link #openBluetoothDevice} + */ + abstract public static class BluetoothOpenCallback { + /** + * Called to respond to a {@link #openBluetoothDevice} request + * + * @param bluetoothDevice the {@link android.bluetooth.BluetoothDevice} to open + * @param device a {@link MidiDevice} for opened device, or null if opening failed + */ + abstract public void onDeviceOpened(BluetoothDevice bluetoothDevice, MidiDevice device); + } + + /** * @hide */ public MidiManager(Context context, IMidiManager service) { @@ -214,6 +246,19 @@ public final class MidiManager { } } + private void sendBluetoothDeviceResponse(final BluetoothDevice bluetoothDevice, + final MidiDevice device, final BluetoothOpenCallback callback, Handler handler) { + if (handler != null) { + handler.post(new Runnable() { + @Override public void run() { + callback.onDeviceOpened(bluetoothDevice, device); + } + }); + } else { + callback.onDeviceOpened(bluetoothDevice, device); + } + } + /** * Opens a MIDI device for reading and writing. * @@ -260,7 +305,7 @@ public final class MidiManager { // return immediately to avoid calling sendOpenDeviceResponse below return; } else { - Log.e(TAG, "Unable to bind service: " + intent); + Log.e(TAG, "Unable to bind service: " + intent); } } } else { @@ -272,6 +317,51 @@ public final class MidiManager { sendOpenDeviceResponse(deviceInfo, device, callback, handler); } + /** + * Opens a Bluetooth MIDI device for reading and writing. + * + * @param bluetoothDevice a {@link android.bluetooth.BluetoothDevice} to open as a MIDI device + * @param callback a {@link MidiManager.BluetoothOpenCallback} to be called to receive the + * result + * @param handler the {@link android.os.Handler Handler} that will be used for delivering + * the result. If handler is null, then the thread used for the + * callback is unspecified. + */ + public void openBluetoothDevice(final BluetoothDevice bluetoothDevice, + final BluetoothOpenCallback callback, final Handler handler) { + Intent intent = new Intent(BLUETOOTH_MIDI_SERVICE_INTENT); + intent.setComponent(new ComponentName(BLUETOOTH_MIDI_SERVICE_PACKAGE, + BLUETOOTH_MIDI_SERVICE_CLASS)); + intent.putExtra("device", bluetoothDevice); + if (!mContext.bindService(intent, + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + IMidiDeviceServer server = + IMidiDeviceServer.Stub.asInterface(binder); + try { + // fetch MidiDeviceInfo from the server + MidiDeviceInfo deviceInfo = server.getDeviceInfo(); + MidiDevice device = new MidiDevice(deviceInfo, server, mContext, this); + sendBluetoothDeviceResponse(bluetoothDevice, device, callback, handler); + } catch (RemoteException e) { + Log.e(TAG, "remote exception in onServiceConnected"); + sendBluetoothDeviceResponse(bluetoothDevice, null, callback, handler); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // FIXME - anything to do here? + } + }, + Context.BIND_AUTO_CREATE)) + { + Log.e(TAG, "Unable to bind service: " + intent); + sendBluetoothDeviceResponse(bluetoothDevice, null, callback, handler); + } + } + /** @hide */ public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, diff --git a/media/packages/BluetoothMidiService/Android.mk b/media/packages/BluetoothMidiService/Android.mk new file mode 100644 index 0000000..2c9c3c5 --- /dev/null +++ b/media/packages/BluetoothMidiService/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := BluetoothMidiService +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml new file mode 100644 index 0000000..15aa581 --- /dev/null +++ b/media/packages/BluetoothMidiService/AndroidManifest.xml @@ -0,0 +1,17 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.bluetoothmidiservice" + > + + <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> + <uses-feature android:name="android.software.midi" android:required="true"/> + <uses-permission android:name="android.permission.BLUETOOTH"/> + + <application + android:label="@string/app_name"> + <service android:name="BluetoothMidiService"> + <intent-filter> + <action android:name="android.media.midi.BluetoothMidiService" /> + </intent-filter> + </service> + </application> +</manifest> diff --git a/media/packages/BluetoothMidiService/res/values/strings.xml b/media/packages/BluetoothMidiService/res/values/strings.xml new file mode 100644 index 0000000..c98e56c --- /dev/null +++ b/media/packages/BluetoothMidiService/res/values/strings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources> + <string name="app_name">Bluetooth MIDI Service</string> +</resources> diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java new file mode 100644 index 0000000..8d194e5 --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java @@ -0,0 +1,276 @@ +/* + * 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.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.media.midi.MidiReceiver; +import android.media.midi.MidiManager; +import android.media.midi.MidiDeviceServer; +import android.media.midi.MidiDeviceInfo; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import com.android.internal.midi.MidiEventScheduler; +import com.android.internal.midi.MidiEventScheduler.MidiEvent; + +import libcore.io.IoUtils; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +/** + * Class used to implement a Bluetooth MIDI device. + */ +public final class BluetoothMidiDevice { + + private static final String TAG = "BluetoothMidiDevice"; + + private static final int MAX_PACKET_SIZE = 20; + + // Bluetooth MIDI Gatt service UUID + private static final UUID MIDI_SERVICE = UUID.fromString( + "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"); + // Bluetooth MIDI Gatt characteristic UUID + private static final UUID MIDI_CHARACTERISTIC = UUID.fromString( + "7772E5DB-3868-4112-A1A9-F2669D106BF3"); + // Descriptor UUID for enabling characteristic changed notifications + private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString( + "00002902-0000-1000-8000-00805f9b34fb"); + + private final BluetoothDevice mBluetoothDevice; + private final BluetoothMidiService mService; + private final MidiManager mMidiManager; + private MidiReceiver mOutputReceiver; + private final MidiEventScheduler mEventScheduler = new MidiEventScheduler(); + + private MidiDeviceServer mDeviceServer; + private BluetoothGatt mBluetoothGatt; + + private BluetoothGattCharacteristic mCharacteristic; + + // PacketReceiver for receiving formatted packets from our BluetoothPacketEncoder + private final PacketReceiver mPacketReceiver = new PacketReceiver(); + + private final BluetoothPacketEncoder mPacketEncoder + = new BluetoothPacketEncoder(mPacketReceiver, MAX_PACKET_SIZE); + + private final BluetoothPacketDecoder mPacketDecoder + = new BluetoothPacketDecoder(MAX_PACKET_SIZE); + + private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, + int newState) { + String intentAction; + if (newState == BluetoothProfile.STATE_CONNECTED) { + Log.i(TAG, "Connected to GATT server."); + Log.i(TAG, "Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.i(TAG, "Disconnected from GATT server."); + // FIXME synchronize? + close(); + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + List<BluetoothGattService> services = mBluetoothGatt.getServices(); + for (BluetoothGattService service : services) { + if (MIDI_SERVICE.equals(service.getUuid())) { + Log.d(TAG, "found MIDI_SERVICE"); + List<BluetoothGattCharacteristic> characteristics + = service.getCharacteristics(); + for (BluetoothGattCharacteristic characteristic : characteristics) { + if (MIDI_CHARACTERISTIC.equals(characteristic.getUuid())) { + Log.d(TAG, "found MIDI_CHARACTERISTIC"); + mCharacteristic = characteristic; + + // Specification says to read the characteristic first and then + // switch to receiving notifications + mBluetoothGatt.readCharacteristic(characteristic); + break; + } + } + break; + } + } + } else { + Log.w(TAG, "onServicesDiscovered received: " + status); + // FIXME - report error back to client? + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + Log.d(TAG, "onCharacteristicRead " + status); + + // switch to receiving notifications after initial characteristic read + mBluetoothGatt.setCharacteristicNotification(characteristic, true); + + BluetoothGattDescriptor descriptor = characteristic.getDescriptor( + CLIENT_CHARACTERISTIC_CONFIG); + // FIXME null check + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGatt.writeDescriptor(descriptor); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + Log.d(TAG, "onCharacteristicWrite " + status); + mPacketEncoder.writeComplete(); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { +// logByteArray("Received ", characteristic.getValue(), 0, +// characteristic.getValue().length); + mPacketDecoder.decodePacket(characteristic.getValue(), mOutputReceiver); + } + }; + + // This receives MIDI data that has already been passed through our MidiEventScheduler + // and has been normalized by our MidiFramer. + + private class PacketReceiver implements PacketEncoder.PacketReceiver { + // buffers of every possible packet size + private final byte[][] mWriteBuffers; + + public PacketReceiver() { + // Create buffers of every possible packet size + mWriteBuffers = new byte[MAX_PACKET_SIZE + 1][]; + for (int i = 0; i <= MAX_PACKET_SIZE; i++) { + mWriteBuffers[i] = new byte[i]; + } + } + + @Override + public void writePacket(byte[] buffer, int count) { + if (mCharacteristic == null) { + Log.w(TAG, "not ready to send packet yet"); + return; + } + byte[] writeBuffer = mWriteBuffers[count]; + System.arraycopy(buffer, 0, writeBuffer, 0, count); + mCharacteristic.setValue(writeBuffer); +// logByteArray("Sent ", mCharacteristic.getValue(), 0, +// mCharacteristic.getValue().length); + mBluetoothGatt.writeCharacteristic(mCharacteristic); + } + } + + public BluetoothMidiDevice(Context context, BluetoothDevice device, + BluetoothMidiService service) { + mBluetoothDevice = device; + mService = service; + + mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback); + + mMidiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); + + Bundle properties = new Bundle(); + properties.putString(MidiDeviceInfo.PROPERTY_NAME, mBluetoothGatt.getDevice().getName()); + properties.putParcelable(MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE, + mBluetoothGatt.getDevice()); + + MidiReceiver[] inputPortReceivers = new MidiReceiver[1]; + inputPortReceivers[0] = mEventScheduler.getReceiver(); + + mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1, + null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, null); + + mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0]; + + // This thread waits for outgoing messages from our MidiEventScheduler + // And forwards them to our MidiFramer to be prepared to send via Bluetooth. + new Thread("BluetoothMidiDevice " + mBluetoothDevice) { + @Override + public void run() { + while (true) { + MidiEvent event; + try { + event = (MidiEvent)mEventScheduler.waitNextEvent(); + } catch (InterruptedException e) { + // try again + continue; + } + if (event == null) { + break; + } + try { + mPacketEncoder.sendWithTimestamp(event.data, 0, event.count, + event.getTimestamp()); + } catch (IOException e) { + Log.e(TAG, "mPacketAccumulator.sendWithTimestamp failed", e); + } + mEventScheduler.addEventToPool(event); + } + Log.d(TAG, "BluetoothMidiDevice thread exit"); + } + }.start(); + } + + void close() { + mEventScheduler.close(); + if (mDeviceServer != null) { + IoUtils.closeQuietly(mDeviceServer); + mDeviceServer = null; + mService.deviceClosed(mBluetoothDevice); + } + if (mBluetoothGatt != null) { + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + } + + public IBinder getBinder() { + return mDeviceServer.asBinder(); + } + + private static void logByteArray(String prefix, byte[] value, int offset, int count) { + StringBuilder builder = new StringBuilder(prefix); + for (int i = offset; i < count; i++) { + String hex = Integer.toHexString(value[i]); + int length = hex.length(); + if (length == 1) { + hex = "0x" + hex; + } else { + hex = hex.substring(length - 2, length); + } + builder.append(hex); + if (i != value.length - 1) { + builder.append(", "); + } + } + Log.d(TAG, builder.toString()); + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiService.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiService.java new file mode 100644 index 0000000..fbde2b4 --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiService.java @@ -0,0 +1,61 @@ +/* + * 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.app.Service; +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.media.midi.MidiManager; +import android.os.IBinder; +import android.util.Log; + +import java.util.HashMap; + +public class BluetoothMidiService extends Service { + private static final String TAG = "BluetoothMidiService"; + + // BluetoothMidiDevices keyed by BluetoothDevice + private final HashMap<BluetoothDevice,BluetoothMidiDevice> mDeviceServerMap + = new HashMap<BluetoothDevice,BluetoothMidiDevice>(); + + @Override + public IBinder onBind(Intent intent) { + if (MidiManager.BLUETOOTH_MIDI_SERVICE_INTENT.equals(intent.getAction())) { + BluetoothDevice bluetoothDevice = (BluetoothDevice)intent.getParcelableExtra("device"); + if (bluetoothDevice == null) { + Log.e(TAG, "no BluetoothDevice in onBind intent"); + return null; + } + + BluetoothMidiDevice device; + synchronized (mDeviceServerMap) { + device = mDeviceServerMap.get(bluetoothDevice); + if (device == null) { + device = new BluetoothMidiDevice(this, bluetoothDevice, this); + } + } + return device.getBinder(); + } + return null; + } + + void deviceClosed(BluetoothDevice device) { + synchronized (mDeviceServerMap) { + mDeviceServerMap.remove(device); + } + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketDecoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketDecoder.java new file mode 100644 index 0000000..c5bfb5f --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketDecoder.java @@ -0,0 +1,106 @@ +/* + * 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 java.io.IOException; + +/** + * This is an abstract base class that decodes a packet buffer and passes it to a + * {@link android.media.midi.MidiReceiver} + */ +public class BluetoothPacketDecoder extends PacketDecoder { + + private static final String TAG = "BluetoothPacketDecoder"; + + private final byte[] mBuffer; + + private final int TIMESTAMP_MASK_HIGH = 0x1F80; + private final int TIMESTAMP_MASK_LOW = 0x7F; + private final int HEADER_TIMESTAMP_MASK = 0x3F; + + public BluetoothPacketDecoder(int maxPacketSize) { + mBuffer = new byte[maxPacketSize]; + } + + @Override + public void decodePacket(byte[] buffer, MidiReceiver receiver) { + int length = buffer.length; + + // NOTE his code allows running status across packets, + // although the specification does not allow that. + + if (length < 1) { + Log.e(TAG, "empty packet"); + return; + } + byte header = buffer[0]; + if ((header & 0xC0) != 0x80) { + Log.e(TAG, "packet does not start with header"); + return; + } + + // shift bits 0 - 5 to bits 7 - 12 + int timestamp = (header & HEADER_TIMESTAMP_MASK) << 7; + boolean lastWasTimestamp = false; + int dataCount = 0; + int previousLowTimestamp = 0; + + // iterate through the rest of the packet, separating MIDI data from timestamps + for (int i = 1; i < buffer.length; i++) { + byte b = buffer[i]; + + if ((b & 0x80) != 0 && !lastWasTimestamp) { + lastWasTimestamp = true; + int lowTimestamp = b & TIMESTAMP_MASK_LOW; + int newTimestamp = (timestamp & TIMESTAMP_MASK_HIGH) | lowTimestamp; + if (lowTimestamp < previousLowTimestamp) { + newTimestamp = (newTimestamp + 0x0080) & TIMESTAMP_MASK_HIGH; + } + previousLowTimestamp = lowTimestamp; + + if (newTimestamp != timestamp) { + if (dataCount > 0) { + // send previous message separately since it has a different timestamp + try { + // FIXME use sendWithTimestamp + receiver.send(mBuffer, 0, dataCount); + } catch (IOException e) { + // ??? + } + dataCount = 0; + } + } + timestamp = newTimestamp; + } else { + lastWasTimestamp = false; + mBuffer[dataCount++] = b; + } + } + + if (dataCount > 0) { + try { + // FIXME use sendWithTimestamp + receiver.send(mBuffer, 0, dataCount); + } catch (IOException e) { + // ??? + } + } + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java new file mode 100644 index 0000000..463edcf --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java @@ -0,0 +1,157 @@ +/* + * 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 int 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 onReceive(byte[] msg, int offset, int count, long timestamp) + throws IOException { + + int milliTimestamp = (int)(timestamp / MILLISECOND_NANOS) & MILLISECOND_MASK; + int status = msg[0] & 0xFF; + + synchronized (mLock) { + boolean needsTimestamp = (milliTimestamp != mPacketTimestamp); + int bytesNeeded = count; + 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 header if we are starting a new packet + if (mAccumulatedBytes == 0) { + // header byte with timestamp bits 7 - 12 + mAccumulationBuffer[mAccumulatedBytes++] = (byte)(0x80 | (milliTimestamp >> 7)); + mPacketTimestamp = milliTimestamp; + needsTimestamp = true; + } + + // write new timestamp byte and status byte if necessary + if (needsTimestamp) { + // timestamp byte with bits 0 - 6 of timestamp + mAccumulationBuffer[mAccumulatedBytes++] = + (byte)(0x80 | (milliTimestamp & 0x7F)); + mPacketTimestamp = milliTimestamp; + } + + if (status != mRunningStatus) { + mAccumulationBuffer[mAccumulatedBytes++] = (byte)status; + if (MidiConstants.allowRunningStatus(status)) { + mRunningStatus = status; + } else if (MidiConstants.allowRunningStatus(status)) { + mRunningStatus = 0; + } + } + + // now copy data bytes + int dataLength = count - 1; + System.arraycopy(msg, 1, mAccumulationBuffer, mAccumulatedBytes, dataLength); + // FIXME - handle long SysEx properly + mAccumulatedBytes += dataLength; + + // write the packet if possible, but do not block + flushLocked(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 onReceive(byte[] msg, int offset, int count, long timestamp) + throws IOException { + // normalize the data by passing it through a MidiFramer first + mMidiFramer.sendWithTimestamp(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; + } + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketDecoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketDecoder.java new file mode 100644 index 0000000..da4b63a --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketDecoder.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * This is an abstract base class that decodes a packet buffer and passes it to a + * {@link android.media.midi.MidiReceiver} + */ +public abstract class PacketDecoder { + + /** + * Decodes MIDI data in a packet and passes it to a {@link android.media.midi.MidiReceiver} + * @param buffer the packet to decode + * @param receiver the {@link android.media.midi.MidiReceiver} to receive the decoded MIDI data + */ + abstract public void decodePacket(byte[] buffer, MidiReceiver receiver); +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java new file mode 100644 index 0000000..12c8b9b --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * This is an abstract base class that encodes MIDI data into a packet buffer. + * PacketEncoder receives data via its {@link android.media.midi.MidiReceiver#onReceive} method + * and notifies its client of packets to write via the {@link PacketEncoder.PacketReceiver} + * interface. + */ +public abstract class PacketEncoder extends MidiReceiver { + + public interface PacketReceiver { + /** Called to write an accumulated packet. + * @param buffer the packet buffer to write + * @param count the number of bytes in the packet buffer to write + */ + public void writePacket(byte[] buffer, int count); + } + + /** + * Called to inform PacketEncoder when the previous write is complete. + */ + abstract public void writeComplete(); +} |