/* * 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 #include #include #include #define TAG "MidiTestManager" #include #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, 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(static_cast(testManager->ProcessInput())); } /* * TestMessage */ #define makeMIDICmd(cmd, channel) (uint8_t)((cmd << 4) | (channel & 0x0F)) uint8_t warmupMsg[] = {makeMIDICmd(kMIDIChanCmd_Control, 0), 0, 0}; uint8_t msg0[] = {makeMIDICmd(kMIDIChanCmd_KeyDown, 0), 64, 120}; uint8_t msg1[] = {makeMIDICmd(kMIDIChanCmd_KeyUp, 0), 64, 35}; 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), mReceiveStreamPos(0), mMidiSendPort(NULL), mMidiReceivePort(NULL), mTestMsgs(NULL), mNumTestMsgs(0), mThrottleData(false) {} MidiTestManager::~MidiTestManager() { mMatchStream.clear(); } void MidiTestManager::jniSetup(JNIEnv* env) { env->GetJavaVM(&mJvm); jclass clsMidiTestModule = env->FindClass("com/android/cts/verifier/audio/MidiNativeTestActivity$NativeMidiTestModule"); 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::buildMatchStream() { mMatchStream.clear(); for(int byteIndex = 0; byteIndex < sizeof(warmupMsg); byteIndex++) { mMatchStream.push_back(warmupMsg[byteIndex]); } for(int msgIndex = 0; msgIndex < mNumTestMsgs; msgIndex++) { for(int byteIndex = 0; byteIndex < mTestMsgs[msgIndex].mNumMsgBytes; byteIndex++) { mMatchStream.push_back(mTestMsgs[msgIndex].mMsgBytes[byteIndex]); } } // Reset stream position mReceiveStreamPos = 0; } static void logBytes(uint8_t* bytes, int count) { int buffSize = (count * 6) + 1; // count of "0x??, " + '\0'; char* logBuff = new char[buffSize]; for (int dataIndex = 0; dataIndex < count; dataIndex++) { sprintf(logBuff + (dataIndex * 6), "0x%.2X", bytes[dataIndex]); if (dataIndex < count - 1) { sprintf(logBuff + (dataIndex * 6) + 4, ", "); } } ALOGD("logbytes(%d): %s", count, logBuff); delete[] logBuff; } /** * Compares the supplied bytes against the sent message stream at the current position * and advances the stream position. */ bool MidiTestManager::matchStream(uint8_t* bytes, int count) { if (DEBUG) { ALOGI("---- matchStream() count:%d", count); } // a little bit of checking here... if (count < 0) { ALOGE("Negative Byte Count in MidiTestManager::matchStream()"); return false; } if (count > MESSAGE_MAX_BYTES) { ALOGE("Too Large Byte Count (%d) in MidiTestManager::matchStream()", count); return false; } bool matches = true; for (int index = 0; index < count; index++) { // Check for buffer overflow if (mReceiveStreamPos >= mMatchStream.size()) { ALOGD("matchStream() out-of-bounds @%d", mReceiveStreamPos); matches = false; break; } if (bytes[index] != mMatchStream[mReceiveStreamPos]) { matches = false; ALOGD("---- mismatch @%d [rec:0x%X : exp:0x%X]", index, bytes[index], mMatchStream[mReceiveStreamPos]); } mReceiveStreamPos++; } if (DEBUG) { ALOGI(" returns:%d", matches); } if (!matches) { ALOGD("Mismatched Received Data:"); logBytes(bytes, count); } return matches; } #define THROTTLE_PERIOD_MS 10 int portSend(AMidiInputPort* sendPort, uint8_t* msg, int length, bool throttle) { int numSent = 0; if (throttle) { for(int index = 0; index < length; index++) { AMidiInputPort_send(sendPort, msg + index, 1); usleep(THROTTLE_PERIOD_MS * 1000); } numSent = length; } else { numSent = AMidiInputPort_send(sendPort, msg, length); } return numSent; } /** * 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]); } } } } // Send "Warm-up" message portSend(mMidiSendPort, warmupMsg, sizeof(warmupMsg), mThrottleData); int totalSent = 0; for(int msgIndex = 0; msgIndex < mNumTestMsgs; msgIndex++) { ssize_t numSent = portSend(mMidiSendPort, mTestMsgs[msgIndex].mMsgBytes, mTestMsgs[msgIndex].mNumMsgBytes, mThrottleData); 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 (DEBUG && numBytesReceived > 0) { logBytes(readBuffer, numBytesReceived); } 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); } // Check first byte for warm-up message if (totalNumReceived == 0 && readBuffer[0] != makeMIDICmd(kMIDIChanCmd_Control, 0)) { // advance stream past the "warm up" message mReceiveStreamPos += sizeof(warmupMsg); if (DEBUG) { ALOGD("---- No Warm Up Message Detected."); } } if (!matchStream(readBuffer, numBytesReceived)) { testResult = TESTSTATUS_FAILED_MISMATCH; if (DEBUG) { ALOGE("---- TESTSTATUS_FAILED_MISMATCH"); } testRunning = false; // bail } totalNumReceived += numBytesReceived; if (totalNumReceived > mMatchStream.size()) { testResult = TESTSTATUS_FAILED_OVERRUN; if (DEBUG) { ALOGE("---- TESTSTATUS_FAILED_OVERRUN"); } testRunning = false; // bail } if (totalNumReceived == mMatchStream.size()) { testResult = TESTSTATUS_PASSED; if (DEBUG) { ALOGE("---- TESTSTATUS_PASSED"); } testRunning = false; // done } } } return testResult; } bool MidiTestManager::StartReading(AMidiDevice* nativeReadDevice) { if (DEBUG) { 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; } bool MidiTestManager::RunTest(jobject testModuleObj, AMidiDevice* sendDevice, AMidiDevice* receiveDevice, bool throttleData) { if (DEBUG) { ALOGI("RunTest(%p, %p, %p)", testModuleObj, sendDevice, receiveDevice); } mThrottleData = throttleData; 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 = 32; 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; buildMatchStream(); 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(reinterpret_cast(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; } }