| /* |
| * 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 <poll.h> |
| #include <unistd.h> |
| |
| #include <binder/Binder.h> |
| #include <android_util_Binder.h> |
| #include <utils/Log.h> |
| |
| #include <core_jni_helpers.h> |
| |
| #include "android/media/midi/BpMidiDeviceServer.h" |
| #include "media/MidiDeviceInfo.h" |
| |
| #include "include/midi.h" |
| #include "midi_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<IBinder> 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) |
| |
| // MidiDevice Fields |
| static jobject deviceClassGlobalRef = nullptr; // A GlobalRef for MidiDevice Class |
| static jfieldID fidDeviceClosed = nullptr; // MidiDevice.mIsDeviceClosed |
| static jfieldID fidNativeHandle = nullptr; // MidiDevice.mNativeHandle |
| static jfieldID fidDeviceServerBinder = nullptr; // MidiDevice.mDeviceServerBinder |
| static jfieldID fidDeviceInfo = nullptr; // MidiDevice.mDeviceInfo |
| |
| // MidiDeviceInfo Fields |
| static jobject deviceInfoClassGlobalRef = nullptr; // A GlobalRef for MidiDeviceInfoClass |
| static jfieldID fidDeviceId = nullptr; // MidiDeviceInfo.mId |
| |
| static std::mutex openMutex; // Ensure that the device can be connected just once to 1 thread |
| |
| static void AMIDI_initJNI(JNIEnv *env) { |
| jclass deviceClass = android::FindClassOrDie(env, "android/media/midi/MidiDevice"); |
| deviceClassGlobalRef = env->NewGlobalRef(deviceClass); |
| |
| // MidiDevice Field IDs |
| fidDeviceClosed = android::GetFieldIDOrDie(env, deviceClass, "mIsDeviceClosed", "Z"); |
| fidNativeHandle = android::GetFieldIDOrDie(env, deviceClass, "mNativeHandle", "J"); |
| fidDeviceServerBinder = android::GetFieldIDOrDie(env, deviceClass, |
| "mDeviceServerBinder", "Landroid/os/IBinder;"); |
| fidDeviceInfo = android::GetFieldIDOrDie(env, deviceClass, |
| "mDeviceInfo", "Landroid/media/midi/MidiDeviceInfo;"); |
| |
| // MidiDeviceInfo Field IDs |
| jclass deviceInfoClass = android::FindClassOrDie(env, "android/media/midi/MidiDeviceInfo"); |
| deviceInfoClassGlobalRef = env->NewGlobalRef(deviceInfoClass); |
| fidDeviceId = android::GetFieldIDOrDie(env, deviceInfoClass, "mId", "I"); |
| } |
| |
| //// 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) |
| { |
| // Ensures JNI initialization is performed just once. |
| static std::once_flag initCallFlag; |
| std::call_once(initCallFlag, AMIDI_initJNI, env); |
| |
| if (j_midiDeviceObj == nullptr) { |
| ALOGE("AMidiDevice_fromJava() invalid MidiDevice object."); |
| return AMEDIA_ERROR_INVALID_OBJECT; |
| } |
| |
| { |
| std::lock_guard<std::mutex> guard(openMutex); |
| |
| long handle = env->GetLongField(j_midiDeviceObj, fidNativeHandle); |
| if (handle != 0) { |
| // Already opened by someone. |
| return AMEDIA_ERROR_INVALID_OBJECT; |
| } |
| |
| jobject serverBinderObj = env->GetObjectField(j_midiDeviceObj, fidDeviceServerBinder); |
| sp<IBinder> 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, fidDeviceInfo); |
| devicePtr->deviceId = env->GetIntField(midiDeviceInfoObj, fidDeviceId); |
| |
| // Synchronize with the associated Java MidiDevice. |
| env->SetLongField(j_midiDeviceObj, fidNativeHandle, (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. |
| // env->CallVoidMethod(j_midiDeviceObj, midClearNativeHandle); |
| { |
| std::lock_guard<std::mutex> guard(openMutex); |
| long handle = env->GetLongField(device->midiDeviceObj, fidNativeHandle); |
| 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, fidNativeHandle, 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<BBinder> 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) { |
| *timestampPtr = *(uint64_t*)(readBuffer + readCount - sizeof(uint64_t)); |
| } |
| } |
| *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; |
| } |
| |