BluetoothMidi test: test encoder and decoder

Also test MidiFramer
Add end-to-end test through encoder and decoder.
Add test for reserved bit in header.

Bug: 35669198
Bug: 140638458
Bug: 149927520
Test: atest BluetoothMidiTests
Change-Id: I767deab6847d2b45e01bed29f5eed3df0f0e46c0
Merged-In: I767deab6847d2b45e01bed29f5eed3df0f0e46c0
This commit is contained in:
Phil Burk
2020-03-13 17:37:35 -07:00
parent b164ea9d7a
commit 6c0aff044d
11 changed files with 1089 additions and 1 deletions

View File

@@ -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",
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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",
],
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()]);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}