444 lines
14 KiB
C++
444 lines
14 KiB
C++
/*
|
|
* 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"
|
|
|
|
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();
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* 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 = 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;
|
|
}
|
|
|