原生 MIDI API
借助 Android Native MIDI API (AMidi),应用开发者可以使用 C/C++ 代码发送和接收 MIDI 数据、与 C/C++ 音频/控制逻辑进行更紧密的集成以及最大限度地减少对 JNI 的需求。
借助 AMidi,应用可以使用 C/C++ 发送和接收 MIDI。不过,您必须使用 Java。具体而言,您必须了解 MidiManager 类和 MidiManager.DeviceCallback 类。
示例代码:NativeMidi
注意:AMidi 从 Android Java MIDI API 继承了大部分术语,它使用的端口命名规范与商业 MIDI 设备中常见的命名规范不同。这可能有点让人迷惑不解。Android Java MIDI API 从连接的 MIDI 外设的角度考虑端口的方向性,而不是从应用的角度。AMidi 使用 AMidiOutputPort 从设备的输出端口接收 MIDI 数据。它使用 AMidiInputPort 将数据发送到设备的输入端口。AMidi 在方法名称中使用动词“send”和“receive”,使应用程序员更清晰地了解含义。
设置 AMidi
所有使用 AMidi 的应用都具有相同的设置和拆解步骤,无论其是要读取 MIDI、写入 MIDI,还是二者兼之:
- 使用 Java MidiManager 类发现 MIDI 硬件。
- 获取与 MIDI 硬件对应的 Java MidiDevice 对象。
- 使用 JNI 将 Java MidiDevice 传递到原生代码。
- 使用 AMidiDevice_fromJava() 获取 AMidiDevice。
- 使用 AMidiInputPort_open() 和/或 AMidiOutputPort_open() 获取 AMidiInputPort 和/或 AMidiOutputPort。
- 使用获取的端口发送和/或接收 MIDI 数据。
MIDI 读取应用
接收 MIDI 本质上是一种异步活动。因此,接收 MIDI 数据应始终在后台线程中完成。在读取数据时,AMidi 不会阻塞。
MIDI 读取应用的典型示例是“虚拟合成器”,此应用可以接收控制音频合成的 MIDI 性能数据。由于系统可以在任何时间异步接收 MIDI 数据,因此有必要使用线程连续轮询 MIDI 端口以获取传入数据。对于使用 OpenSL ES 或 AAudio 的原生音频生成应用,这主要是直接在音频回调函数中完成的。
发现和选择 MIDI 硬件
没有用于枚举/发现 MIDI 外设的原生 API。您必须使用现有的 Java MIDI API 发现和选择连接的 MIDI 硬件。通常,MIDI 读取应用会使用 Java MidiManager API 获取连接的 MIDI 设备列表,并打开其中一个或多个设备:
AppMidiManager.java
class AppMidiManager {
// System MIDI Manager
MidiManager mMidiManager =
(MidiManager)getSystemService(Context.MIDI_SERVICE);
// Open a device
MidiDeviceInfo[] devInfos = mMidiManager.getDevices();
MidiDeviceInfo devInfo = devInfos[0];
mMidiManager.openDevice(devInfo,
new OpenMidiReceiveDeviceListener(), null);
将选定的 MIDI 设备传达到原生层
在应用选择 MIDI 设备后,必须使用 JNI 将相应的基于 Java 的 MidiDevice 对象传递到原生层。原生层会利用它获取 AMidi_Device 以与原生 MIDI API 一起使用:
AppMidiManager.java
class AppMidiManager {
public class OpenMidiReceiveDeviceListener
implements MidiManager.OnDeviceOpenedListener {
@Override
public void onDeviceOpened(MidiDevice device) {
appMidiManager.startReadingMidi(mReceiveDevice, 0);
}
}
}
AppMidiManger.java
package com.nativemidiapp;
class AppMidiManager {
public native void startReadingMidi(MidiDevice device, int portNumber);
}
AppMidiManager.c
AMidi_Device 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 数据
读取 MIDI 的应用必须轮询输出(接收)端口,并在 AMidiOutputPort_receive() 返回大于零的数字时作出响应。对于生成音频的应用,这通常是在主音频生成回调(OpenSL ES 的 BufferQueue 回调、AAudio 中的 AudioStream 数据回调)中完成的。由于 AMidiOutputPort_receive() 不会阻塞,因此对音频生成回调的性能影响非常小。
请注意,对于非性能关键型或非音频生成型 MIDI 读取应用(例如“MIDI Scope”),可以使用低优先级的普通后台线程(具有适当的休眠)读取低带宽的 MIDI 数据。
上面的函数 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;
// Generate Audio…
// ...
// 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
// ...
}
}
关闭和释放 AMidi
Java 应用会通知原生层释放资源并关闭。
在应用退出时,您的代码应执行以下任务:
- 关闭并加入读取线程。
- 使用 AMidiInputPort_close() 和/或 AMidiOutputPort_close() 函数关闭所有打开的 AMidiInputPort 和/或 AMidiOutputPort 对象。
- 使用 AMidiDevice_release() 释放 AMidiDevice。
AppMidiManger.java
public native void stopReadingMidi();
AppMidiManager.c
void Java_com_nativemidiapp_TBMidiManager_stopReadingMidi(
JNIEnv*, jobject) {
// shut down the read thread
reading = false;
AMidiOutputPort* outputPort =
midiOutputPort.exchange(AMIDI_INVALID_HANDLE);
AMidiOutputPort_close(outputPort);
AMidiDevice_release(midiDevice);
}
下图说明了 MIDI 读取应用的流程:
MIDI 写入应用
MIDI 写入应用的典型示例是 MIDI 控制器或排序器。由于应用本身能够很好地理解和控制传出 MIDI 数据的时间,因此数据传输可以在 MIDI 应用的主线程中完成。但是,由于性能方面的原因(例如在排序器中),MIDI 的生成和传输可以在单独的线程中完成。
应用可以在需要时随时发送 MIDI 数据。请注意,在写入数据时,AMidi 会阻塞。
打开 MIDI 设备
void Java_com_nativemidiapp_TBMidiManager_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 数据
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);
}
关闭和释放 AMidi
在应用退出时,您的代码应执行以下任务:
- 使用 AMidiInputPort_close() 和/或 AMidiOutputPort_close() 函数关闭所有打开的 AMidiInputPort 和/或 AMidiOutputPort 对象。这可以与线程停止和加入操作并行完成。端口的状态是以线程安全的方式进行管理的。
- 使用 AMidiDevice_release() 释放 AMidiDevice。
void Java_com_nativemidiapp_TBMidiManager_stopWritingMidi(
JNIEnv*, jobject) {
AMidiDevice_release(sNativeSendDevice);
sNativeSendDevice = NULL;
}
下图说明了 MIDI 写入应用的流程: