Android Q (十三)原生 MIDI API

本文深入探讨Android Native MIDI API(AMidi),介绍如何使用C/C++代码进行MIDI数据的发送与接收,与音频控制逻辑的紧密集成,以及如何减少JNI需求。详细讲解了MIDI读取和写入应用的实现过程,包括设备发现、数据处理和资源释放等关键步骤。

原生 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,还是二者兼之:

  1. 使用 Java MidiManager 类发现 MIDI 硬件。
  2. 获取与 MIDI 硬件对应的 Java MidiDevice 对象。
  3. 使用 JNI 将 Java MidiDevice 传递到原生代码。
  4. 使用 AMidiDevice_fromJava() 获取 AMidiDevice。
  5. 使用 AMidiInputPort_open() 和/或 AMidiOutputPort_open() 获取 AMidiInputPort 和/或 AMidiOutputPort。
  6. 使用获取的端口发送和/或接收 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), &timestamp);

            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), &timestamp);

        if (numMessages >= 0 && opCode == AMIDI_OPCODE_DATA) {

            // Parse and respond to MIDI data

            // ...

        }

 

    }

   

 

关闭和释放 AMidi

Java 应用会通知原生层释放资源并关闭。

在应用退出时,您的代码应执行以下任务:

  1. 关闭并加入读取线程。
  2. 使用 AMidiInputPort_close() 和/或 AMidiOutputPort_close() 函数关闭所有打开的 AMidiInputPort 和/或 AMidiOutputPort 对象。
  3. 使用 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

在应用退出时,您的代码应执行以下任务:

  1. 使用 AMidiInputPort_close() 和/或 AMidiOutputPort_close() 函数关闭所有打开的 AMidiInputPort 和/或 AMidiOutputPort 对象。这可以与线程停止和加入操作并行完成。端口的状态是以线程安全的方式进行管理的。
  2. 使用 AMidiDevice_release() 释放 AMidiDevice。

void Java_com_nativemidiapp_TBMidiManager_stopWritingMidi(

        JNIEnv*, jobject) {

       AMidiDevice_release(sNativeSendDevice);

       sNativeSendDevice = NULL;

    }

   

 

下图说明了 MIDI 写入应用的流程:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值