AMidi API 在 Android NDK r20b 及更高版本中提供。它让应用开发者能够使用 C/C++ 代码发送和接收 MIDI 数据。
Android MIDI 应用通常使用 midi
API 与 Android MIDI 服务通信。MIDI 应用主要依赖于 MidiManager
来发现、打开和关闭一个或多个 MidiDevice
对象,并通过设备的 MIDI 输入端口和输出端口向每个设备传递数据或从设备接收数据。
使用 AMidi 时,您可以通过 JNI 调用将 MidiDevice
的地址传递到原生代码层。从那里,AMidi 创建对 AMidiDevice
的引用,该引用具有 MidiDevice
的大部分功能。您的原生代码使用与 AMidiDevice
直接通信的 AMidi 函数。AMidiDevice
直接连接到 MIDI 服务。
使用 AMidi 调用,您可以将应用中的 C/C++ 音频/控制逻辑与 MIDI 传输紧密集成。这减少了对 JNI 调用或对应用 Java 侧回调的需要。例如,用 C 代码实现的数字合成器可以直接从 AMidiDevice
接收按键事件,而无需等待 JNI 调用从 Java 侧发送事件。或者,算法作曲过程可以直接将 MIDI 演奏发送到 AMidiDevice
,而无需回调 Java 侧来传输按键事件。
虽然 AMidi 改进了与 MIDI 设备的直接连接,但应用仍必须使用 MidiManager
来发现并打开 MidiDevice
对象。AMidi 可以从那里接管。
有时,您可能需要将信息从 UI 层传递到原生代码。例如,当 MIDI 事件是为了响应屏幕上的按钮而发送时。为此,请创建自定义 JNI 调用到您的原生逻辑。如果需要将数据发送回以更新 UI,则可以像往常一样从原生层回调。
本文档演示了如何设置 AMidi 原生代码应用,并提供了发送和接收 MIDI 命令的示例。有关完整的可运行示例,请参阅 NativeMidi 示例应用。
使用 AMidi
所有使用 AMidi 的应用都有相同的设置和关闭步骤,无论它们是发送或接收 MIDI,还是两者都做。
启动 AMidi
在 Java 侧,应用必须发现连接的 MIDI 硬件,创建一个相应的 MidiDevice
,并将其传递到原生代码。
- 使用 Java
MidiManager
类发现 MIDI 硬件。 - 获取与 MIDI 硬件对应的 Java
MidiDevice
对象。 - 通过 JNI 将 Java
MidiDevice
传递到原生代码。
发现硬件和端口
输入和输出端口对象不属于应用。它们代表的是 MIDI 设备上的端口。要向设备发送 MIDI 数据,应用会打开一个 MIDIInputPort
,然后向其中写入数据。相反,要接收数据,应用会打开一个 MIDIOutputPort
。为了正常工作,应用必须确保打开的端口类型正确。设备和端口的发现是在 Java 侧完成的。
下面是一个方法,它发现每个 MIDI 设备并查看其端口。它返回一个列表,其中包含用于接收数据的输出端口的设备,或者包含用于发送数据的输入端口的设备。一个 MIDI 设备可以同时有输入端口和输出端口。
Kotlin
private fun getMidiDevices(isOutput: Boolean) : List{ if (isOutput) { return mMidiManager.devices.filter { it.outputPortCount > 0 } } else { return mMidiManager.devices.filter { it.inputPortCount > 0 } } }
Java
private ListgetMidiDevices(boolean isOutput){ ArrayList filteredMidiDevices = new ArrayList<>(); for (MidiDeviceInfo midiDevice : mMidiManager.getDevices()){ if (isOutput){ if (midiDevice.getOutputPortCount() > 0) filteredMidiDevices.add(midiDevice); } else { if (midiDevice.getInputPortCount() > 0) filteredMidiDevices.add(midiDevice); } } return filteredMidiDevices; }
要在您的 C/C++ 代码中使用 AMidi 函数,您必须包含 AMidi/AMidi.h
并链接到 amidi
库。这两者都可以在 Android NDK 中找到。
Java 侧应通过 JNI 调用将一个或多个 MidiDevice
对象和端口号传递到原生层。原生层随后应执行以下步骤:
- 对于每个 Java
MidiDevice
,使用AMidiDevice_fromJava()
获取一个AMidiDevice
。 - 使用
AMidiInputPort_open()
和/或AMidiOutputPort_open()
从AMidiDevice
获取一个AMidiInputPort
和/或AMidiOutputPort
。 - 使用获取的端口发送和/或接收 MIDI 数据。
停止 AMidi
当不再使用 MIDI 设备时,Java 应用应向原生层发出信号以释放资源。这可能是因为 MIDI 设备已断开连接,或者应用正在退出。
要释放 MIDI 资源,您的代码应执行以下任务:
- 停止对 MIDI 端口的读取和/或写入。如果您使用读取线程轮询输入(参见下面的实现轮询循环),则停止该线程。
- 使用
AMidiInputPort_close()
和/或AMidiOutputPort_close()
函数关闭任何打开的AMidiInputPort
和/或AMidiOutputPort
对象。 - 使用
AMidiDevice_release()
释放AMidiDevice
。
接收 MIDI 数据
一个接收 MIDI 的典型 MIDI 应用示例是“虚拟合成器”,它接收 MIDI 演奏数据来控制音频合成。
接收传入的 MIDI 数据是异步的。因此,最好在单独的线程中读取 MIDI,该线程持续轮询一个或多个 MIDI 输出端口。这可以是后台线程,也可以是音频线程。AMidi 在从端口读取时不会阻塞,因此可以在音频回调中使用它。
设置 MidiDevice 及其输出端口
应用从设备的输出端口读取传入的 MIDI 数据。应用的 Java 侧必须确定要使用哪个设备和端口。
此片段从 Android 的 MIDI 服务创建 MidiManager
,并为找到的第一个设备打开一个 MidiDevice
。当 MidiDevice
打开后,会收到对 MidiManager.OnDeviceOpenedListener()
实例的回调。将调用此监听器的 onDeviceOpened
方法,然后该方法会调用 startReadingMidi()
来打开设备的输出端口 0。这是一个在 AppMidiManager.cpp
中定义的 JNI 函数。此函数在下一个片段中解释。
Kotlin
//AppMidiManager.kt class AppMidiManager(context : Context) { private external fun startReadingMidi(midiDevice: MidiDevice, portNumber: Int) val mMidiManager : MidiManager = context.getSystemService(Context.MIDI_SERVICE) as MidiManager init { val midiDevices = getMidiDevices(true) // method defined in snippet above if (midiDevices.isNotEmpty()){ midiManager.openDevice(midiDevices[0], { startReadingMidi(it, 0) }, null) } } }
Java
//AppMidiManager.java public class AppMidiManager { private native void startReadingMidi(MidiDevice device, int portNumber); private MidiManager mMidiManager; AppMidiManager(Context context){ mMidiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE); ListmidiDevices = getMidiDevices(true); // method defined in snippet above if (midiDevices.size() > 0){ mMidiManager.openDevice(midiDevices.get(0), new MidiManager.OnDeviceOpenedListener() { @Override public void onDeviceOpened(MidiDevice device) { startReadingMidi(device, 0); } },null); } } }
原生代码将 Java 侧的 MIDI 设备及其端口转换为 AMidi 函数使用的引用。
这是通过调用 AMidiDevice_fromJava()
创建 AMidiDevice
,然后调用 AMidiOutputPort_open()
打开设备上输出端口的 JNI 函数:
AppMidiManager.cpp
AMidiDevice midiDevice;
static pthread_t readThread;
static const AMidiDevice* midiDevice = AMIDI_INVALID_HANDLE;
static std::atomic<AMidiOutputPort*> midiOutputPort(AMIDI_INVALID_HANDLE);
void Java_com_nativemidiapp_AppMidiManager_startReadingMidi(
JNIEnv* env, jobject, jobject deviceObj, jint portNumber) {
AMidiDevice_fromJava(j_env, deviceObj, &midiDevice);
AMidiOutputPort* outputPort;
int32_t result =
AMidiOutputPort_open(midiDevice, portNumber, &outputPort);
// check for errors...
// Start read thread
int pthread_result =
pthread_create(&readThread, NULL, readThreadRoutine, NULL);
// check for errors...
}
实现轮询循环
接收 MIDI 数据的应用必须轮询输出端口并在 AMidiOutputPort_receive()
返回大于零的数字时做出响应。
对于低带宽应用(如 MIDI 示波器),您可以在低优先级的后台线程中轮询(并适当休眠)。
对于生成音频且具有更严格实时性能要求的应用,您可以在主音频生成回调中轮询(OpenSL ES 的 BufferQueue
回调,AAudio 的 AudioStream 数据回调)。由于 AMidiOutputPort_receive()
是非阻塞的,因此对性能影响很小。
上面 startReadingMidi()
函数调用的 readThreadRoutine()
函数可能如下所示:
void* readThreadRoutine(void * /*context*/) {
uint8_t inDataBuffer[SIZE_DATABUFFER];
int32_t numMessages;
uint32_t opCode;
uint64_t timestamp;
reading = true;
while (reading) {
AMidiOutputPort* outputPort = midiOutputPort.load();
numMessages =
AMidiOutputPort_receive(outputPort, &opCode, inDataBuffer,
sizeof(inDataBuffer), ×tamp);
if (numMessages >= 0) {
if (opCode == AMIDI_OPCODE_DATA) {
// Dispatch the MIDI data….
}
} else {
// some error occurred, the negative numMessages is the error code
int32_t errorCode = numMessages;
}
}
}
使用原生音频 API(如 OpenSL ES 或 AAudio)的应用可以这样将 MIDI 接收代码添加到音频生成回调中:
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void */*context*/)
{
uint8_t inDataBuffer[SIZE_DATABUFFER];
int32_t numMessages;
uint32_t opCode;
uint64_t timestamp;
// Read MIDI Data
numMessages = AMidiOutputPort_receive(outputPort, &opCode, inDataBuffer,
sizeof(inDataBuffer), ×tamp);
if (numMessages >= 0 && opCode == AMIDI_OPCODE_DATA) {
// Parse and respond to MIDI data
// ...
}
// Generate Audio…
// ...
}
下图演示了 MIDI 读取应用的流程:
发送 MIDI 数据
MIDI 写入应用的典型示例是 MIDI 控制器或音序器。
设置 MidiDevice 及其输入端口
应用将传出的 MIDI 数据写入 MIDI 设备的输入端口。应用的 Java 侧必须确定要使用哪个 MIDI 设备和端口。
下面的设置代码是对上面接收示例的变体。它从 Android 的 MIDI 服务创建 MidiManager
。然后打开找到的第一个 MidiDevice
,并调用 startWritingMidi()
打开设备的第一个输入端口。这是一个在 AppMidiManager.cpp
中定义的 JNI 调用。该函数在下一个片段中解释。
Kotlin
//AppMidiManager.kt class AppMidiManager(context : Context) { private external fun startWritingMidi(midiDevice: MidiDevice, portNumber: Int) val mMidiManager : MidiManager = context.getSystemService(Context.MIDI_SERVICE) as MidiManager init { val midiDevices = getMidiDevices(false) // method defined in snippet above if (midiDevices.isNotEmpty()){ midiManager.openDevice(midiDevices[0], { startWritingMidi(it, 0) }, null) } } }
Java
//AppMidiManager.java public class AppMidiManager { private native void startWritingMidi(MidiDevice device, int portNumber); private MidiManager mMidiManager; AppMidiManager(Context context){ mMidiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE); ListmidiDevices = getMidiDevices(false); // method defined in snippet above if (midiDevices.size() > 0){ mMidiManager.openDevice(midiDevices.get(0), new MidiManager.OnDeviceOpenedListener() { @Override public void onDeviceOpened(MidiDevice device) { startWritingMidi(device, 0); } },null); } } }
这是通过调用 AMidiDevice_fromJava()
创建 AMidiDevice
,然后调用 AMidiInputPort_open()
打开设备上输入端口的 JNI 函数:
AppMidiManager.cpp
void Java_com_nativemidiapp_AppMidiManager_startWritingMidi(
JNIEnv* env, jobject, jobject midiDeviceObj, jint portNumber) {
media_status_t status;
status = AMidiDevice_fromJava(
env, midiDeviceObj, &sNativeSendDevice);
AMidiInputPort *inputPort;
status = AMidiInputPort_open(
sNativeSendDevice, portNumber, &inputPort);
// store it in a global
sMidiInputPort = inputPort;
}
发送 MIDI 数据
由于传出的 MIDI 数据的时间是由应用本身很好地理解和控制的,因此数据传输可以在 MIDI 应用的主线程中完成。但是,出于性能原因(如在音序器中),MIDI 的生成和传输可以在单独的线程中完成。
应用可以在需要时发送 MIDI 数据。请注意,AMidi 在写入数据时会阻塞。
这是一个 JNI 方法示例,它接收 MIDI 命令缓冲区并将其写出:
void Java_com_nativemidiapp_TBMidiManager_writeMidi(
JNIEnv* env, jobject, jbyteArray data, jint numBytes) {
jbyte* bufferPtr = env->GetByteArrayElements(data, NULL);
AMidiInputPort_send(sMidiInputPort, (uint8_t*)bufferPtr, numBytes);
env->ReleaseByteArrayElements(data, bufferPtr, JNI_ABORT);
}
下图演示了 MIDI 写入应用的流程:
回调
虽然不完全是 AMidi 的功能,但您的原生代码可能需要将数据传回 Java 侧(例如用于更新 UI)。为此,您必须在 Java 侧和原生层编写代码。
- 在 Java 侧创建一个回调方法。
- 编写一个 JNI 函数,用于存储调用回调所需的信息。
在需要回调时,您的原生代码可以构建一个
这是 Java 侧的回调方法 onNativeMessageReceive()
:
Kotlin
//MainActivity.kt private fun onNativeMessageReceive(message: ByteArray) { // Messages are received on some other thread, so switch to the UI thread // before attempting to access the UI runOnUiThread { showReceivedMessage(message) } }
Java
//MainActivity.java private void onNativeMessageReceive(final byte[] message) { // Messages are received on some other thread, so switch to the UI thread // before attempting to access the UI runOnUiThread(new Runnable() { public void run() { showReceivedMessage(message); } }); }
这是用于设置回调到 MainActivity.onNativeMessageReceive()
的 JNI 函数的 C 代码。Java MainActivity
在启动时调用 initNative()
:
MainActivity.cpp
/**
* Initializes JNI interface stuff, specifically the info needed to call back into the Java
* layer when MIDI data is received.
*/
JNICALL void Java_com_example_nativemidi_MainActivity_initNative(JNIEnv * env, jobject instance) {
env->GetJavaVM(&theJvm);
// Setup the receive data callback (into Java)
jclass clsMainActivity = env->FindClass("com/example/nativemidi/MainActivity");
dataCallbackObj = env->NewGlobalRef(instance);
midDataCallback = env->GetMethodID(clsMainActivity, "onNativeMessageReceive", "([B)V");
}
当需要将数据发送回 Java 时,原生代码会检索回调指针并构建回调:
AppMidiManager.cpp
// The Data Callback
extern JavaVM* theJvm; // Need this for allocating data buffer for...
extern jobject dataCallbackObj; // This is the (Java) object that implements...
extern jmethodID midDataCallback; // ...this callback routine
static void SendTheReceivedData(uint8_t* data, int numBytes) {
JNIEnv* env;
theJvm->AttachCurrentThread(&env, NULL);
if (env == NULL) {
LOGE("Error retrieving JNI Env");
}
// Allocate the Java array and fill with received data
jbyteArray ret = env->NewByteArray(numBytes);
env->SetByteArrayRegion (ret, 0, numBytes, (jbyte*)data);
// send it to the (Java) callback
env->CallVoidMethod(dataCallbackObj, midDataCallback, ret);
}
其他资源
- AMidi 参考
- 请参阅 GitHub 上的完整 Native MIDI 示例应用。