/* * 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. */ #define LOG_TAG "NativeMIDI" #include #include #include #include #include #include #include "android/media/midi/BpMidiDeviceServer.h" #include "media/MidiDeviceInfo.h" #include "include/amidi/AMidi.h" #include "amidi_internal.h" using namespace android::media::midi; using android::IBinder; using android::BBinder; using android::OK; using android::sp; using android::status_t; using android::base::unique_fd; using android::binder::Status; struct AMIDI_Port { std::atomic_int state; // One of the port status constants below. const AMidiDevice *device; // Points to the AMidiDevice associated with the port. sp binderToken;// The Binder token associated with the port. unique_fd ufd; // The unique file descriptor associated with the port. }; /* * Port Status Constants */ enum { MIDI_PORT_STATE_CLOSED = 0, MIDI_PORT_STATE_OPEN_IDLE, MIDI_PORT_STATE_OPEN_ACTIVE }; /* * Port Type Constants */ enum { PORTTYPE_OUTPUT = 0, PORTTYPE_INPUT = 1 }; /* TRANSFER PACKET FORMAT (as defined in MidiPortImpl.java) * * Transfer packet format is as follows (see MidiOutputPort.mThread.run() to see decomposition): * |oc|md|md| ......... |md|ts|ts|ts|ts|ts|ts|ts|ts| * ^ +--------------------+-----------------------+ * | ^ ^ * | | | * | | + timestamp (8 bytes) * | | * | + MIDI data bytes (numBytes bytes) * | * + OpCode (AMIDI_OPCODE_DATA) * * NOTE: The socket pair is configured to use SOCK_SEQPACKET mode. * SOCK_SEQPACKET, for a sequenced-packet socket that is connection-oriented, preserves message * boundaries, and delivers messages in the order that they were sent. * So 'read()' always returns a whole message. */ #define AMIDI_PACKET_SIZE 1024 #define AMIDI_PACKET_OVERHEAD 9 #define AMIDI_BUFFER_SIZE (AMIDI_PACKET_SIZE - AMIDI_PACKET_OVERHEAD) // JNI IDs (see android_media_midi.cpp) namespace android { namespace midi { // MidiDevice Fields extern jfieldID gFidMidiNativeHandle; // MidiDevice.mNativeHandle extern jfieldID gFidMidiDeviceServerBinder; // MidiDevice.mDeviceServerBinder extern jfieldID gFidMidiDeviceInfo; // MidiDevice.mDeviceInfo // MidiDeviceInfo Fields extern jfieldID mFidMidiDeviceId; // MidiDeviceInfo.mId }} using namespace android::midi; static std::mutex openMutex; // Ensure that the device can be connected just once to 1 thread //// Handy debugging function. //static void AMIDI_logBuffer(const uint8_t *data, size_t numBytes) { // for (size_t index = 0; index < numBytes; index++) { // ALOGI(" data @%zu [0x%X]", index, data[index]); // } //} /* * Device Functions */ /** * Retrieves information for the native MIDI device. * * device The Native API token for the device. This value is obtained from the * AMidiDevice_fromJava(). * outDeviceInfoPtr Receives the associated device info. * * Returns AMEDIA_OK or a negative error code. * - AMEDIA_ERROR_INVALID_PARAMETER * AMEDIA_ERROR_UNKNOWN */ static media_status_t AMIDI_API AMIDI_getDeviceInfo(const AMidiDevice *device, AMidiDeviceInfo *outDeviceInfoPtr) { if (device == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } MidiDeviceInfo deviceInfo; Status txResult = device->server->getDeviceInfo(&deviceInfo); if (!txResult.isOk()) { ALOGE("AMIDI_getDeviceInfo transaction error: %d", txResult.transactionError()); return AMEDIA_ERROR_UNKNOWN; } outDeviceInfoPtr->type = deviceInfo.getType(); outDeviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size(); outDeviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size(); return AMEDIA_OK; } media_status_t AMIDI_API AMidiDevice_fromJava(JNIEnv *env, jobject j_midiDeviceObj, AMidiDevice** devicePtrPtr) { if (j_midiDeviceObj == nullptr) { ALOGE("AMidiDevice_fromJava() invalid MidiDevice object."); return AMEDIA_ERROR_INVALID_OBJECT; } { std::lock_guard guard(openMutex); long handle = env->GetLongField(j_midiDeviceObj, gFidMidiNativeHandle); if (handle != 0) { // Already opened by someone. return AMEDIA_ERROR_INVALID_OBJECT; } jobject serverBinderObj = env->GetObjectField(j_midiDeviceObj, gFidMidiDeviceServerBinder); sp serverBinder = android::ibinderForJavaObject(env, serverBinderObj); if (serverBinder.get() == nullptr) { ALOGE("AMidiDevice_fromJava couldn't connect to native MIDI server."); return AMEDIA_ERROR_UNKNOWN; } // don't check allocation failures, just abort.. AMidiDevice* devicePtr = new AMidiDevice; devicePtr->server = new BpMidiDeviceServer(serverBinder); jobject midiDeviceInfoObj = env->GetObjectField(j_midiDeviceObj, gFidMidiDeviceInfo); devicePtr->deviceId = env->GetIntField(midiDeviceInfoObj, mFidMidiDeviceId); // Synchronize with the associated Java MidiDevice. env->SetLongField(j_midiDeviceObj, gFidMidiNativeHandle, (long)devicePtr); env->GetJavaVM(&devicePtr->javaVM); devicePtr->midiDeviceObj = env->NewGlobalRef(j_midiDeviceObj); if (AMIDI_getDeviceInfo(devicePtr, &devicePtr->deviceInfo) != AMEDIA_OK) { // This is weird, but maybe not fatal? ALOGE("AMidiDevice_fromJava couldn't retrieve attributes of native device."); } *devicePtrPtr = devicePtr; } return AMEDIA_OK; } media_status_t AMIDI_API AMidiDevice_release(const AMidiDevice *device) { if (device == nullptr || device->midiDeviceObj == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } JNIEnv* env; jint err = device->javaVM->GetEnv((void**)&env, JNI_VERSION_1_6); LOG_ALWAYS_FATAL_IF(err != JNI_OK, "AMidiDevice_release Error accessing JNIEnv err:%d", err); // Synchronize with the associated Java MidiDevice. { std::lock_guard guard(openMutex); long handle = env->GetLongField(device->midiDeviceObj, gFidMidiNativeHandle); if (handle == 0) { // Not opened as native. ALOGE("AMidiDevice_release() device not opened in native client."); return AMEDIA_ERROR_INVALID_OBJECT; } env->SetLongField(device->midiDeviceObj, gFidMidiNativeHandle, 0L); } env->DeleteGlobalRef(device->midiDeviceObj); delete device; return AMEDIA_OK; } int32_t AMIDI_API AMidiDevice_getType(const AMidiDevice *device) { if (device == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } return device->deviceInfo.type; } ssize_t AMIDI_API AMidiDevice_getNumInputPorts(const AMidiDevice *device) { if (device == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } return device->deviceInfo.inputPortCount; } ssize_t AMIDI_API AMidiDevice_getNumOutputPorts(const AMidiDevice *device) { if (device == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } return device->deviceInfo.outputPortCount; } /* * Port Helpers */ static media_status_t AMIDI_openPort(const AMidiDevice *device, int32_t portNumber, int type, AMIDI_Port **portPtr) { if (device == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } sp portToken(new BBinder()); unique_fd ufd; Status txResult = type == PORTTYPE_OUTPUT ? device->server->openOutputPort(portToken, portNumber, &ufd) : device->server->openInputPort(portToken, portNumber, &ufd); if (!txResult.isOk()) { ALOGE("AMIDI_openPort transaction error: %d", txResult.transactionError()); return AMEDIA_ERROR_UNKNOWN; } AMIDI_Port *port = new AMIDI_Port; port->state = MIDI_PORT_STATE_OPEN_IDLE; port->device = device; port->binderToken = portToken; port->ufd = std::move(ufd); *portPtr = port; return AMEDIA_OK; } static void AMIDI_closePort(AMIDI_Port *port) { if (port == nullptr) { return; } int portState = MIDI_PORT_STATE_OPEN_IDLE; while (!port->state.compare_exchange_weak(portState, MIDI_PORT_STATE_CLOSED)) { if (portState == MIDI_PORT_STATE_CLOSED) { return; // Already closed } } Status txResult = port->device->server->closePort(port->binderToken); if (!txResult.isOk()) { ALOGE("Transaction error closing MIDI port:%d", txResult.transactionError()); } delete port; } /* * Output (receiving) API */ media_status_t AMIDI_API AMidiOutputPort_open(const AMidiDevice *device, int32_t portNumber, AMidiOutputPort **outOutputPortPtr) { return AMIDI_openPort(device, portNumber, PORTTYPE_OUTPUT, (AMIDI_Port**)outOutputPortPtr); } /* * A little RAII (https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization) * class to ensure that the port state is correct irrespective of errors. */ class MidiReceiver { public: MidiReceiver(AMIDI_Port *port) : mPort(port) {} ~MidiReceiver() { // flag the port state to idle mPort->state.store(MIDI_PORT_STATE_OPEN_IDLE); } ssize_t receive(int32_t *opcodePtr, uint8_t *buffer, size_t maxBytes, size_t *numBytesReceivedPtr, int64_t *timestampPtr) { int portState = MIDI_PORT_STATE_OPEN_IDLE; // check to see if the port is idle, then set to active if (!mPort->state.compare_exchange_strong(portState, MIDI_PORT_STATE_OPEN_ACTIVE)) { // The port not idle or has been closed. return AMEDIA_ERROR_UNKNOWN; } struct pollfd checkFds[1] = { { mPort->ufd, POLLIN, 0 } }; if (poll(checkFds, 1, 0) < 1) { // Nothing there return 0; } uint8_t readBuffer[AMIDI_PACKET_SIZE]; ssize_t readCount = read(mPort->ufd, readBuffer, sizeof(readBuffer)); if (readCount == EINTR || readCount < 1) { return AMEDIA_ERROR_UNKNOWN; } // see Packet Format definition at the top of this file. size_t numMessageBytes = 0; *opcodePtr = readBuffer[0]; if (*opcodePtr == AMIDI_OPCODE_DATA && readCount >= AMIDI_PACKET_OVERHEAD) { numMessageBytes = readCount - AMIDI_PACKET_OVERHEAD; numMessageBytes = std::min(maxBytes, numMessageBytes); memcpy(buffer, readBuffer + 1, numMessageBytes); if (timestampPtr != nullptr) { memcpy(timestampPtr, readBuffer + readCount - sizeof(uint64_t), sizeof(*timestampPtr)); } } *numBytesReceivedPtr = numMessageBytes; return 1; } private: AMIDI_Port *mPort; }; ssize_t AMIDI_API AMidiOutputPort_receive(const AMidiOutputPort *outputPort, int32_t *opcodePtr, uint8_t *buffer, size_t maxBytes, size_t* numBytesReceivedPtr, int64_t *timestampPtr) { if (outputPort == nullptr || buffer == nullptr) { return -EINVAL; } return MidiReceiver((AMIDI_Port*)outputPort).receive(opcodePtr, buffer, maxBytes, numBytesReceivedPtr, timestampPtr); } void AMIDI_API AMidiOutputPort_close(const AMidiOutputPort *outputPort) { AMIDI_closePort((AMIDI_Port*)outputPort); } /* * Input (sending) API */ media_status_t AMIDI_API AMidiInputPort_open(const AMidiDevice *device, int32_t portNumber, AMidiInputPort **outInputPortPtr) { return AMIDI_openPort(device, portNumber, PORTTYPE_INPUT, (AMIDI_Port**)outInputPortPtr); } void AMIDI_API AMidiInputPort_close(const AMidiInputPort *inputPort) { AMIDI_closePort((AMIDI_Port*)inputPort); } static ssize_t AMIDI_makeSendBuffer( uint8_t *buffer, const uint8_t *data, size_t numBytes, uint64_t timestamp) { // Error checking will happen in the caller since this isn't an API function. buffer[0] = AMIDI_OPCODE_DATA; memcpy(buffer + 1, data, numBytes); memcpy(buffer + 1 + numBytes, ×tamp, sizeof(timestamp)); return numBytes + AMIDI_PACKET_OVERHEAD; } ssize_t AMIDI_API AMidiInputPort_send(const AMidiInputPort *inputPort, const uint8_t *buffer, size_t numBytes) { return AMidiInputPort_sendWithTimestamp(inputPort, buffer, numBytes, 0); } ssize_t AMIDI_API AMidiInputPort_sendWithTimestamp(const AMidiInputPort *inputPort, const uint8_t *data, size_t numBytes, int64_t timestamp) { if (inputPort == nullptr || data == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } // AMIDI_logBuffer(data, numBytes); uint8_t writeBuffer[AMIDI_BUFFER_SIZE + AMIDI_PACKET_OVERHEAD]; size_t numSent = 0; while (numSent < numBytes) { size_t blockSize = AMIDI_BUFFER_SIZE; blockSize = std::min(blockSize, numBytes - numSent); ssize_t numTransferBytes = AMIDI_makeSendBuffer(writeBuffer, data + numSent, blockSize, timestamp); ssize_t numWritten = write(((AMIDI_Port*)inputPort)->ufd, writeBuffer, numTransferBytes); if (numWritten < 0) { break; // error so bail out. } if (numWritten < numTransferBytes) { ALOGE("AMidiInputPort_sendWithTimestamp Couldn't write MIDI data buffer." " requested:%zu, written%zu",numTransferBytes, numWritten); break; // bail } numSent += numWritten - AMIDI_PACKET_OVERHEAD; } return numSent; } media_status_t AMIDI_API AMidiInputPort_sendFlush(const AMidiInputPort *inputPort) { if (inputPort == nullptr) { return AMEDIA_ERROR_INVALID_PARAMETER; } uint8_t opCode = AMIDI_OPCODE_FLUSH; ssize_t numTransferBytes = 1; ssize_t numWritten = write(((AMIDI_Port*)inputPort)->ufd, &opCode, numTransferBytes); if (numWritten < numTransferBytes) { ALOGE("AMidiInputPort_flush Couldn't write MIDI flush. requested:%zd, written:%zd", numTransferBytes, numWritten); return AMEDIA_ERROR_UNSUPPORTED; } return AMEDIA_OK; }