/*
* Copyright (C) 2018 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.
*/
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#define TAG "MidiTestManager"
#include <android/log.h>
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#include "MidiTestManager.h"
static pthread_t readThread;
static const bool DEBUG = false;
static const bool DEBUG_MIDIDATA = false;
//
// MIDI Messages
//
// Channel Commands
static const uint8_t kMIDIChanCmd_KeyDown = 9;
static const uint8_t kMIDIChanCmd_KeyUp = 8;
static const uint8_t kMIDIChanCmd_PolyPress = 10;
static const uint8_t kMIDIChanCmd_Control = 11;
static const uint8_t kMIDIChanCmd_ProgramChange = 12;
static const uint8_t kMIDIChanCmd_ChannelPress = 13;
static const uint8_t kMIDIChanCmd_PitchWheel = 14;
// System Commands
static const uint8_t kMIDISysCmd_SysEx = 0xF0;
static const uint8_t kMIDISysCmd_EndOfSysEx = 0xF7;
static const uint8_t kMIDISysCmd_ActiveSensing = 0xFE;
static const uint8_t kMIDISysCmd_Reset = 0xFF;
static void* readThreadRoutine(void * context) {
MidiTestManager* testManager = (MidiTestManager*)context;
return reinterpret_cast<void*>(static_cast<intptr_t>(testManager->ProcessInput()));
}
/*
* TestMessage
*/
#define makeMIDICmd(cmd, channel) (uint8_t)((cmd << 4) | (channel & 0x0F))
class TestMessage {
public:
uint8_t* mMsgBytes;
int mNumMsgBytes;
TestMessage()
: mMsgBytes(NULL), mNumMsgBytes(0)
{}
~TestMessage() {
delete[] mMsgBytes;
}
bool set(uint8_t* msgBytes, int numMsgBytes) {
if (msgBytes == NULL || numMsgBytes <= 0) {
return false;
}
mNumMsgBytes = numMsgBytes;
mMsgBytes = new uint8_t[numMsgBytes];
memcpy(mMsgBytes, msgBytes, mNumMsgBytes * sizeof(uint8_t));
return true;
}
}; /* class TestMessage */
/*
* MidiTestManager
*/
MidiTestManager::MidiTestManager()
: mTestModuleObj(NULL),
mTestStream(NULL), mNumTestStreamBytes(0),
mReceiveStreamPos(0),
mMidiSendPort(NULL), mMidiReceivePort(NULL),
mTestMsgs(NULL), mNumTestMsgs(0)
{}
MidiTestManager::~MidiTestManager(){
delete[] mTestStream;
}
void MidiTestManager::jniSetup(JNIEnv* env) {
env->GetJavaVM(&mJvm);
jclass clsMidiTestModule =
env->FindClass("com/android/cts/verifier/audio/NDKMidiActivity$NDKMidiTestModule");
if (DEBUG) {
ALOGI("gClsMidiTestModule:%p", clsMidiTestModule);
}
// public void endTest(int endCode)
mMidEndTest = env->GetMethodID(clsMidiTestModule, "endTest", "(I)V");
if (DEBUG) {
ALOGI("mMidEndTestgMidEndTest:%p", mMidEndTest);
}
}
void MidiTestManager::buildTestStream() {
// add up the total space
mNumTestStreamBytes = 0;
for(int msgIndex = 0; msgIndex < mNumTestMsgs; msgIndex++) {
mNumTestStreamBytes += mTestMsgs[msgIndex].mNumMsgBytes;
}
delete[] mTestStream;
mTestStream = new uint8_t[mNumTestStreamBytes];
int streamIndex = 0;
for(int msgIndex = 0; msgIndex < mNumTestMsgs; msgIndex++) {
for(int byteIndex = 0; byteIndex < mTestMsgs[msgIndex].mNumMsgBytes; byteIndex++) {
mTestStream[streamIndex++] = mTestMsgs[msgIndex].mMsgBytes[byteIndex];
}
}
// Reset stream position
mReceiveStreamPos = 0;
}
/**
* Compares the supplied bytes against the sent message stream at the current postion
* and advances the stream position.
*/
bool MidiTestManager::matchStream(uint8_t* bytes, int count) {
if (DEBUG) {
ALOGI("---- matchStream() count:%d", count);
}
bool matches = true;
for (int index = 0; index < count; index++) {
if (bytes[index] != mTestStream[mReceiveStreamPos]) {
matches = false;
if (DEBUG) {
ALOGI("---- mismatch @%d [%d : %d]",
index, bytes[index], mTestStream[mReceiveStreamPos]);
}
}
mReceiveStreamPos++;
}
if (DEBUG) {
ALOGI(" returns:%d", matches);
}
return matches;
}
/**
* Writes out the list of MIDI messages to the output port.
* Returns total number of bytes sent.
*/
int MidiTestManager::sendMessages() {
if (DEBUG) {
ALOGI("---- sendMessages()...");
for(int msgIndex = 0; msgIndex < mNumTestMsgs; msgIndex++) {
if (DEBUG_MIDIDATA) {
ALOGI("--------");
for(int byteIndex = 0; byteIndex < mTestMsgs[msgIndex].mNumMsgBytes; byteIndex++) {
ALOGI(" 0x%X", mTestMsgs[msgIndex].mMsgBytes[byteIndex]);
}
}
}
}
int totalSent = 0;
for(int msgIndex = 0; msgIndex < mNumTestMsgs; msgIndex++) {
ssize_t numSent =
AMidiInputPort_send(mMidiSendPort,
mTestMsgs[msgIndex].mMsgBytes, mTestMsgs[msgIndex].mNumMsgBytes);
totalSent += numSent;
}
if (DEBUG) {
ALOGI("---- totalSent:%d", totalSent);
}
return totalSent;
}
int MidiTestManager::ProcessInput() {
uint8_t readBuffer[128];
size_t totalNumReceived = 0;
bool testRunning = true;
int testResult = TESTSTATUS_NOTRUN;
int32_t opCode;
size_t numBytesReceived;
int64_t timeStamp;
while (testRunning) {
// AMidiOutputPort_receive is non-blocking, so let's not burn up the CPU unnecessarily
usleep(2000);
numBytesReceived = 0;
ssize_t numMessagesReceived =
AMidiOutputPort_receive(mMidiReceivePort, &opCode, readBuffer, 128,
&numBytesReceived, &timeStamp);
if (testRunning &&
numBytesReceived > 0 &&
opCode == AMIDI_OPCODE_DATA &&
readBuffer[0] != kMIDISysCmd_ActiveSensing &&
readBuffer[0] != kMIDISysCmd_Reset) {
if (DEBUG) {
ALOGI("---- msgs:%zd, bytes:%zu", numMessagesReceived, numBytesReceived);
}
// Process Here
if (!matchStream(readBuffer, numBytesReceived)) {
testResult = TESTSTATUS_FAILED_MISMATCH;
testRunning = false; // bail
}
totalNumReceived += numBytesReceived;
if (totalNumReceived > mNumTestStreamBytes) {
testResult = TESTSTATUS_FAILED_OVERRUN;
testRunning = false; // bail
}
if (totalNumReceived == mNumTestStreamBytes) {
testResult = TESTSTATUS_PASSED;
testRunning = false; // done
}
}
}
return testResult;
}
bool MidiTestManager::StartReading(AMidiDevice* nativeReadDevice) {
ALOGI("StartReading()...");
media_status_t m_status =
AMidiOutputPort_open(nativeReadDevice, 0, &mMidiReceivePort);
if (m_status != 0) {
ALOGE("Can't open MIDI device for reading err:%d", m_status);
return false;
}
// Start read thread
int status = pthread_create(&readThread, NULL, readThreadRoutine, this);
if (status != 0) {
ALOGE("Can't start readThread: %s (%d)", strerror(status), status);
}
return status == 0;
}
bool MidiTestManager::StartWriting(AMidiDevice* nativeWriteDevice) {
ALOGI("StartWriting()...");
media_status_t status =
AMidiInputPort_open(nativeWriteDevice, 0, &mMidiSendPort);
if (status != 0) {
ALOGE("Can't open MIDI device for writing err:%d", status);
return false;
}
return true;
}
uint8_t msg0[] = {makeMIDICmd(kMIDIChanCmd_KeyDown, 0), 64, 120};
//uint8_t msg0Alt[] = {makeMIDICmd(kMIDIChanCmd_KeyDown, 0), 65, 120};
uint8_t msg1[] = {makeMIDICmd(kMIDIChanCmd_KeyUp, 0), 64, 35};
bool MidiTestManager::RunTest(jobject testModuleObj, AMidiDevice* sendDevice,
AMidiDevice* receiveDevice) {
if (DEBUG) {
ALOGI("RunTest(%p, %p, %p)", testModuleObj, sendDevice, receiveDevice);
}
JNIEnv* env;
mJvm->AttachCurrentThread(&env, NULL);
if (env == NULL) {
EndTest(TESTSTATUS_FAILED_JNI);
}
mTestModuleObj = env->NewGlobalRef(testModuleObj);
// Call StartWriting first because StartReading starts a thread.
if (!StartWriting(sendDevice) || !StartReading(receiveDevice)) {
// Test call to EndTest will close any open devices.
EndTest(TESTSTATUS_FAILED_DEVICE);
return false; // bail
}
// setup messages
delete[] mTestMsgs;
mNumTestMsgs = 3;
mTestMsgs = new TestMessage[mNumTestMsgs];
int sysExSize = 8;
uint8_t* sysExMsg = new uint8_t[sysExSize];
sysExMsg[0] = kMIDISysCmd_SysEx;
for(int index = 1; index < sysExSize-1; index++) {
sysExMsg[index] = (uint8_t)index;
}
sysExMsg[sysExSize-1] = kMIDISysCmd_EndOfSysEx;
if (!mTestMsgs[0].set(msg0, sizeof(msg0)) ||
!mTestMsgs[1].set(msg1, sizeof(msg1)) ||
!mTestMsgs[2].set(sysExMsg, sysExSize)) {
return false;
}
delete[] sysExMsg;
buildTestStream();
// Inject an error
// mTestMsgs[0].set(msg0Alt, 3);
sendMessages();
void* threadRetval = (void*)TESTSTATUS_NOTRUN;
int status = pthread_join(readThread, &threadRetval);
if (status != 0) {
ALOGE("Failed to join readThread: %s (%d)", strerror(status), status);
}
EndTest(static_cast<int>(reinterpret_cast<intptr_t>(threadRetval)));
return true;
}
void MidiTestManager::EndTest(int endCode) {
JNIEnv* env;
mJvm->AttachCurrentThread(&env, NULL);
if (env == NULL) {
ALOGE("Error retrieving JNI Env");
}
env->CallVoidMethod(mTestModuleObj, mMidEndTest, endCode);
env->DeleteGlobalRef(mTestModuleObj);
// EndTest() will ALWAYS be called, so we can close the ports here.
if (mMidiSendPort != NULL) {
AMidiInputPort_close(mMidiSendPort);
mMidiSendPort = NULL;
}
if (mMidiReceivePort != NULL) {
AMidiOutputPort_close(mMidiReceivePort);
mMidiReceivePort = NULL;
}
}