diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2020-04-04 03:46:15 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-04-04 03:46:15 +0000 |
commit | 7bfa49bf27d1d716308733f592fd939239bd9297 (patch) | |
tree | b16c3783cc9aa07201a0f0dda91bd8b01c295ec8 /media/packages | |
parent | b7bca21ba9e6560f3caa268f9016994803d313f5 (diff) | |
parent | 6c0aff044df035637f37c2a9ba43f5af07d89878 (diff) |
Merge "BluetoothMidi test: test encoder and decoder" into rvc-dev
Diffstat (limited to 'media/packages')
11 files changed, 1089 insertions, 1 deletions
diff --git a/media/packages/BluetoothMidiService/Android.bp b/media/packages/BluetoothMidiService/Android.bp index f45114a277aa..77e6a14e1f00 100644 --- a/media/packages/BluetoothMidiService/Android.bp +++ b/media/packages/BluetoothMidiService/Android.bp @@ -1,6 +1,35 @@ +// +// Copyright (C) 2019 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. +// + +android_library { + name: "BluetoothMidiLib", + srcs: [ + "src/**/*.java", + ], + platform_apis: true, + plugins: ["java_api_finder"], + manifest: "AndroidManifestBase.xml", +} + android_app { name: "BluetoothMidiService", - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + ], platform_apis: true, certificate: "platform", + manifest: "AndroidManifest.xml", } diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml index 1cfd55d556a9..4042ce8b93df 100644 --- a/media/packages/BluetoothMidiService/AndroidManifest.xml +++ b/media/packages/BluetoothMidiService/AndroidManifest.xml @@ -1,12 +1,35 @@ +<?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. + */ +--> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.android.bluetoothmidiservice" + android:versionCode="1" + android:versionName="R-initial" > + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> <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 + tools:replace="android:label" android:label="@string/app_name"> <service android:name=".BluetoothMidiService" android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"> diff --git a/media/packages/BluetoothMidiService/AndroidManifestBase.xml b/media/packages/BluetoothMidiService/AndroidManifestBase.xml new file mode 100644 index 000000000000..ebe62b039434 --- /dev/null +++ b/media/packages/BluetoothMidiService/AndroidManifestBase.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2020 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. + */ +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.bluetoothmidiservice" + android:versionCode="1" + android:versionName="R-initial" + > + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> + <application + android:label="BluetoothMidi" + android:defaultToDeviceProtectedStorage="true" + android:directBootAware="true"> + </application> +</manifest> diff --git a/media/packages/BluetoothMidiService/tests/unit/Android.bp b/media/packages/BluetoothMidiService/tests/unit/Android.bp new file mode 100644 index 000000000000..4d4ae9e15532 --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/Android.bp @@ -0,0 +1,38 @@ +// +// Copyright (C) 2019 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. +// + +android_test { + name: "BluetoothMidiTests", + srcs: ["src/**/*.java"], + certificate: "platform", + static_libs: [ + //"frameworks-base-testutils", + "android-support-test", + "androidx.test.core", + "androidx.test.ext.truth", + "androidx.test.runner", + "androidx.test.rules", + "platform-test-annotations", + "BluetoothMidiLib", + ], + test_suites: ["device-tests"], + libs: [ + "framework-res", + "android.test.runner", + "android.test.base", + "android.test.mock", + ], +} diff --git a/media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml b/media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml new file mode 100644 index 000000000000..4d27e1e7fe04 --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.bluetoothmidiservice.tests.unit"> + <uses-sdk + android:minSdkVersion="29" + android:targetSdkVersion="29" /> + + <application android:testOnly="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.bluetoothmidiservice.tests.unit" + android:label="Bluetooth MIDI Service tests"> + </instrumentation> +</manifest> diff --git a/media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml b/media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml new file mode 100644 index 000000000000..02e7f0d55349 --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> + +<configuration description="Runs Bluetooth MIDI Service Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="BluetoothMidiTests.apk" /> + </target_preparer> + + <option name="test-tag" value="BLEMidiTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.bluetoothmidiservice.tests.unit" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false" /> + </test> +</configuration> diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java new file mode 100644 index 000000000000..57859bc114d8 --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 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.MidiFramer; + +import java.util.ArrayList; + +class AccumulatingMidiReceiver extends MidiReceiver { + private static final String TAG = "AccumulatingMidiReceiver"; + ArrayList<byte[]> mBuffers = new ArrayList<byte[]>(); + ArrayList<Long> mTimestamps = new ArrayList<Long>(); + + public void onSend(byte[] buffer, int offset, int count, long timestamp) { + Log.d(TAG, "onSend() passed " + MidiFramer.formatMidiData(buffer, offset, count)); + byte[] actualRow = new byte[count]; + System.arraycopy(buffer, offset, actualRow, 0, count); + mBuffers.add(actualRow); + mTimestamps.add(timestamp); + } + + byte[][] getBuffers() { + return mBuffers.toArray(new byte[mBuffers.size()][]); + } + + Long[] getTimestamps() { + return mTimestamps.toArray(new Long[mTimestamps.size()]); + } +} + diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java new file mode 100644 index 000000000000..3285f59e55c9 --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2020 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.util.Log; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.midi.MidiFramer; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * End to end testing of the Bluetooth Encoder and Decoder + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class BluetoothMidiCodecTest { + + private static final String TAG = "BluetoothMidiCodecTest"; + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + private static final long NANOS_PER_MSEC = 1000000L; + + static class EncoderDecoderChecker implements PacketEncoder.PacketReceiver { + BluetoothPacketEncoder mEncoder; + BluetoothPacketDecoder mDecoder; + AccumulatingMidiReceiver mReceiver; + MidiFramer mFramer; + AccumulatingMidiReceiver mBypassReceiver; + MidiFramer mBypassFramer; + int mMaxPacketsPerConnection; + int mConnectionIntervalMillis; + BlockingQueue<byte[]> mPacketQueue; + ScheduledExecutorService mScheduler; + + EncoderDecoderChecker() { + this(2, 15, 20); + } + + EncoderDecoderChecker( + int maxPacketsPerConnection, + int connectionIntervalMillis, + int maxBytesPerPacket) { + mMaxPacketsPerConnection = maxPacketsPerConnection; + mConnectionIntervalMillis = connectionIntervalMillis; + mEncoder = new BluetoothPacketEncoder(this, maxBytesPerPacket); + mDecoder = new BluetoothPacketDecoder(maxBytesPerPacket); + mReceiver = new AccumulatingMidiReceiver(); + mFramer = new MidiFramer(mReceiver); + mBypassReceiver = new AccumulatingMidiReceiver(); + mBypassFramer = new MidiFramer(mBypassReceiver); + mScheduler = Executors.newSingleThreadScheduledExecutor(); + mPacketQueue = new LinkedBlockingDeque<>(maxPacketsPerConnection); + } + + void processQueue() throws InterruptedException { + for (int i = 0; i < mMaxPacketsPerConnection; i++) { + byte[] packet = mPacketQueue.poll(0, TimeUnit.SECONDS); + if (packet == null) break; + Log.d(TAG, "decode " + MidiFramer.formatMidiData(packet, 0, packet.length)); + mDecoder.decodePacket(packet, mFramer); + } + Log.d(TAG, "call writeComplete()"); + mEncoder.writeComplete(); + } + + public void start() { + mScheduler.scheduleAtFixedRate( + () -> { + Log.d(TAG, "run scheduled task"); + try { + processQueue(); + } catch (Exception e) { + assertEquals(null, e); + } + }, + mConnectionIntervalMillis, // initial delay + mConnectionIntervalMillis, // period + TimeUnit.MILLISECONDS); + } + + public void stop() { + // TODO wait for queue to empty + mScheduler.shutdown(); + } + + // TODO Should this block? + // Store the packets and then write them from a periodic task. + @Override + public void writePacket(byte[] buffer, int count) { + Log.d(TAG, "writePacket() passed " + MidiFramer.formatMidiData(buffer, 0, count)); + byte[] packet = new byte[count]; + System.arraycopy(buffer, 0, packet, 0, count); + try { + mPacketQueue.put(packet); + } catch (Exception e) { + assertEquals(null, e); + } + Log.d(TAG, "writePacket() returns"); + } + + void test(final byte[][] midi) + throws IOException, InterruptedException { + test(midi, 2); + } + + // Send the MIDI messages through the encoder, + // then through the decoder, + // then gather the resulting MIDI and compare the results. + void test(final byte[][] midi, int intervalMillis) + throws IOException, InterruptedException { + start(); + long timestamp = 0; + // Send all of the MIDI messages and gather the response. + for (int i = 0; i < midi.length; i++) { + byte[] outMessage = midi[i]; + Log.d(TAG, "outMessage " + + MidiFramer.formatMidiData(outMessage, 0, outMessage.length)); + mEncoder.send(outMessage, 0, outMessage.length, timestamp); + timestamp += 2 * NANOS_PER_MSEC; + // Also send a copy through a MidiFramer to align the messages. + mBypassFramer.send(outMessage, 0, outMessage.length, timestamp); + } + Thread.sleep(200); + stop(); + + // Compare the gathered rows with the expected rows. + byte[][] expectedMessages = mBypassReceiver.getBuffers(); + byte[][] inMessages = mReceiver.getBuffers(); + Log.d(TAG, "expectedMessage length = " + expectedMessages.length + + ", inMessages length = " + inMessages.length); + assertEquals(expectedMessages.length, inMessages.length); + Long[] actualTimestamps = mReceiver.getTimestamps(); + long previousTime = 0; + for (int i = 0; i < expectedMessages.length; i++) { + byte[] expectedMessage = expectedMessages[i]; + Log.d(TAG, "expectedMessage = " + + MidiFramer.formatMidiData(expectedMessage, + 0, expectedMessage.length)); + byte[] actualMessage = inMessages[i]; + Log.d(TAG, "actualMessage = " + + MidiFramer.formatMidiData(actualMessage, 0, actualMessage.length)); + assertArrayEquals(expectedMessage, actualMessage); + // Are the timestamps monotonic? + long currentTime = actualTimestamps[i]; + Log.d(TAG, "previousTime = " + previousTime + + ", currentTime = " + currentTime); + assertTrue(currentTime >= previousTime); + previousTime = currentTime; + } + } + } + + @Test + public void testOneNoteOn() throws IOException, InterruptedException { + final byte[][] midi = { + {(byte) 0x90, 0x40, 0x64} + }; + EncoderDecoderChecker checker = new EncoderDecoderChecker(); + checker.test(midi); + } + + @Test + public void testTwoNoteOnSameTime() throws IOException, InterruptedException { + final byte[][] midi = { + {(byte) 0x90, 0x40, 0x64, (byte) 0x90, 0x47, 0x70} + }; + EncoderDecoderChecker checker = new EncoderDecoderChecker(); + checker.test(midi); + } + + @Test + public void testTwoNoteOnStaggered() throws IOException, InterruptedException { + final byte[][] midi = { + {(byte) 0x90, 0x40, 0x64}, + {(byte) 0x90, 0x47, 0x70} + }; + EncoderDecoderChecker checker = new EncoderDecoderChecker(); + checker.test(midi); + } + + public void checkNoteBurst(int maxPacketsPerConnection, + int period, + int maxBytesPerPacket) throws IOException, InterruptedException { + final int numNotes = 100; + final byte[][] midi = new byte[numNotes][]; + int channel = 2; + for (int i = 0; i < numNotes; i++) { + byte[] message = {(byte) (0x90 + channel), (byte) (i + 1), 0x64}; + midi[i] = message; + channel ^= 1; + } + EncoderDecoderChecker checker = new EncoderDecoderChecker( + maxPacketsPerConnection, 15, maxBytesPerPacket); + checker.test(midi, period); + } + + @Test + public void testNoteBurstM1P6() throws IOException, InterruptedException { + checkNoteBurst(1, 6, 20); + } + @Test + public void testNoteBurstM1P2() throws IOException, InterruptedException { + checkNoteBurst(1, 2, 20); + } + @Test + public void testNoteBurstM2P6() throws IOException, InterruptedException { + checkNoteBurst(2, 6, 20); + } + @Test + public void testNoteBurstM2P2() throws IOException, InterruptedException { + checkNoteBurst(2, 2, 20); + } + @Test + public void testNoteBurstM2P0() throws IOException, InterruptedException { + checkNoteBurst(2, 0, 20); + } + @Test + public void testNoteBurstM2P6B21() throws IOException, InterruptedException { + checkNoteBurst(2, 6, 21); + } + @Test + public void testNoteBurstM2P2B21() throws IOException, InterruptedException { + checkNoteBurst(2, 2, 21); + } + @Test + public void testNoteBurstM2P0B21() throws IOException, InterruptedException { + checkNoteBurst(2, 0, 21); + } +} diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java new file mode 100644 index 000000000000..6ecc53957eaa --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2020 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.util.Log; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.midi.MidiFramer; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class BluetoothMidiDecoderTest { + + private static final String TAG = "BluetoothMidiDecoderTest"; + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + private static final long NANOS_PER_MSEC = 1000000L; + + static class DecoderChecker { + AccumulatingMidiReceiver mReceiver; + BluetoothPacketDecoder mDecoder; + + DecoderChecker() { + mReceiver = new AccumulatingMidiReceiver(); + final int maxBytes = 20; + mDecoder = new BluetoothPacketDecoder(maxBytes); + } + + void compareWithExpected(final byte[][] expectedMessages) { + byte[][] actualRows = mReceiver.getBuffers(); + Long[] actualTimestamps = mReceiver.getTimestamps(); + long previousTime = 0; + // Compare the gathered with the expected. + assertEquals(expectedMessages.length, actualRows.length); + for (int i = 0; i < expectedMessages.length; i++) { + byte[] expectedRow = expectedMessages[i]; + Log.d(TAG, "expectedRow = " + + MidiFramer.formatMidiData(expectedRow, 0, expectedRow.length)); + byte[] actualRow = actualRows[i]; + Log.d(TAG, "actualRow = " + + MidiFramer.formatMidiData(actualRow, 0, actualRow.length)); + assertArrayEquals(expectedRow, actualRow); + // Are the timestamps monotonic? + long currentTime = actualTimestamps[i]; + Log.d(TAG, "previousTime = " + previousTime + ", currentTime = " + currentTime); + assertTrue(currentTime >= previousTime); + previousTime = currentTime; + } + } + + void decodePacket(byte[] packet) throws IOException { + mDecoder.decodePacket(packet, mReceiver); + } + + void decodePackets(byte[][] multiplePackets) throws IOException { + try { + for (int i = 0; i < multiplePackets.length; i++) { + byte[] packet = multiplePackets[i]; + mDecoder.decodePacket(packet, mReceiver); + } + } catch (Exception e) { + assertEquals(null, e); + } + } + + void test(byte[] encoded, byte[][] decoded) throws IOException { + decodePacket(encoded); + compareWithExpected(decoded); + } + + void test(byte[][] encoded, byte[][] decoded) throws IOException { + decodePackets(encoded); + compareWithExpected(decoded); + } + } + + @Test + public void testOneNoteOn() throws IOException { + final byte[] encoded = { + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x90, 0x40, 0x64 + }; + final byte[][] decoded = { + {(byte) 0x90, 0x40, 0x64} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testReservedHeaderBit() throws IOException { + final byte[] encoded = { + // Decoder should ignore the reserved bit. + (byte) (0x80 | 0x40), // set RESERVED bit in header! + (byte) 0x80, // high bit of timestamp + (byte) 0x90, 0x40, 0x64 + }; + final byte[][] decoded = { + {(byte) 0x90, 0x40, 0x64} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testTwoNotesOnRunning() throws IOException { + final byte[] encoded = { + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x90, 0x40, 0x64, + (byte) 0x85, // timestamp + (byte) 0x42, 0x70 + }; + final byte[][] decoded = { + {(byte) 0x90, 0x40, 0x64}, + {(byte) 0x42, 0x70} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testTwoNoteOnsTwoChannels() throws IOException { + final byte[] encoded = { + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x93, 0x40, 0x60, + // two channels so no running status + (byte) 0x80, // high bit of timestamp + (byte) 0x95, 0x47, 0x64 + }; + final byte[][] decoded = { + {(byte) 0x93, 0x40, 0x60, (byte) 0x95, 0x47, 0x64} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testTwoNoteOnsOverTime() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x98, 0x45, 0x60 + }, + { + (byte) 0x80, // high bit of header must be set + (byte) 0x82, // timestamp advanced by 2 msec + (byte) 0x90, 0x40, 0x64, + (byte) 0x84, // timestamp needed because of time delay + // encoder uses running status + 0x47, 0x72 + }}; + final byte[][] decoded = { + {(byte) 0x98, 0x45, 0x60}, + {(byte) 0x90, 0x40, 0x64}, + {(byte) 0x47, 0x72} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testSysExBasic() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // timestamp + (byte) 0xF0, 0x7D, // Begin prototyping SysEx + 0x01, 0x02, 0x03, 0x04, 0x05, + (byte) 0x80, // timestamp + (byte) 0xF7 // End SysEx + }}; + final byte[][] decoded = { + {(byte) 0xF0, 0x7D, // experimental SysEx + 0x01, 0x02, 0x03, 0x04, 0x05, (byte) 0xF7} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testSysExTwoPackets() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // timestamp + (byte) 0xF0, 0x7D, // Begin prototyping SysEx + 0x01, 0x02 + }, + { + (byte) 0x80, // high bit of header must be set + 0x03, 0x04, 0x05, + (byte) 0x80, // timestamp + (byte) 0xF7 // End SysEx + }}; + final byte[][] decoded = { + {(byte) 0xF0, 0x7D, 0x01, 0x02}, // experimental SysEx + {0x03, 0x04, 0x05, (byte) 0xF7} + }; + new DecoderChecker().test(encoded, decoded); + } + + @Test + public void testSysExThreePackets() throws IOException { + final byte[][] encoded = { + {(byte) 0x80, // high bit of header must be set + (byte) 0x80, // timestamp + (byte) 0xF0, 0x7D, // Begin prototyping SysEx + 0x01, 0x02 + }, + { + (byte) 0x80, // high bit of header must be set + 0x03, 0x04, 0x05, + }, + { + (byte) 0x80, // high bit of header must be set + 0x06, 0x07, 0x08, + (byte) 0x80, // timestamp + (byte) 0xF7 // End SysEx + }}; + final byte[][] decoded = { + {(byte) 0xF0, 0x7D, 0x01, 0x02}, // experimental SysEx + {0x03, 0x04, 0x05}, + {0x06, 0x07, 0x08, (byte) 0xF7} + }; + new DecoderChecker().test(encoded, decoded); + } + +} diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java new file mode 100644 index 000000000000..a169c0d7c7f9 --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2020 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 static org.junit.Assert.assertEquals; + +import android.util.Log; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.midi.MidiFramer; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class BluetoothMidiEncoderTest { + + private static final String TAG = "BluetoothMidiEncoderTest"; + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + private static final long NANOS_PER_MSEC = 1000000L; + + static class AccumulatingPacketReceiver implements PacketEncoder.PacketReceiver { + ArrayList<byte[]> mBuffers = new ArrayList<byte[]>(); + + public void writePacket(byte[] buffer, int count) { + byte[] actualRow = new byte[count]; + Log.d(TAG, "writePacket() passed " + MidiFramer.formatMidiData(buffer, 0, count)); + System.arraycopy(buffer, 0, actualRow, 0, count); + mBuffers.add(actualRow); + } + + byte[][] getBuffers() { + return mBuffers.toArray(new byte[mBuffers.size()][]); + } + } + + static class EncoderChecker { + AccumulatingPacketReceiver mReceiver; + BluetoothPacketEncoder mEncoder; + + EncoderChecker() { + mReceiver = new AccumulatingPacketReceiver(); + final int maxBytes = 20; + mEncoder = new BluetoothPacketEncoder(mReceiver, maxBytes); + } + + void send(byte[] data) throws IOException { + send(data, 0); + } + + void send(byte[] data, long timestamp) throws IOException { + Log.d(TAG, "send " + MidiFramer.formatMidiData(data, 0, data.length)); + mEncoder.send(data, 0, data.length, timestamp); + } + + void compareWithExpected(final byte[][] expected) { + byte[][] actualRows = mReceiver.getBuffers(); + assertEquals(expected.length, actualRows.length); + // Compare the gathered rows with the expected rows. + for (int i = 0; i < expected.length; i++) { + byte[] expectedRow = expected[i]; + Log.d(TAG, "expectedRow = " + + MidiFramer.formatMidiData(expectedRow, 0, expectedRow.length)); + byte[] actualRow = actualRows[i]; + Log.d(TAG, "actualRow = " + + MidiFramer.formatMidiData(actualRow, 0, actualRow.length)); + assertEquals(expectedRow.length, actualRow.length); + for (int k = 0; k < expectedRow.length; k++) { + assertEquals(expectedRow[k], actualRow[k]); + } + } + } + + void writeComplete() { + mEncoder.writeComplete(); + } + + } + + @Test + public void testOneNoteOn() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x90, 0x40, 0x64 + }}; + EncoderChecker checker = new EncoderChecker(); + checker.send(new byte[] {(byte) 0x90, 0x40, 0x64}); + checker.compareWithExpected(encoded); + } + + @Test + public void testTwoNoteOnsSameChannel() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x90, 0x40, 0x64, + // encoder converts to running status + 0x47, 0x72 + }}; + EncoderChecker checker = new EncoderChecker(); + checker.send(new byte[] {(byte) 0x90, 0x40, 0x64, (byte) 0x90, 0x47, 0x72}); + checker.compareWithExpected(encoded); + } + + @Test + public void testTwoNoteOnsTwoChannels() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x93, 0x40, 0x60, + // two channels so no running status + (byte) 0x80, // high bit of timestamp + (byte) 0x95, 0x47, 0x64 + }}; + EncoderChecker checker = new EncoderChecker(); + checker.send(new byte[] {(byte) 0x93, 0x40, 0x60, (byte) 0x95, 0x47, 0x64}); + checker.compareWithExpected(encoded); + } + + @Test + public void testTwoNoteOnsOverTime() throws IOException { + final byte[][] encoded = { + { + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // high bit of timestamp + (byte) 0x98, 0x45, 0x60 + }, + { + (byte) 0x80, // high bit of header must be set + (byte) 0x82, // timestamp advanced by 2 msec + (byte) 0x90, 0x40, 0x64, + (byte) 0x84, // timestamp needed because of time delay + // encoder converts to running status + 0x47, 0x72 + }}; + EncoderChecker checker = new EncoderChecker(); + long timestamp = 0; + // Send one note. This will cause an immediate packet write + // because we don't know when the next one will arrive. + checker.send(new byte[] {(byte) 0x98, 0x45, 0x60}, timestamp); + + // Send two notes. These should accumulate into the + // same packet because we do not yet have a writeComplete. + timestamp += 2 * NANOS_PER_MSEC; + checker.send(new byte[] {(byte) 0x90, 0x40, 0x64}, timestamp); + timestamp += 2 * NANOS_PER_MSEC; + checker.send(new byte[] {(byte) 0x90, 0x47, 0x72}, timestamp); + // Tell the encoder that the first packet has been written to the + // hardware. So it can flush the two pending notes. + checker.writeComplete(); + checker.compareWithExpected(encoded); + } + + @Test + public void testSysExBasic() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // timestamp + (byte) 0xF0, 0x7D, // Begin prototyping SysEx + 0x01, 0x02, 0x03, 0x04, 0x05, + (byte) 0x80, // timestamp + (byte) 0xF7 // End SysEx + }}; + EncoderChecker checker = new EncoderChecker(); + checker.send(new byte[] {(byte) 0xF0, 0x7D, // experimental SysEx + 0x01, 0x02, 0x03, 0x04, 0x05, (byte) 0xF7}); + checker.compareWithExpected(encoded); + } + + @Test + public void testSysExTwoPackets() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // timestamp + (byte) 0xF0, 0x7D, // Begin prototyping SysEx + 0x01, 0x02 + }, + { + (byte) 0x80, // high bit of header must be set + 0x03, 0x04, 0x05, + (byte) 0x80, // timestamp + (byte) 0xF7 // End SysEx + }}; + EncoderChecker checker = new EncoderChecker(); + // Send in two messages. + checker.send(new byte[] {(byte) 0xF0, 0x7D, // experimental SysEx + 0x01, 0x02}); + checker.send(new byte[] {0x03, 0x04, 0x05, (byte) 0xF7}); + // Tell the encoder that the first packet has been written to the + // hardware. So it can flush the remaining data. + checker.writeComplete(); + checker.compareWithExpected(encoded); + } + + @Test + public void testSysExThreePackets() throws IOException { + final byte[][] encoded = {{ + (byte) 0x80, // high bit of header must be set + (byte) 0x80, // timestamp + (byte) 0xF0, 0x7D, // Begin prototyping SysEx + 0x01, 0x02 + }, + { + (byte) 0x80, // high bit of header must be set + 0x03, 0x04, 0x05, + }, + { + (byte) 0x80, // high bit of header must be set + 0x06, 0x07, 0x08, + (byte) 0x80, // timestamp + (byte) 0xF7 // End SysEx + }}; + EncoderChecker checker = new EncoderChecker(); + // Send in three messages. + checker.send(new byte[] {(byte) 0xF0, 0x7D, // experimental SysEx + 0x01, 0x02}); + checker.send(new byte[] {0x03, 0x04, 0x05}); + checker.writeComplete(); + checker.send(new byte[] {0x06, 0x07, 0x08, (byte) 0xF7}); + checker.writeComplete(); + checker.compareWithExpected(encoded); + } +} diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java new file mode 100644 index 000000000000..8cda6eb3a3ea --- /dev/null +++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.util.Log; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.midi.MidiFramer; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MidiFramerTest { + + private static final String TAG = "MidiFramerTest"; + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + + // For testing MidiFramer + // TODO move MidiFramer tests to their own file + static class FramerChecker { + AccumulatingMidiReceiver mReceiver; + MidiFramer mFramer; + + FramerChecker() { + mReceiver = new AccumulatingMidiReceiver(); + mFramer = new MidiFramer(mReceiver); + } + + void compareWithExpected(final byte[][] expected) { + byte[][] actualRows = mReceiver.getBuffers(); + assertEquals(expected.length, actualRows.length); + // Compare the gathered rows with the expected rows. + for (int i = 0; i < expected.length; i++) { + byte[] expectedRow = expected[i]; + Log.d(TAG, "expectedRow = " + + MidiFramer.formatMidiData(expectedRow, 0, expectedRow.length)); + byte[] actualRow = actualRows[i]; + Log.d(TAG, "actualRow = " + + MidiFramer.formatMidiData(actualRow, 0, actualRow.length)); + assertArrayEquals(expectedRow, actualRow); + } + } + + void send(byte[] data) throws IOException { + Log.d(TAG, "send " + MidiFramer.formatMidiData(data, 0, data.length)); + mFramer.send(data, 0, data.length, 0); + } + } + + @Test + public void testFramerTwoNoteOns() throws IOException { + final byte[][] expected = { + {(byte) 0x90, 0x40, 0x64}, + {(byte) 0x90, 0x47, 0x50} + }; + FramerChecker checker = new FramerChecker(); + checker.send(new byte[] {(byte) 0x90, 0x40, 0x64, (byte) 0x90, 0x47, 0x50}); + checker.compareWithExpected(expected); + } + + @Test + public void testFramerTwoNoteOnsRunning() throws IOException { + final byte[][] expected = { + {(byte) 0x90, 0x40, 0x64}, + {(byte) 0x90, 0x47, 0x70} + }; + FramerChecker checker = new FramerChecker(); + // Two notes with running status + checker.send(new byte[] {(byte) 0x90, 0x40, 0x64, 0x47, 0x70}); + checker.compareWithExpected(expected); + } + + @Test + public void testFramerPreGarbage() throws IOException { + final byte[][] expected = { + {(byte) 0x90, 0x40, 0x64}, + {(byte) 0x90, 0x47, 0x70} + }; + FramerChecker checker = new FramerChecker(); + // Garbage can come before the first status byte if you connect + // a MIDI cable in the middle of a message. + checker.send(new byte[] {0x01, 0x02, // garbage bytes + (byte) 0x90, 0x40, 0x64, 0x47, 0x70}); + checker.compareWithExpected(expected); + } +} |