diff options
25 files changed, 2282 insertions, 0 deletions
diff --git a/media/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java index f79cd06a3b5a..4b43260ece01 100644 --- a/media/java/android/media/midi/MidiDevice.java +++ b/media/java/android/media/midi/MidiDevice.java @@ -35,6 +35,10 @@ import java.io.IOException; * Instances of this class are created by {@link MidiManager#openDevice}. */ public final class MidiDevice implements Closeable { + static { + System.loadLibrary("media_jni"); + } + private static final String TAG = "MidiDevice"; private final MidiDeviceInfo mDeviceInfo; @@ -43,6 +47,7 @@ public final class MidiDevice implements Closeable { private final IBinder mClientToken; private final IBinder mDeviceToken; private boolean mIsDeviceClosed; + private boolean mIsMirroredToNative; private final CloseGuard mGuard = CloseGuard.get(); @@ -209,10 +214,45 @@ public final class MidiDevice implements Closeable { } } + /** + * Makes Midi Device available to the Native API + * @hide + */ + public void mirrorToNative() throws IOException { + if (mIsDeviceClosed || mIsMirroredToNative) { + return; + } + + int result = mirrorToNative(mDeviceServer.asBinder(), mDeviceInfo.getId()); + if (result != 0) { + throw new IOException("Failed mirroring to native: " + result); + } + + mIsMirroredToNative = true; + } + + /** + * Makes Midi Device no longer available to the Native API + * @hide + */ + public void removeFromNative() throws IOException { + if (!mIsMirroredToNative) { + return; + } + + int result = removeFromNative(mDeviceInfo.getId()); + if (result != 0) { + throw new IOException("Failed removing from native: " + result); + } + + mIsMirroredToNative = false; + } + @Override public void close() throws IOException { synchronized (mGuard) { if (!mIsDeviceClosed) { + removeFromNative(); mGuard.close(); mIsDeviceClosed = true; try { @@ -238,4 +278,7 @@ public final class MidiDevice implements Closeable { public String toString() { return ("MidiDevice: " + mDeviceInfo.toString()); } + + private native int mirrorToNative(IBinder deviceServerBinder, int uid); + private native int removeFromNative(int uid); } diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 861ed0a3b08a..23bf3d6711f6 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -26,6 +26,7 @@ LOCAL_SRC_FILES:= \ android_mtp_MtpDatabase.cpp \ android_mtp_MtpDevice.cpp \ android_mtp_MtpServer.cpp \ + midi/android_media_midi_MidiDevice.cpp \ LOCAL_SHARED_LIBRARIES := \ libandroid_runtime \ @@ -34,6 +35,7 @@ LOCAL_SHARED_LIBRARIES := \ libbinder \ libmedia \ libmediadrm \ + libmidi \ libskia \ libui \ liblog \ @@ -55,6 +57,7 @@ LOCAL_C_INCLUDES += \ external/tremor/Tremor \ frameworks/base/core/jni \ frameworks/base/libs/hwui \ + frameworks/base/media/native \ frameworks/av/media/libmedia \ frameworks/av/media/libstagefright \ frameworks/av/media/mtp \ diff --git a/media/jni/midi/android_media_midi_MidiDevice.cpp b/media/jni/midi/android_media_midi_MidiDevice.cpp new file mode 100644 index 000000000000..1e54bacc469b --- /dev/null +++ b/media/jni/midi/android_media_midi_MidiDevice.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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_NDEBUG 0 +#define LOG_TAG "Midi-JNI" + +#include <android_util_Binder.h> +#include <midi/MidiDeviceRegistry.h> +#include <nativehelper/jni.h> +#include <utils/Log.h> + +using namespace android; +using namespace android::media::midi; + +extern "C" jint Java_android_media_midi_MidiDevice_mirrorToNative( + JNIEnv *env, jobject thiz, jobject midiDeviceServer, jint id) +{ + (void)thiz; + sp<IBinder> serverBinder = ibinderForJavaObject(env, midiDeviceServer); + if (serverBinder.get() == NULL) { + ALOGE("Could not obtain IBinder from passed jobject"); + return -EINVAL; + } + // return MidiDeviceManager::getInstance().addDevice(serverBinder, uid); + return MidiDeviceRegistry::getInstance().addDevice( + new BpMidiDeviceServer(serverBinder), id); +} + +extern "C" jint Java_android_media_midi_MidiDevice_removeFromNative( + JNIEnv *env, jobject thiz, jint uid) +{ + (void)env; + (void)thiz; + // return MidiDeviceManager::getInstance().removeDevice(uid); + return MidiDeviceRegistry::getInstance().removeDevice(uid); +} diff --git a/media/native/midi/Android.bp b/media/native/midi/Android.bp new file mode 100644 index 000000000000..3500805d2794 --- /dev/null +++ b/media/native/midi/Android.bp @@ -0,0 +1,21 @@ +// Copyright (C) 2017 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. + +// The headers module is in frameworks/media/native/midi/Android.bp. +ndk_library { + name: "libmidi.ndk", + symbol_file: "libmidi.map.txt", + first_version: "26", +// unversioned_until: "current", +} diff --git a/media/native/midi/Android.mk b/media/native/midi/Android.mk new file mode 100644 index 000000000000..b91c430e2bca --- /dev/null +++ b/media/native/midi/Android.mk @@ -0,0 +1,22 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + ../../java/android/media/midi/IMidiDeviceServer.aidl \ + midi.cpp \ + MidiDeviceRegistry.cpp \ + MidiPortRegistry.cpp + +LOCAL_AIDL_INCLUDES := \ + $(FRAMEWORKS_BASE_JAVA_SRC_DIRS) \ + frameworks/native/aidl/binder + +LOCAL_CFLAGS += -Wall -Werror -O0 + +LOCAL_MODULE := libmidi +LOCAL_MODULE_TAGS := optional + +LOCAL_SHARED_LIBRARIES := liblog libbinder libutils libmedia + +include $(BUILD_SHARED_LIBRARY) diff --git a/media/native/midi/MidiDeviceRegistry.cpp b/media/native/midi/MidiDeviceRegistry.cpp new file mode 100644 index 000000000000..8854a08a0a45 --- /dev/null +++ b/media/native/midi/MidiDeviceRegistry.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017 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 "MidiDeviceRegistry.h" + +namespace android { + +ANDROID_SINGLETON_STATIC_INSTANCE(media::midi::MidiDeviceRegistry); + +namespace media { +namespace midi { + +MidiDeviceRegistry::MidiDeviceRegistry() : mNextDeviceToken(1) { +} + +status_t MidiDeviceRegistry::addDevice(sp<BpMidiDeviceServer> server, int32_t deviceId) { + if (server.get() == nullptr) { + return -EINVAL; + } + + std::lock_guard<std::mutex> guard(mMapsLock); + mServers[deviceId] = server; + return OK; +} + +status_t MidiDeviceRegistry::removeDevice(int32_t deviceId) { + std::lock_guard<std::mutex> guard(mMapsLock); + mServers.erase(deviceId); + const auto& iter = mUidToToken.find(deviceId); + if (iter != mUidToToken.end()) { + mTokenToUid.erase(iter->second); + mUidToToken.erase(iter); + } + return OK; +} + +//NOTE: This creates an entry if not found, or returns an existing one. +status_t MidiDeviceRegistry::obtainDeviceToken(int32_t deviceId, AMIDI_Device *deviceTokenPtr) { + std::lock_guard<std::mutex> guard(mMapsLock); + const auto& serversIter = mServers.find(deviceId); + if (serversIter == mServers.end()) { + // Not found. + return -EINVAL; + } + + const auto& iter = mUidToToken.find(deviceId); + if (iter != mUidToToken.end()) { + *deviceTokenPtr = iter->second; + } else { + *deviceTokenPtr = mNextDeviceToken++; + mTokenToUid[*deviceTokenPtr] = deviceId; + mUidToToken[deviceId] = *deviceTokenPtr; + } + return OK; +} + +status_t MidiDeviceRegistry::releaseDevice(AMIDI_Device deviceToken) { + std::lock_guard<std::mutex> guard(mMapsLock); + const auto& iter = mTokenToUid.find(deviceToken); + if (iter == mTokenToUid.end()) { + // Not found + return -EINVAL; + } + + mServers.erase(iter->second); + mUidToToken.erase(iter->second); + mTokenToUid.erase(iter); + return OK; +} + +status_t MidiDeviceRegistry::getDeviceByToken( + AMIDI_Device deviceToken, sp<BpMidiDeviceServer> *devicePtr) { + std::lock_guard<std::mutex> guard(mMapsLock); + int32_t id = -1; + { + const auto& iter = mTokenToUid.find(deviceToken); + if (iter == mTokenToUid.end()) { + return -EINVAL; + } + id = iter->second; + } + const auto& iter = mServers.find(id); + if (iter == mServers.end()) { + return -EINVAL; + } + + *devicePtr = iter->second; + return OK; +} + +} // namespace midi +} // namespace media +} // namespace android diff --git a/media/native/midi/MidiDeviceRegistry.h b/media/native/midi/MidiDeviceRegistry.h new file mode 100644 index 000000000000..93be7338385b --- /dev/null +++ b/media/native/midi/MidiDeviceRegistry.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 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. + */ + +#ifndef ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_ +#define ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_ + +#include <map> +#include <mutex> + +#include <binder/IBinder.h> +#include <utils/Errors.h> +#include <utils/Singleton.h> + +#include "android/media/midi/BpMidiDeviceServer.h" +#include "midi.h" + +namespace android { +namespace media { +namespace midi { + +/* + * Maintains a thread-safe, (singleton) list of MIDI devices with associated Binder interfaces, + * which are exposed to the Native API via (Java) MidiDevice.mirrorToNative() & + * MidiDevice.removeFromNative(). + * (Called via MidiDeviceManager::addDevice() MidiManager::removeDevice()). + */ +class MidiDeviceRegistry : public Singleton<MidiDeviceRegistry> { + public: + /* Add a MIDI Device to the registry. + * + * server The Binder interface to the MIDI device server. + * deviceUId The unique ID of the device obtained from + * the Java API via MidiDeviceInfo.getId(). + */ + status_t addDevice(sp<BpMidiDeviceServer> server, int32_t deviceId); + + /* Remove the device (and associated server) from the Device registry. + * + * deviceUid The ID of the device which was used in the call to addDevice(). + */ + status_t removeDevice(int32_t deviceId); + + /* Gets a device token associated with the device ID. This is used by the + * native API to identify/access the device. + * Multiple calls without releasing the token will return the same value. + * + * deviceUid The ID of the device. + * deviceTokenPtr Receives the device (native) token associated with the device ID. + * returns: OK on success, error code otherwise. + */ + status_t obtainDeviceToken(int32_t deviceId, AMIDI_Device *deviceTokenPtr); + + /* + * Releases the native API device token associated with a MIDI device. + * + * deviceToken The device (native) token associated with the device ID. + */ + status_t releaseDevice(AMIDI_Device deviceToken); + + /* + * Gets the Device server binder interface associated with the device token. + * + * deviceToken The device (native) token associated with the device ID. + */ + status_t getDeviceByToken(AMIDI_Device deviceToken, sp<BpMidiDeviceServer> *devicePtr); + + private: + friend class Singleton<MidiDeviceRegistry>; + MidiDeviceRegistry(); + + // Access Mutex + std::mutex mMapsLock; + + // maps device IDs to servers + std::map<int32_t, sp<BpMidiDeviceServer>> mServers; + + // maps device tokens to device ID + std::map<AMIDI_Device, int32_t> mTokenToUid; + + // maps device IDs to device tokens + std::map<int32_t, AMIDI_Device> mUidToToken; + + // Value of next device token to dole out. + AMIDI_Device mNextDeviceToken; +}; + +} // namespace midi +} // namespace media +} // namespace android + +#endif // ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_ diff --git a/media/native/midi/MidiPortRegistry.cpp b/media/native/midi/MidiPortRegistry.cpp new file mode 100644 index 000000000000..fa70af8d0947 --- /dev/null +++ b/media/native/midi/MidiPortRegistry.cpp @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2017 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 "MidiPortRegistry.h" + +namespace android { + +ANDROID_SINGLETON_STATIC_INSTANCE(media::midi::MidiPortRegistry); + +namespace media { +namespace midi { + +//TODO Note that these 2 are identical +struct MidiPortRegistry::OutputPort { + AMIDI_Device device; + sp<IBinder> binderToken; + base::unique_fd ufd; +}; + +struct MidiPortRegistry::InputPort { + AMIDI_Device device; + sp<IBinder> binderToken; + base::unique_fd ufd; +}; + +MidiPortRegistry::MidiPortRegistry() : mNextOutputPortToken(0), mNextInputPortToken(0) { +} + +status_t MidiPortRegistry::addOutputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_OutputPort *portPtr) { + *portPtr = mNextOutputPortToken++; + + OutputPortEntry* portEntry = new OutputPortEntry; + portEntry->port = new OutputPort; + portEntry->state = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE; + portEntry->port = new OutputPort; + portEntry->port->device = device; + portEntry->port->binderToken = portToken; + portEntry->port->ufd = std::move(ufd); + + mOutputPortMap[*portPtr] = portEntry; + + return OK; +} + +status_t MidiPortRegistry::removeOutputPort( + AMIDI_OutputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr) { + OutputPortMap::iterator itr = mOutputPortMap.find(port); + if (itr == mOutputPortMap.end()) { + return -EINVAL; + } + + OutputPortEntry *entry = mOutputPortMap[port]; + int portState = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE; + while (!entry->state.compare_exchange_weak(portState, MIDI_OUTPUT_PORT_STATE_CLOSED)) { + if (portState == MIDI_OUTPUT_PORT_STATE_CLOSED) { + return -EINVAL; // Already closed + } + } + *devicePtr = entry->port->device; + *portTokenPtr = entry->port->binderToken; + delete entry->port; + entry->port = nullptr; + + mOutputPortMap.erase(itr); + + return OK; +} + +status_t MidiPortRegistry::getOutputPortFdAndLock( + AMIDI_OutputPort port, base::unique_fd **ufdPtr) { + if (mOutputPortMap.find(port) == mOutputPortMap.end()) { + return -EINVAL; + } + + OutputPortEntry *entry = mOutputPortMap[port]; + int portState = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE; + if (!entry->state.compare_exchange_strong(portState, MIDI_OUTPUT_PORT_STATE_OPEN_ACTIVE)) { + // The port has been closed. + return -EPIPE; + } + *ufdPtr = &entry->port->ufd; + + return OK; +} + +status_t MidiPortRegistry::unlockOutputPort(AMIDI_OutputPort port) { + if (mOutputPortMap.find(port) == mOutputPortMap.end()) { + return -EINVAL; + } + + OutputPortEntry *entry = mOutputPortMap[port]; + entry->state.store(MIDI_OUTPUT_PORT_STATE_OPEN_IDLE); + return OK; +} + +status_t MidiPortRegistry::addInputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_InputPort *portPtr) { + *portPtr = mNextInputPortToken++; + + InputPortEntry *entry = new InputPortEntry; + + entry->state = MIDI_INPUT_PORT_STATE_OPEN_IDLE; + entry->port = new InputPort; + entry->port->device = device; + entry->port->binderToken = portToken; + entry->port->ufd = std::move(ufd); + + mInputPortMap[*portPtr] = entry; + + return OK; +} + +status_t MidiPortRegistry::removeInputPort( + AMIDI_InputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr) { + InputPortMap::iterator itr = mInputPortMap.find(port); + if (itr == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + int portState = MIDI_INPUT_PORT_STATE_OPEN_IDLE; + while (!entry->state.compare_exchange_weak(portState, MIDI_INPUT_PORT_STATE_CLOSED)) { + if (portState == MIDI_INPUT_PORT_STATE_CLOSED) return -EINVAL; // Already closed + } + + *devicePtr = entry->port->device; + *portTokenPtr = entry->port->binderToken; + delete entry->port; + entry->port = nullptr; + + mInputPortMap.erase(itr); + + return OK; +} + +status_t MidiPortRegistry::getInputPortFd(AMIDI_InputPort port, base::unique_fd **ufdPtr) { + if (mInputPortMap.find(port) == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + + *ufdPtr = &entry->port->ufd; + + return OK; +} + +status_t MidiPortRegistry::getInputPortFdAndLock(AMIDI_InputPort port, base::unique_fd **ufdPtr) { + if (mInputPortMap.find(port) == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + + int portState = MIDI_INPUT_PORT_STATE_OPEN_IDLE; + if (!entry->state.compare_exchange_strong(portState, MIDI_INPUT_PORT_STATE_OPEN_ACTIVE)) { + // The port has been closed. + return -EPIPE; + } + *ufdPtr = &entry->port->ufd; + return OK; +} + +status_t MidiPortRegistry::MidiPortRegistry::unlockInputPort(AMIDI_InputPort port) { + if (mInputPortMap.find(port) == mInputPortMap.end()) { + return -EINVAL; + } + + InputPortEntry *entry = mInputPortMap[port]; + entry->state.store(MIDI_INPUT_PORT_STATE_OPEN_IDLE); + return OK; +} + +} // namespace midi +} // namespace media +} // namespace android diff --git a/media/native/midi/MidiPortRegistry.h b/media/native/midi/MidiPortRegistry.h new file mode 100644 index 000000000000..f1ffb78e98ab --- /dev/null +++ b/media/native/midi/MidiPortRegistry.h @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2017 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. + */ + +#ifndef ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_ +#define ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_ + +#include <atomic> +#include <map> + +#include <android-base/unique_fd.h> +#include <binder/IBinder.h> +#include <utils/Errors.h> +#include <utils/Singleton.h> + +#include "midi.h" + +namespace android { +namespace media { +namespace midi { + +/* + * Maintains lists of all active input and output MIDI ports and controls access to them. Provides + * exclusive access to specific MIDI ports. + */ +class MidiPortRegistry : public Singleton<MidiPortRegistry> { + public: + /* + * Creates an output port entry and associates it with the specified MIDI device. + * Called by AMIDI_openOutputPort(); + * + * device The native API device ID. + * portToken The port token (returned from the device server). + * udf File descriptor for the data port associated with the MIDI output port. + * portPtr Receives the native API port ID of the port being opened. + */ + status_t addOutputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_OutputPort *portPtr); + + /* + * Removes for the output port list a previously added output port. + * Called by AMIDI_closeOutputPort(); + * + * port The native API port ID of the port being closed. + * devicePtr Receives the native API device ID associated with the port. + * portTokenPtr Receives the binder token associated with the port. + */ + status_t removeOutputPort( + AMIDI_OutputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr); + + /* + * Creates an input port entry and associates it with the specified MIDI device. + * Called by AMIDI_openInputPort(); + * + * device The native API device ID. + * portToken The port token (returned from the device server). + * udf File descriptor for the data port associated with the MIDI input port. + * portPtr Receives the native API port ID of the port being opened. + */ + status_t addInputPort( + AMIDI_Device device, + sp<IBinder> portToken, + base::unique_fd &&ufd, + AMIDI_InputPort *portPtr); + + /* + * Removes for the input port list a previously added input port. + * Called by AMIDI_closeINputPort(); + * + * port The native API port ID of the port being closed. + * devicePtr Receives the native API device ID associated with the port. + * portTokenPtr Receives the binder token associated with the port. + */ + status_t removeInputPort( + AMIDI_InputPort port, + AMIDI_Device *devicePtr, + sp<IBinder> *portTokenPtr); + + /* + * Retrieves an exclusive-access file descriptor for an output port. + * Called from AMIDI_receive(). + * + * port The native API id of the output port. + * ufdPtr Receives the exclusive-access file descriptor for the output port. + */ + status_t getOutputPortFdAndLock(AMIDI_OutputPort port, base::unique_fd **ufdPtr); + + /* + * Releases exclusive-access to the port and invalidates the previously received file + * descriptor. + * Called from AMIDI_receive(). + * + * port The native API id of the output port. + */ + status_t unlockOutputPort(AMIDI_OutputPort port); + + /* + * Retrieves an exclusive-access file descriptor for an input port. + * (Not being used as (perhaps) AMIDI_sendWithTimestamp() doesn't need exclusive access + * to the port). + * + * port The native API id of the input port. + * ufdPtr Receives the exclusive-access file descriptor for the input port. + */ + status_t getInputPortFdAndLock(AMIDI_InputPort port, base::unique_fd **ufdPtr); + + /* + * Releases exclusive-access to the port and invalidates the previously received file + * descriptor. + * (Not used. See above). + * + * port The native API id of the input port. + */ + status_t unlockInputPort(AMIDI_InputPort port); + + /* + * Retrieves an unlocked (multi-access) file descriptor for an input port. + * Used by AMIDI_sendWith(), AMIDI_sendWithTimestamp & AMIDI_flush. + * + * port The native API id of the input port. + * ufdPtr Receives the multi-access file descriptor for the input port. + */ + status_t getInputPortFd(AMIDI_InputPort port, base::unique_fd **ufdPtr); + + private: + friend class Singleton<MidiPortRegistry>; + MidiPortRegistry(); + + /* + * Output (data receiving) ports. + */ + struct OutputPort; + enum { + MIDI_OUTPUT_PORT_STATE_CLOSED = 0, + MIDI_OUTPUT_PORT_STATE_OPEN_IDLE, + MIDI_OUTPUT_PORT_STATE_OPEN_ACTIVE + }; + + struct OutputPortEntry { + std::atomic_int state; + OutputPort *port; + }; + + typedef std::map<AMIDI_OutputPort, OutputPortEntry*> OutputPortMap; + // Access is synchronized per record via 'state' field. + std::atomic<AMIDI_OutputPort> mNextOutputPortToken; + OutputPortMap mOutputPortMap; + + /* + * Input (data sending) ports. + */ + struct InputPort; + enum { + MIDI_INPUT_PORT_STATE_CLOSED = 0, + MIDI_INPUT_PORT_STATE_OPEN_IDLE, + MIDI_INPUT_PORT_STATE_OPEN_ACTIVE + }; + + struct InputPortEntry { + std::atomic_int state; + InputPort *port; + }; + + typedef std::map<AMIDI_OutputPort, InputPortEntry*> InputPortMap; + // Access is synchronized per record via 'state' field. + std::atomic<AMIDI_InputPort> mNextInputPortToken; + InputPortMap mInputPortMap; + +}; + +} // namespace midi +} // namespace media +} // namespace android + +#endif // ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_ diff --git a/media/native/midi/midi.cpp b/media/native/midi/midi.cpp new file mode 100644 index 000000000000..1bf0bd080e94 --- /dev/null +++ b/media/native/midi/midi.cpp @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2017 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 <utils/Errors.h> +#include <utils/Log.h> + +#include "android/media/midi/BpMidiDeviceServer.h" +#include "media/MidiDeviceInfo.h" +#include "MidiDeviceRegistry.h" +#include "MidiPortRegistry.h" + +#include "midi.h" + +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; +using android::media::midi::BpMidiDeviceServer; +using android::media::midi::MidiDeviceInfo; +using android::media::midi::MidiDeviceRegistry; +using android::media::midi::MidiPortRegistry; + +#define SIZE_MIDIRECEIVEBUFFER AMIDI_BUFFER_SIZE + +/* 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. + */ + +status_t AMIDI_getDeviceById(int32_t id, AMIDI_Device *devicePtr) { + return MidiDeviceRegistry::getInstance().obtainDeviceToken(id, devicePtr); +} + +status_t AMIDI_getDeviceInfo(AMIDI_Device device, AMIDI_DeviceInfo *deviceInfoPtr) { + sp<BpMidiDeviceServer> deviceServer; + status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_getDeviceInfo bad device token %d: %d", device, result); + return result; + } + + MidiDeviceInfo deviceInfo; + Status txResult = deviceServer->getDeviceInfo(&deviceInfo); + if (!txResult.isOk()) { + ALOGE("AMIDI_getDeviceInfo transaction error: %d", txResult.transactionError()); + return txResult.transactionError(); + } + + deviceInfoPtr->type = deviceInfo.getType(); + deviceInfoPtr->uid = deviceInfo.getUid(); + deviceInfoPtr->isPrivate = deviceInfo.isPrivate(); + deviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size(); + deviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size(); + return OK; +} + +/* + * Output (receiving) API + */ +status_t AMIDI_openOutputPort(AMIDI_Device device, int portNumber, AMIDI_OutputPort *outputPortPtr) { + sp<BpMidiDeviceServer> deviceServer; + status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_openOutputPort bad device token %d: %d", device, result); + return result; + } + + sp<BBinder> portToken(new BBinder()); + unique_fd ufd; + Status txResult = deviceServer->openOutputPort(portToken, portNumber, &ufd); + if (!txResult.isOk()) { + ALOGE("AMIDI_openOutputPort transaction error: %d", txResult.transactionError()); + return txResult.transactionError(); + } + + result = MidiPortRegistry::getInstance().addOutputPort( + device, portToken, std::move(ufd), outputPortPtr); + if (result != OK) { + ALOGE("AMIDI_openOutputPort port registration error: %d", result); + // Close port + return result; + } + return OK; +} + +ssize_t AMIDI_receive(AMIDI_OutputPort outputPort, AMIDI_Message *messages, ssize_t maxMessages) { + unique_fd *ufd; + // TODO: May return a nicer self-unlocking object + status_t result = MidiPortRegistry::getInstance().getOutputPortFdAndLock(outputPort, &ufd); + if (result != OK) { + return result; + } + + ssize_t messagesRead = 0; + while (messagesRead < maxMessages) { + struct pollfd checkFds[1] = { { *ufd, POLLIN, 0 } }; + int pollResult = poll(checkFds, 1, 0); + if (pollResult < 1) { + result = android::INVALID_OPERATION; + break; + } + + AMIDI_Message *message = &messages[messagesRead]; + uint8_t readBuffer[AMIDI_PACKET_SIZE]; + memset(readBuffer, 0, sizeof(readBuffer)); + ssize_t readCount = read(*ufd, readBuffer, sizeof(readBuffer)); + if (readCount == EINTR) { + continue; + } + if (readCount < 1) { + result = android::NOT_ENOUGH_DATA; + break; + } + + // set Packet Format definition at the top of this file. + size_t dataSize = 0; + message->opcode = readBuffer[0]; + message->timestamp = 0; + if (message->opcode == AMIDI_OPCODE_DATA && readCount >= AMIDI_PACKET_OVERHEAD) { + dataSize = readCount - AMIDI_PACKET_OVERHEAD; + if (dataSize) { + memcpy(message->buffer, readBuffer + 1, dataSize); + } + message->timestamp = *(uint64_t*) (readBuffer + readCount - sizeof(uint64_t)); + } + message->len = dataSize; + ++messagesRead; + } + + MidiPortRegistry::getInstance().unlockOutputPort(outputPort); + return result == OK ? messagesRead : result; +} + +status_t AMIDI_closeOutputPort(AMIDI_OutputPort outputPort) { + AMIDI_Device device; + sp<IBinder> portToken; + status_t result = + MidiPortRegistry::getInstance().removeOutputPort(outputPort, &device, &portToken); + if (result != OK) { + return result; + } + + sp<BpMidiDeviceServer> deviceServer; + result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + return result; + } + + Status txResult = deviceServer->closePort(portToken); + if (!txResult.isOk()) { + return txResult.transactionError(); + } + return OK; +} + +/* + * Input (sending) API + */ +status_t AMIDI_openInputPort(AMIDI_Device device, int portNumber, AMIDI_InputPort *inputPortPtr) { + sp<BpMidiDeviceServer> deviceServer; + status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_openInputPort bad device token %d: %d", device, result); + return result; + } + + sp<BBinder> portToken(new BBinder()); + unique_fd ufd; // this is the file descriptor of the "receive" port s + Status txResult = deviceServer->openInputPort(portToken, portNumber, &ufd); + if (!txResult.isOk()) { + ALOGE("AMIDI_openInputPort transaction error: %d", txResult.transactionError()); + return txResult.transactionError(); + } + + result = MidiPortRegistry::getInstance().addInputPort( + device, portToken, std::move(ufd), inputPortPtr); + if (result != OK) { + ALOGE("AMIDI_openInputPort port registration error: %d", result); + // Close port + return result; + } + + return OK; +} + +status_t AMIDI_closeInputPort(AMIDI_InputPort inputPort) { + AMIDI_Device device; + sp<IBinder> portToken; + status_t result = MidiPortRegistry::getInstance().removeInputPort( + inputPort, &device, &portToken); + if (result != OK) { + ALOGE("AMIDI_closeInputPort remove port error: %d", result); + return result; + } + + sp<BpMidiDeviceServer> deviceServer; + result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer); + if (result != OK) { + ALOGE("AMIDI_closeInputPort can't find device error: %d", result); + return result; + } + + Status txResult = deviceServer->closePort(portToken); + if (!txResult.isOk()) { + result = txResult.transactionError(); + ALOGE("AMIDI_closeInputPort transaction error: %d", result); + return result; + } + + return OK; +} + +ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort /*inputPort*/) { + return SIZE_MIDIRECEIVEBUFFER; +} + +static ssize_t AMIDI_makeSendBuffer(uint8_t *buffer, uint8_t *data, ssize_t numBytes, uint64_t timestamp) { + buffer[0] = AMIDI_OPCODE_DATA; + memcpy(buffer + 1, data, numBytes); + memcpy(buffer + 1 + numBytes, ×tamp, sizeof(timestamp)); + return numBytes + AMIDI_PACKET_OVERHEAD; +} + +// Handy debugging function. +//static void AMIDI_logBuffer(uint8_t *data, size_t numBytes) { +// for (size_t index = 0; index < numBytes; index++) { +// ALOGI(" data @%zu [0x%X]", index, data[index]); +// } +//} + +ssize_t AMIDI_send(AMIDI_InputPort inputPort, uint8_t *buffer, ssize_t numBytes) { + return AMIDI_sendWithTimestamp(inputPort, buffer, numBytes, 0); +} + +ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort inputPort, uint8_t *data, + ssize_t numBytes, int64_t timestamp) { + + if (numBytes > SIZE_MIDIRECEIVEBUFFER) { + return android::BAD_VALUE; + } + + // AMIDI_logBuffer(data, numBytes); + + unique_fd *ufd = NULL; + status_t result = MidiPortRegistry::getInstance().getInputPortFd(inputPort, &ufd); + if (result != OK) { + return result; + } + + uint8_t writeBuffer[SIZE_MIDIRECEIVEBUFFER + AMIDI_PACKET_OVERHEAD]; + ssize_t numTransferBytes = AMIDI_makeSendBuffer(writeBuffer, data, numBytes, timestamp); + ssize_t numWritten = write(*ufd, writeBuffer, numTransferBytes); + + if (numWritten < numTransferBytes) { + ALOGE("AMIDI_sendWithTimestamp Couldn't write MIDI data buffer. requested:%zu, written%zu", + numTransferBytes, numWritten); + } + + return numWritten - AMIDI_PACKET_OVERHEAD; +} + +status_t AMIDI_flush(AMIDI_InputPort inputPort) { + unique_fd *ufd = NULL; + status_t result = MidiPortRegistry::getInstance().getInputPortFd(inputPort, &ufd); + if (result != OK) { + return result; + } + + uint8_t opCode = AMIDI_OPCODE_FLUSH; + ssize_t numTransferBytes = 1; + ssize_t numWritten = write(*ufd, &opCode, numTransferBytes); + + if (numWritten < numTransferBytes) { + ALOGE("AMIDI_flush Couldn't write MIDI flush. requested:%zu, written%zu", + numTransferBytes, numWritten); + return android::INVALID_OPERATION; + } + + return OK; +} + diff --git a/media/native/midi/midi.h b/media/native/midi/midi.h new file mode 100644 index 000000000000..717bc666253e --- /dev/null +++ b/media/native/midi/midi.h @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2017 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. + */ + +#ifndef ANDROID_MEDIA_MIDI_H_ +#define ANDROID_MEDIA_MIDI_H_ + +#include <stdarg.h> +#include <stdint.h> +#include <sys/types.h> + +#include <utils/Errors.h> + +using android::status_t; + +#ifdef __cplusplus +extern "C" { +#endif + +//typedef struct _AMIDI_Device; +//typedef struct _AMIDI_InputPort; +//typedef struct _AMIDI_OutputPort; +//typedef _AMIDI_Device* AMIDI_Device; +//typedef _AMIDI_InputPort* AMIDI_InputPort; +//typedef _AMIDI_OutputPort* AMIDI_OutputPort; + +typedef int32_t AMIDI_Device; +typedef int32_t AMIDI_InputPort; +typedef int32_t AMIDI_OutputPort; + +//TODO - Do we want to wrap this stuff in namespace android { namespace media { namespace midi {? + +enum { + AMIDI_INVALID_HANDLE = -1 +}; + +enum { + AMIDI_OPCODE_DATA = 1, + AMIDI_OPCODE_FLUSH = 2, + AMIDI_PACKET_SIZE = 1024, /* !!! Currently MidiPortImpl.MAX_PACKET_SIZE !!! */ + AMIDI_PACKET_OVERHEAD = 9, + AMIDI_BUFFER_SIZE = AMIDI_PACKET_SIZE - AMIDI_PACKET_OVERHEAD + /* !!! TBD, currently MidiPortImpl.MAX_PACKET_DATA_SIZE !!! */ +}; + +typedef struct { + uint32_t opcode; + uint8_t buffer[AMIDI_BUFFER_SIZE]; + size_t len; + int64_t timestamp; +} AMIDI_Message; + +enum { + AMIDI_DEVICE_TYPE_USB = 1, + AMIDI_DEVICE_TYPE_VIRTUAL = 2, + AMIDI_DEVICE_TYPE_BLUETOOTH = 3 +}; + +typedef struct { + int32_t type; + int32_t uid; + int32_t isPrivate; + int32_t inputPortCount; + int32_t outputPortCount; +} AMIDI_DeviceInfo; + +/* + * Device API + */ +/* + * Retrieves the native API device token for the specified Java API device ID. + * + * uid The Java API id of the device. + * devicePtr Receives the associated native API token for the device. + * + * Returns OK or a (negative) error code. + */ +status_t AMIDI_getDeviceById(int32_t id, AMIDI_Device *devicePtr); + +/* + * Retrieves information for the native MIDI device. + * + * device The Native API token for the device. + * deviceInfoPtr Receives the associated device info. + * + * Returns OK or a (negative) error code. + */ +status_t AMIDI_getDeviceInfo(AMIDI_Device device, AMIDI_DeviceInfo *deviceInfoPtr); + +/* + * API for receiving data from the Output port of a device. + */ +/* + * Opens the output port. + * + * device Identifies the device. + * portNumber Specifies the zero-based port index on the device to open. + * outputPortPtr Receives the native API port identifier of the opened port. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_openOutputPort(AMIDI_Device device, int portNumber, AMIDI_OutputPort *outputPortPtr); + +/* + * Receives any pending MIDI messages (up to the specified maximum number of messages). + * + * outputPort Identifies the port to receive messages from. + * messages Points to an array (size maxMessages) to receive the MIDI messages. + * maxMessages The number of messages allocated in the messages array. + * + * Returns the number of messages received, or a (negative) error code. + */ +ssize_t AMIDI_receive(AMIDI_OutputPort outputPort, AMIDI_Message *messages, ssize_t maxMessages); + +/* + * Closes the output port. + * + * outputPort The native API port identifier of the port. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_closeOutputPort(AMIDI_OutputPort outputPort); + +/* + * API for sending data to the Input port of a device. + */ +/* + * Opens the input port. + * + * device Identifies the device. + * portNumber Specifies the zero-based port index on the device to open. + * inputPortPtr Receives the native API port identifier of the opened port. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_openInputPort(AMIDI_Device device, int portNumber, AMIDI_InputPort *inputPortPtr); + +/* + * Returns the maximum number of bytes that can be received in a single MIDI message. + */ +ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort inputPort); + +/* + * Sends data to the specified input port. + * + * inputPort The native API identifier of the port to send data to. + * buffer Points to the array of bytes containing the data to send. + * numBytes Specifies the number of bytes to write. + * + * Returns The number of bytes sent or a (negative) error code. + */ +ssize_t AMIDI_send(AMIDI_InputPort inputPort, uint8_t *buffer, ssize_t numBytes); + +/* + * Sends data to the specified input port with a timestamp. + * + * inputPort The native API identifier of the port to send data to. + * buffer Points to the array of bytes containing the data to send. + * numBytes Specifies the number of bytes to write. + * timestamp The time stamp to associate with the sent data. + * + * Returns The number of bytes sent or a (negative) error code. + */ +ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort inputPort, uint8_t *buffer, + ssize_t numBytes, int64_t timestamp); + +/* + * Sends a message with a 'MIDI flush command code' to the specified port. + * + * inputPort The native API identifier of the port to send the flush message to. + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_flush(AMIDI_InputPort inputPort); + +/* + * Closes the input port. + * + * inputPort The native API port identifier of the port. + * + * + * Returns OK, or a (negative) error code. + */ +status_t AMIDI_closeInputPort(AMIDI_InputPort inputPort); + +#ifdef __cplusplus +} +#endif + +#endif /* ANDROID_MEDIA_MIDI_H_ */ diff --git a/media/tests/NativeMidiDemo/Android.mk b/media/tests/NativeMidiDemo/Android.mk new file mode 100644 index 000000000000..6b08f6bba3f4 --- /dev/null +++ b/media/tests/NativeMidiDemo/Android.mk @@ -0,0 +1,30 @@ +# Copyright (C) 2016 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_PACKAGE_NAME := NativeMidiDemo + +#LOCAL_SDK_VERSION := current +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_SRC_FILES := $(call all-java-files-under, java) + +LOCAL_JNI_SHARED_LIBRARIES := libnativemidi_jni + +include $(BUILD_PACKAGE) + +# Include packages in subdirectories +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/media/tests/NativeMidiDemo/AndroidManifest.xml b/media/tests/NativeMidiDemo/AndroidManifest.xml new file mode 100644 index 000000000000..322873f11895 --- /dev/null +++ b/media/tests/NativeMidiDemo/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.nativemididemo" + android:versionCode="1" + android:versionName="1.0"> + <application + android:allowBackup="false" + android:fullBackupContent="false" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name"> + <uses-feature android:name="android.software.midi" android:required="true"/> + <activity android:name=".NativeMidi" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java new file mode 100644 index 000000000000..0969b10b1f90 --- /dev/null +++ b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2016 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. + * + */ + +package com.example.android.nativemididemo; + +import android.app.Activity; +import android.content.Context; +import android.media.midi.MidiDevice; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; +import android.media.midi.MidiOutputPort; +import android.media.midi.MidiReceiver; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.view.WindowManager; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import java.io.IOException; + +public class NativeMidi extends Activity +{ + private TextView mCallbackStatusTextView; + private TextView mJavaMidiStatusTextView; + private TextView mMessagesTextView; + private RadioGroup mMidiDevicesRadioGroup; + private Handler mTimerHandler = new Handler(); + private boolean mAudioWorks; + private final int mMinFramesPerBuffer = 32; // See {min|max}PlaySamples in nativemidi-jni.cpp + private final int mMaxFramesPerBuffer = 1000; + private int mFramesPerBuffer; + + private TouchableScrollView mMessagesContainer; + private MidiManager mMidiManager; + private MidiOutputPortSelector mActivePortSelector; + + private Runnable mTimerRunnable = new Runnable() { + private long mLastTime; + private long mLastPlaybackCounter; + private int mLastCallbackRate; + private long mLastUntouchedTime; + + @Override + public void run() { + final long checkIntervalMs = 1000; + long currentTime = System.currentTimeMillis(); + long currentPlaybackCounter = getPlaybackCounter(); + if (currentTime - mLastTime >= checkIntervalMs) { + int callbackRate = Math.round( + (float)(currentPlaybackCounter - mLastPlaybackCounter) / + ((float)(currentTime - mLastTime) / (float)1000)); + if (mLastCallbackRate != callbackRate) { + mCallbackStatusTextView.setText( + "CB: " + callbackRate + " Hz"); + mLastCallbackRate = callbackRate; + } + mLastTime = currentTime; + mLastPlaybackCounter = currentPlaybackCounter; + } + + String[] newMessages = getRecentMessages(); + if (newMessages != null) { + for (String message : newMessages) { + mMessagesTextView.append(message); + mMessagesTextView.append("\n"); + } + if (!mMessagesContainer.isTouched) { + if (mLastUntouchedTime == 0) mLastUntouchedTime = currentTime; + if (currentTime - mLastUntouchedTime > 3000) { + mMessagesContainer.fullScroll(View.FOCUS_DOWN); + } + } else { + mLastUntouchedTime = 0; + } + } + + mTimerHandler.postDelayed(this, checkIntervalMs / 4); + } + }; + + private void addMessage(final String message) { + mTimerHandler.postDelayed(new Runnable() { + @Override + public void run() { + mMessagesTextView.append(message); + } + }, 0); + } + + private class MidiOutputPortSelector implements View.OnClickListener { + private final MidiDeviceInfo mDeviceInfo; + private final int mPortNumber; + private MidiDevice mDevice; + private MidiOutputPort mOutputPort; + + MidiOutputPortSelector() { + mDeviceInfo = null; + mPortNumber = -1; + } + + MidiOutputPortSelector(MidiDeviceInfo info, int portNumber) { + mDeviceInfo = info; + mPortNumber = portNumber; + } + + MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; } + + @Override + public void onClick(View v) { + if (mActivePortSelector != null) { + mActivePortSelector.close(); + mActivePortSelector = null; + } + if (mDeviceInfo == null) { + mActivePortSelector = this; + return; + } + mMidiManager.openDevice(mDeviceInfo, new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + addMessage("! Failed to open MIDI device !\n"); + } else { + mDevice = device; + try { + mDevice.mirrorToNative(); + startReadingMidi(mDevice.getInfo().getId(), mPortNumber); + } catch (IOException e) { + addMessage("! Failed to mirror to native !\n" + e.getMessage() + "\n"); + } + + mActivePortSelector = MidiOutputPortSelector.this; + + mOutputPort = device.openOutputPort(mPortNumber); + mOutputPort.connect(mMidiReceiver); + } + } + }, null); + } + + void closePortOnly() { + stopReadingMidi(); + } + + void close() { + closePortOnly(); + try { + if (mOutputPort != null) { + mOutputPort.close(); + } + } catch (IOException e) { + mMessagesTextView.append("! Port close error: " + e + "\n"); + } finally { + mOutputPort = null; + } + try { + if (mDevice != null) { + mDevice.close(); + } + } catch (IOException e) { + mMessagesTextView.append("! Device close error: " + e + "\n"); + } finally { + mDevice = null; + } + } + } + + private MidiManager.DeviceCallback mMidiDeviceCallback = new MidiManager.DeviceCallback() { + @Override + public void onDeviceAdded(MidiDeviceInfo info) { + Bundle deviceProps = info.getProperties(); + String deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_NAME); + if (deviceName == null) { + deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); + } + + for (MidiDeviceInfo.PortInfo port : info.getPorts()) { + if (port.getType() != MidiDeviceInfo.PortInfo.TYPE_OUTPUT) continue; + String portName = port.getName(); + int portNumber = port.getPortNumber(); + if (portName.length() == 0) portName = "[" + portNumber + "]"; + portName += "@" + deviceName; + RadioButton outputDevice = new RadioButton(NativeMidi.this); + outputDevice.setText(portName); + outputDevice.setTag(info); + outputDevice.setOnClickListener(new MidiOutputPortSelector(info, portNumber)); + mMidiDevicesRadioGroup.addView(outputDevice); + } + + NativeMidi.this.updateKeepScreenOn(); + } + + @Override + public void onDeviceRemoved(MidiDeviceInfo info) { + if (mActivePortSelector != null && info.equals(mActivePortSelector.getDeviceInfo())) { + mActivePortSelector.close(); + mActivePortSelector = null; + } + int removeButtonStart = -1, removeButtonCount = 0; + final int buttonCount = mMidiDevicesRadioGroup.getChildCount(); + boolean checked = false; + for (int i = 0; i < buttonCount; ++i) { + RadioButton button = (RadioButton) mMidiDevicesRadioGroup.getChildAt(i); + if (!info.equals(button.getTag())) continue; + if (removeButtonStart == -1) removeButtonStart = i; + ++removeButtonCount; + if (button.isChecked()) checked = true; + } + if (removeButtonStart != -1) { + mMidiDevicesRadioGroup.removeViews(removeButtonStart, removeButtonCount); + if (checked) { + mMidiDevicesRadioGroup.check(R.id.device_none); + } + } + + NativeMidi.this.updateKeepScreenOn(); + } + }; + + private class JavaMidiReceiver extends MidiReceiver implements Runnable { + @Override + public void onSend(byte[] data, int offset, + int count, long timestamp) throws IOException { + mTimerHandler.removeCallbacks(this); + mTimerHandler.postDelayed(new Runnable() { + @Override + public void run() { + mJavaMidiStatusTextView.setText("Java: MSG"); + } + }, 0); + mTimerHandler.postDelayed(this, 100); + } + + @Override + public void run() { + mJavaMidiStatusTextView.setText("Java: ---"); + } + } + + private JavaMidiReceiver mMidiReceiver = new JavaMidiReceiver(); + + private void updateKeepScreenOn() { + if (mMidiDevicesRadioGroup.getChildCount() > 1) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + mCallbackStatusTextView = (TextView) findViewById(R.id.callback_status); + mJavaMidiStatusTextView = (TextView) findViewById(R.id.java_midi_status); + mMessagesTextView = (TextView) findViewById(R.id.messages); + mMessagesContainer = (TouchableScrollView) findViewById(R.id.messages_scroll); + mMidiDevicesRadioGroup = (RadioGroup) findViewById(R.id.devices); + RadioButton deviceNone = (RadioButton) findViewById(R.id.device_none); + deviceNone.setOnClickListener(new MidiOutputPortSelector()); + + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + if (sampleRate == null) sampleRate = "48000"; + String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + if (framesPerBuffer == null) framesPerBuffer = Integer.toString(mMaxFramesPerBuffer); + mFramesPerBuffer = Integer.parseInt(framesPerBuffer); + String audioInitResult = initAudio(Integer.parseInt(sampleRate), mFramesPerBuffer); + mMessagesTextView.append("Open SL ES init: " + audioInitResult + "\n"); + + if (audioInitResult.startsWith("Success")) { + mAudioWorks = true; + mTimerHandler.postDelayed(mTimerRunnable, 0); + mTimerHandler.postDelayed(mMidiReceiver, 0); + } + + mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + mMidiManager.registerDeviceCallback(mMidiDeviceCallback, new Handler()); + for (MidiDeviceInfo info : mMidiManager.getDevices()) { + mMidiDeviceCallback.onDeviceAdded(info); + } + } + + @Override + public void onPause() { + super.onPause(); + if (mAudioWorks) { + mTimerHandler.removeCallbacks(mTimerRunnable); + pauseAudio(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (mAudioWorks) { + mTimerHandler.postDelayed(mTimerRunnable, 0); + resumeAudio(); + } + } + + @Override + protected void onDestroy() { + if (mActivePortSelector != null) { + mActivePortSelector.close(); + mActivePortSelector = null; + } + shutdownAudio(); + super.onDestroy(); + } + + public void onClearMessages(View v) { + mMessagesTextView.setText(""); + } + + public void onClosePort(View v) { + if (mActivePortSelector != null) { + mActivePortSelector.closePortOnly(); + } + } + + private native String initAudio(int sampleRate, int playSamples); + private native void pauseAudio(); + private native void resumeAudio(); + private native void shutdownAudio(); + + private native long getPlaybackCounter(); + private native String[] getRecentMessages(); + + private native void startReadingMidi(int deviceId, int portNumber); + private native void stopReadingMidi(); + + static { + System.loadLibrary("nativemidi_jni"); + } +} diff --git a/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java new file mode 100644 index 000000000000..645aafa88507 --- /dev/null +++ b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java @@ -0,0 +1,32 @@ +package com.example.android.nativemididemo; + +import android.content.Context; +import android.view.MotionEvent; +import android.util.AttributeSet; +import android.widget.ScrollView; + +public class TouchableScrollView extends ScrollView { + public boolean isTouched; + + public TouchableScrollView(Context context) { + super(context); + } + + public TouchableScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + isTouched = true; + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + isTouched = false; + break; + } + return super.onTouchEvent(event); + } +} diff --git a/media/tests/NativeMidiDemo/jni/Android.mk b/media/tests/NativeMidiDemo/jni/Android.mk new file mode 100644 index 000000000000..69a64bd0218b --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/Android.mk @@ -0,0 +1,35 @@ +# Copyright (C) 2016 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_MODULE := libnativemidi_jni + +LOCAL_SRC_FILES := \ + nativemidi-jni.cpp \ + messagequeue.cpp + +LOCAL_CFLAGS += -Wall -Wextra -Werror -O0 + +LOCAL_C_INCLUDES += \ + frameworks/base/media/native + +LOCAL_CXX_STL := libc++_static + +LOCAL_SHARED_LIBRARIES := libOpenSLES libmidi + +include $(BUILD_SHARED_LIBRARY) diff --git a/media/tests/NativeMidiDemo/jni/messagequeue.cpp b/media/tests/NativeMidiDemo/jni/messagequeue.cpp new file mode 100644 index 000000000000..ffaef38bed8c --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/messagequeue.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2016 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 <atomic> +#include <stdio.h> +#include <string.h> + +#include "messagequeue.h" + +namespace nativemididemo { + +static const int messageBufferSize = 64 * 1024; +static char messageBuffer[messageBufferSize]; +static std::atomic_ullong messagesLastWritePosition; + +void writeMessage(const char* message) +{ + static unsigned long long lastWritePos = 0; + size_t messageLen = strlen(message); + if (messageLen == 0) return; + + messageLen += 1; // Also count in the null terminator. + char buffer[1024]; + if (messageLen >= messageBufferSize) { + snprintf(buffer, sizeof(buffer), "!!! Message too long: %zu bytes !!!", messageLen); + message = buffer; + messageLen = strlen(message); + } + + size_t wrappedWritePos = lastWritePos % messageBufferSize; + if (wrappedWritePos + messageLen >= messageBufferSize) { + size_t tailLen = messageBufferSize - wrappedWritePos; + memset(messageBuffer + wrappedWritePos, 0, tailLen); + lastWritePos += tailLen; + wrappedWritePos = 0; + } + + memcpy(messageBuffer + wrappedWritePos, message, messageLen); + lastWritePos += messageLen; + messagesLastWritePosition.store(lastWritePos); +} + +static char messageBufferCopy[messageBufferSize]; + +jobjectArray getRecentMessagesForJava(JNIEnv* env, jobject) +{ + static unsigned long long lastReadPos = 0; + const char* overrunMessage = ""; + size_t messagesCount = 0; + jobjectArray result = NULL; + + // First we copy the portion of the message buffer into messageBufferCopy. If after finishing + // the copy we notice that the writer has mutated the portion of the buffer that we were + // copying, we report an overrun. Afterwards we can safely read messages from the copy. + memset(messageBufferCopy, 0, sizeof(messageBufferCopy)); + unsigned long long lastWritePos = messagesLastWritePosition.load(); + if (lastWritePos - lastReadPos > messageBufferSize) { + overrunMessage = "!!! Message buffer overrun !!!"; + messagesCount = 1; + lastReadPos = lastWritePos; + goto create_array; + } + if (lastWritePos == lastReadPos) return result; + if (lastWritePos / messageBufferSize == lastReadPos / messageBufferSize) { + size_t wrappedReadPos = lastReadPos % messageBufferSize; + memcpy(messageBufferCopy + wrappedReadPos, + messageBuffer + wrappedReadPos, + lastWritePos % messageBufferSize - wrappedReadPos); + } else { + size_t wrappedReadPos = lastReadPos % messageBufferSize; + memcpy(messageBufferCopy, messageBuffer, lastWritePos % messageBufferSize); + memcpy(messageBufferCopy + wrappedReadPos, + messageBuffer + wrappedReadPos, + messageBufferSize - wrappedReadPos); + } + { + unsigned long long newLastWritePos = messagesLastWritePosition.load(); + if (newLastWritePos - lastReadPos > messageBufferSize) { + overrunMessage = "!!! Message buffer overrun !!!"; + messagesCount = 1; + lastReadPos = lastWritePos = newLastWritePos; + goto create_array; + } + } + // Otherwise we ignore newLastWritePos, since we only have a copy of the buffer + // up to lastWritePos. + + for (unsigned long long readPos = lastReadPos; readPos < lastWritePos; ) { + size_t messageLen = strlen(messageBufferCopy + (readPos % messageBufferSize)); + if (messageLen != 0) { + readPos += messageLen + 1; + messagesCount++; + } else { + // Skip to the beginning of the buffer. + readPos = (readPos / messageBufferSize + 1) * messageBufferSize; + } + } + if (messagesCount == 0) { + lastReadPos = lastWritePos; + return result; + } + +create_array: + result = env->NewObjectArray( + messagesCount, env->FindClass("java/lang/String"), env->NewStringUTF(overrunMessage)); + if (lastWritePos == lastReadPos) return result; + + jsize arrayIndex = 0; + while (lastReadPos < lastWritePos) { + size_t wrappedReadPos = lastReadPos % messageBufferSize; + if (messageBufferCopy[wrappedReadPos] != '\0') { + jstring message = env->NewStringUTF(messageBufferCopy + wrappedReadPos); + env->SetObjectArrayElement(result, arrayIndex++, message); + lastReadPos += env->GetStringLength(message) + 1; + env->DeleteLocalRef(message); + } else { + // Skip to the beginning of the buffer. + lastReadPos = (lastReadPos / messageBufferSize + 1) * messageBufferSize; + } + } + return result; +} + +} // namespace nativemididemo diff --git a/media/tests/NativeMidiDemo/jni/messagequeue.h b/media/tests/NativeMidiDemo/jni/messagequeue.h new file mode 100644 index 000000000000..20aa9e805d12 --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/messagequeue.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 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. + * + */ + +#ifndef NATIVEMIDIDEMO_MESSAGEQUEUE_H +#define NATIVEMIDIDEMO_MESSAGEQUEUE_H + +#include <jni.h> + +namespace nativemididemo { + +void writeMessage(const char* message); +jobjectArray getRecentMessagesForJava(JNIEnv* env, jobject thiz); + +} // namespace nativemididemo + +#endif // NATIVEMIDIDEMO_MESSAGEQUEUE_H diff --git a/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp b/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp new file mode 100644 index 000000000000..8aa874ae0748 --- /dev/null +++ b/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp @@ -0,0 +1,285 @@ +#include <atomic> +#include <inttypes.h> +#include <stdio.h> +#include <string.h> + +#include <jni.h> + +#include <midi/midi.h> +#include <SLES/OpenSLES.h> +#include <SLES/OpenSLES_Android.h> + +#include "messagequeue.h" + +extern "C" { +JNIEXPORT jstring JNICALL Java_com_example_android_nativemididemo_NativeMidi_initAudio( + JNIEnv* env, jobject thiz, jint sampleRate, jint playSamples); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_pauseAudio( + JNIEnv* env, jobject thiz); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_resumeAudio( + JNIEnv* env, jobject thiz); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_shutdownAudio( + JNIEnv* env, jobject thiz); +JNIEXPORT jlong JNICALL Java_com_example_android_nativemididemo_NativeMidi_getPlaybackCounter( + JNIEnv* env, jobject thiz); +JNIEXPORT jobjectArray JNICALL Java_com_example_android_nativemididemo_NativeMidi_getRecentMessages( + JNIEnv* env, jobject thiz); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_startReadingMidi( + JNIEnv* env, jobject thiz, jint deviceId, jint portNumber); +JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_stopReadingMidi( + JNIEnv* env, jobject thiz); +} + +static const char* errStrings[] = { + "SL_RESULT_SUCCESS", // 0 + "SL_RESULT_PRECONDITIONS_VIOLATED", // 1 + "SL_RESULT_PARAMETER_INVALID", // 2 + "SL_RESULT_MEMORY_FAILURE", // 3 + "SL_RESULT_RESOURCE_ERROR", // 4 + "SL_RESULT_RESOURCE_LOST", // 5 + "SL_RESULT_IO_ERROR", // 6 + "SL_RESULT_BUFFER_INSUFFICIENT", // 7 + "SL_RESULT_CONTENT_CORRUPTED", // 8 + "SL_RESULT_CONTENT_UNSUPPORTED", // 9 + "SL_RESULT_CONTENT_NOT_FOUND", // 10 + "SL_RESULT_PERMISSION_DENIED", // 11 + "SL_RESULT_FEATURE_UNSUPPORTED", // 12 + "SL_RESULT_INTERNAL_ERROR", // 13 + "SL_RESULT_UNKNOWN_ERROR", // 14 + "SL_RESULT_OPERATION_ABORTED", // 15 + "SL_RESULT_CONTROL_LOST" }; // 16 +static const char* getSLErrStr(int code) { + return errStrings[code]; +} + +static SLObjectItf engineObject; +static SLEngineItf engineEngine; +static SLObjectItf outputMixObject; +static SLObjectItf playerObject; +static SLPlayItf playerPlay; +static SLAndroidSimpleBufferQueueItf playerBufferQueue; + +static const int minPlaySamples = 32; +static const int maxPlaySamples = 1000; +static std::atomic_int playSamples(maxPlaySamples); +static short playBuffer[maxPlaySamples]; + +static std::atomic_ullong sharedCounter; + +static AMIDI_Device midiDevice = AMIDI_INVALID_HANDLE; +static std::atomic<AMIDI_OutputPort> midiOutputPort(AMIDI_INVALID_HANDLE); + +static int setPlaySamples(int newPlaySamples) +{ + if (newPlaySamples < minPlaySamples) newPlaySamples = minPlaySamples; + if (newPlaySamples > maxPlaySamples) newPlaySamples = maxPlaySamples; + playSamples.store(newPlaySamples); + return newPlaySamples; +} + +// Amount of messages we are ready to handle during one callback cycle. +static const size_t MAX_INCOMING_MIDI_MESSAGES = 20; +// Static allocation to save time in the callback. +static AMIDI_Message incomingMidiMessages[MAX_INCOMING_MIDI_MESSAGES]; + +static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void */*context*/) +{ + sharedCounter++; + + AMIDI_OutputPort outputPort = midiOutputPort.load(); + if (outputPort != AMIDI_INVALID_HANDLE) { + char midiDumpBuffer[1024]; + ssize_t midiReceived = AMIDI_receive( + outputPort, incomingMidiMessages, MAX_INCOMING_MIDI_MESSAGES); + if (midiReceived >= 0) { + for (ssize_t i = 0; i < midiReceived; ++i) { + AMIDI_Message* msg = &incomingMidiMessages[i]; + if (msg->opcode == AMIDI_OPCODE_DATA) { + memset(midiDumpBuffer, 0, sizeof(midiDumpBuffer)); + int pos = snprintf(midiDumpBuffer, sizeof(midiDumpBuffer), + "%" PRIx64 " ", msg->timestamp); + for (uint8_t *b = msg->buffer, *e = b + msg->len; b < e; ++b) { + pos += snprintf(midiDumpBuffer + pos, sizeof(midiDumpBuffer) - pos, + "%02x ", *b); + } + nativemididemo::writeMessage(midiDumpBuffer); + } else if (msg->opcode == AMIDI_OPCODE_FLUSH) { + nativemididemo::writeMessage("MIDI flush"); + } + } + } else { + snprintf(midiDumpBuffer, sizeof(midiDumpBuffer), + "! MIDI Receive error: %s !", strerror(-midiReceived)); + nativemididemo::writeMessage(midiDumpBuffer); + } + } + + size_t usedBufferSize = playSamples.load() * sizeof(playBuffer[0]); + if (usedBufferSize > sizeof(playBuffer)) { + usedBufferSize = sizeof(playBuffer); + } + (*bq)->Enqueue(bq, playBuffer, usedBufferSize); +} + +jstring Java_com_example_android_nativemididemo_NativeMidi_initAudio( + JNIEnv* env, jobject, jint sampleRate, jint playSamples) { + const char* stage; + SLresult result; + char printBuffer[1024]; + + playSamples = setPlaySamples(playSamples); + + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { stage = "slCreateEngine"; goto handle_error; } + + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { stage = "realize Engine object"; goto handle_error; } + + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + if (SL_RESULT_SUCCESS != result) { stage = "get Engine interface"; goto handle_error; } + + result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { stage = "CreateOutputMix"; goto handle_error; } + + result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { stage = "realize OutputMix object"; goto handle_error; } + + { + SLDataFormat_PCM format_pcm = { SL_DATAFORMAT_PCM, 1, (SLuint32)sampleRate * 1000, + SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, + SL_SPEAKER_FRONT_LEFT, SL_BYTEORDER_LITTLEENDIAN }; + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = + { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 }; + SLDataSource audioSrc = { &loc_bufq, &format_pcm }; + SLDataLocator_OutputMix loc_outmix = { SL_DATALOCATOR_OUTPUTMIX, outputMixObject }; + SLDataSink audioSnk = { &loc_outmix, NULL }; + const SLInterfaceID ids[1] = { SL_IID_BUFFERQUEUE }; + const SLboolean req[1] = { SL_BOOLEAN_TRUE }; + result = (*engineEngine)->CreateAudioPlayer( + engineEngine, &playerObject, &audioSrc, &audioSnk, 1, ids, req); + if (SL_RESULT_SUCCESS != result) { stage = "CreateAudioPlayer"; goto handle_error; } + + result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { stage = "realize Player object"; goto handle_error; } + } + + result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay); + if (SL_RESULT_SUCCESS != result) { stage = "get Play interface"; goto handle_error; } + + result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue); + if (SL_RESULT_SUCCESS != result) { stage = "get BufferQueue interface"; goto handle_error; } + + result = (*playerBufferQueue)->RegisterCallback(playerBufferQueue, bqPlayerCallback, NULL); + if (SL_RESULT_SUCCESS != result) { stage = "register BufferQueue callback"; goto handle_error; } + + result = (*playerBufferQueue)->Enqueue(playerBufferQueue, playBuffer, sizeof(playBuffer)); + if (SL_RESULT_SUCCESS != result) { + stage = "enqueue into PlayerBufferQueue"; goto handle_error; } + + result = (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING); + if (SL_RESULT_SUCCESS != result) { + stage = "SetPlayState(SL_PLAYSTATE_PLAYING)"; goto handle_error; } + + snprintf(printBuffer, sizeof(printBuffer), + "Success, sample rate %d, buffer samples %d", sampleRate, playSamples); + return env->NewStringUTF(printBuffer); + +handle_error: + snprintf(printBuffer, sizeof(printBuffer), "Error at %s: %s", stage, getSLErrStr(result)); + return env->NewStringUTF(printBuffer); +} + +void Java_com_example_android_nativemididemo_NativeMidi_pauseAudio( + JNIEnv*, jobject) { + if (playerPlay != NULL) { + (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PAUSED); + } +} + +void Java_com_example_android_nativemididemo_NativeMidi_resumeAudio( + JNIEnv*, jobject) { + if (playerBufferQueue != NULL && playerPlay != NULL) { + (*playerBufferQueue)->Enqueue(playerBufferQueue, playBuffer, sizeof(playBuffer)); + (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING); + } +} + +void Java_com_example_android_nativemididemo_NativeMidi_shutdownAudio( + JNIEnv*, jobject) { + if (playerObject != NULL) { + (*playerObject)->Destroy(playerObject); + playerObject = NULL; + playerPlay = NULL; + playerBufferQueue = NULL; + } + + if (outputMixObject != NULL) { + (*outputMixObject)->Destroy(outputMixObject); + outputMixObject = NULL; + } + + if (engineObject != NULL) { + (*engineObject)->Destroy(engineObject); + engineObject = NULL; + engineEngine = NULL; + } +} + +jlong Java_com_example_android_nativemididemo_NativeMidi_getPlaybackCounter(JNIEnv*, jobject) { + return sharedCounter.load(); +} + +jobjectArray Java_com_example_android_nativemididemo_NativeMidi_getRecentMessages( + JNIEnv* env, jobject thiz) { + return nativemididemo::getRecentMessagesForJava(env, thiz); +} + +void Java_com_example_android_nativemididemo_NativeMidi_startReadingMidi( + JNIEnv*, jobject, jint deviceId, jint portNumber) { + char buffer[1024]; + + int result = AMIDI_getDeviceById(deviceId, &midiDevice); + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Obtained device token for uid %d: token %d", deviceId, midiDevice); + } else { + snprintf(buffer, sizeof(buffer), "Could not obtain device token for uid %d: %d", deviceId, result); + } + nativemididemo::writeMessage(buffer); + if (result) return; + + AMIDI_DeviceInfo deviceInfo; + result = AMIDI_getDeviceInfo(midiDevice, &deviceInfo); + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Device info: uid %d, type %d, priv %d, ports %d I / %d O", + deviceInfo.uid, deviceInfo.type, deviceInfo.isPrivate, + (int)deviceInfo.inputPortCount, (int)deviceInfo.outputPortCount); + } else { + snprintf(buffer, sizeof(buffer), "Could not obtain device info %d", result); + } + nativemididemo::writeMessage(buffer); + if (result) return; + + AMIDI_OutputPort outputPort; + result = AMIDI_openOutputPort(midiDevice, portNumber, &outputPort); + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Opened port %d: token %d", portNumber, outputPort); + midiOutputPort.store(outputPort); + } else { + snprintf(buffer, sizeof(buffer), "Could not open port %d: %d", deviceId, result); + } + nativemididemo::writeMessage(buffer); +} + +void Java_com_example_android_nativemididemo_NativeMidi_stopReadingMidi( + JNIEnv*, jobject) { + AMIDI_OutputPort outputPort = midiOutputPort.exchange(AMIDI_INVALID_HANDLE); + if (outputPort == AMIDI_INVALID_HANDLE) return; + int result = AMIDI_closeOutputPort(outputPort); + char buffer[1024]; + if (result == 0) { + snprintf(buffer, sizeof(buffer), "Closed port by token %d", outputPort); + } else { + snprintf(buffer, sizeof(buffer), "Could not close port by token %d: %d", outputPort, result); + } + nativemididemo::writeMessage(buffer); +} diff --git a/media/tests/NativeMidiDemo/res/layout/main.xml b/media/tests/NativeMidiDemo/res/layout/main.xml new file mode 100644 index 000000000000..465d471a2de7 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/layout/main.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + > + <TextView + android:id="@+id/callback_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + android:text=" | " + /> + <TextView + android:id="@+id/java_midi_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + /> + <TextView + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textSize="13sp" + android:typeface="monospace" + android:text=" " + /> + </LinearLayout> + <HorizontalScrollView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingTop="6sp" + android:paddingBottom="6sp" + > + <RadioGroup + android:id="@+id/devices" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:checkedButton="@+id/device_none" + > + <RadioButton + android:id="@+id/device_none" + android:text="None" + /> + </RadioGroup> + </HorizontalScrollView> + <com.example.android.nativemididemo.TouchableScrollView android:id="@+id/messages_scroll" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1" + > + <TextView android:id="@+id/messages" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="13sp" + android:typeface="monospace" + /> + </com.example.android.nativemididemo.TouchableScrollView> + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + > + <Button android:id="@+id/clear_messages" + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/clear_messages" + android:onClick="onClearMessages" + /> + <Button android:id="@+id/close_port" + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/close_port" + android:onClick="onClosePort" + /> + </LinearLayout> +</LinearLayout> diff --git a/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..cde69bcccec6 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..c133a0cbd379 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..bfa42f0e7b91 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..324e72cdd748 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png diff --git a/media/tests/NativeMidiDemo/res/values/strings.xml b/media/tests/NativeMidiDemo/res/values/strings.xml new file mode 100644 index 000000000000..5b69b52a5836 --- /dev/null +++ b/media/tests/NativeMidiDemo/res/values/strings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">NativeMidiDemo</string> + <string name="clear_messages">Clear Messages</string> + <string name="close_port">Close Port</string> +</resources> |