| /* | |
| * 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 "MidiDeviceInfo.h" | |
| #include "include/amidi/AMidi.h" | |
| #include "amidi_internal.h" | |
| #include <mutex> | |
| 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) | |
| // 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_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("%s server exception code: %d", __func__, txResult.exceptionCode()); | |
| return AMEDIA_ERROR_UNKNOWN; | |
| } | |
| outDeviceInfoPtr->type = deviceInfo.getType(); | |
| outDeviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size(); | |
| outDeviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size(); | |
| outDeviceInfoPtr->defaultProtocol = deviceInfo.getDefaultProtocol(); | |
| 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<std::mutex> 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<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, 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<std::mutex> 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; | |
| } | |
| AMidiDevice_Protocol AMIDI_API AMidiDevice_getDefaultProtocol(const AMidiDevice *device) { | |
| if (device == nullptr) { | |
| return AMIDI_DEVICE_PROTOCOL_UNKNOWN; | |
| } | |
| return static_cast<AMidiDevice_Protocol>(device->deviceInfo.defaultProtocol); | |
| } | |
| /* | |
| * 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("%s server exception code: %d", __func__, txResult.exceptionCode()); | |
| 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("%s server exception code: %d", __func__, txResult.exceptionCode()); | |
| } | |
| 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 = TEMP_FAILURE_RETRY(read(mPort->ufd, readBuffer, sizeof(readBuffer))); | |
| if (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 || numBytes < 0 || timestamp < 0) { | |
| return AMEDIA_ERROR_INVALID_PARAMETER; | |
| } | |
| if (numBytes == 0) { | |
| return 0; | |
| } | |
| // 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 = TEMP_FAILURE_RETRY(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 = TEMP_FAILURE_RETRY(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; | |
| } | |