Собственный MIDI API

API AMidi доступен в Android NDK r20b и более поздних версиях. Он дает разработчикам приложений возможность отправлять и получать данные MIDI с помощью кода C/C++.

Приложения Android MIDI обычно используют API midi для связи с сервисом Android MIDI. Приложения MIDI в первую очередь зависят от MidiManager для обнаружения, открытия и закрытия одного или нескольких объектов MidiDevice и передачи данных на каждое устройство и с него через порты ввода и вывода MIDI устройства:

При использовании AMidi вы передаете адрес MidiDevice в слой собственного кода с помощью вызова JNI. Оттуда AMidi создает ссылку на AMidiDevice , который имеет большую часть функциональности MidiDevice . Ваш собственный код использует функции AMidi , которые напрямую взаимодействуют с AMidiDevice . AMidiDevice подключается напрямую к службе MIDI:

Используя вызовы AMidi, вы можете тесно интегрировать логику аудио/управления C/C++ вашего приложения с передачей MIDI. Меньше необходимости в вызовах JNI или обратных вызовах на сторону Java вашего приложения. Например, цифровой синтезатор, реализованный в коде C, может получать ключевые события напрямую от AMidiDevice , а не ждать вызова JNI для отправки событий со стороны Java. Или алгоритмический процесс составления может отправлять MIDI-исполнение напрямую на AMidiDevice без обратного вызова на сторону Java для передачи ключевых событий.

Хотя AMidi улучшает прямое подключение к устройствам MIDI, приложения по-прежнему должны использовать MidiManager для обнаружения и открытия объектов MidiDevice . AMidi может взять это оттуда.

Иногда вам может потребоваться передать информацию из слоя пользовательского интерфейса в собственный код. Например, когда события MIDI отправляются в ответ на нажатия кнопок на экране. Для этого создайте пользовательские вызовы JNI для своей собственной логики. Если вам нужно отправить данные обратно для обновления пользовательского интерфейса, вы можете сделать обратный вызов из собственного слоя, как обычно.

В этом документе показано, как настроить приложение с собственным кодом AMidi, давая примеры отправки и получения команд MIDI. Для полного рабочего примера ознакомьтесь с примером приложения NativeMidi .

Использовать AMidi

Все приложения, использующие AMidi, имеют одинаковые шаги настройки и закрытия, независимо от того, отправляют ли они MIDI, получают ли они его или и то, и другое.

Запустить AMidi

На стороне Java приложение должно обнаружить подключенное устройство MIDI, создать соответствующий MidiDevice и передать его в машинный код.

  1. Откройте для себя MIDI-оборудование с помощью класса Java MidiManager .
  2. Получите объект Java MidiDevice , соответствующий оборудованию MIDI.
  3. Передайте Java MidiDevice в машинный код с помощью JNI.

Откройте для себя оборудование и порты

Объекты входного и выходного порта не принадлежат приложению. Они представляют порты на устройстве MIDI . Чтобы отправить данные MIDI на устройство, приложение открывает MIDIInputPort , а затем записывает в него данные. И наоборот, чтобы получить данные, приложение открывает MIDIOutputPort . Для правильной работы приложение должно быть уверено, что открываемые им порты имеют правильный тип. Обнаружение устройств и портов выполняется на стороне Java.

Вот метод, который обнаруживает каждое MIDI-устройство и просматривает его порты. Он возвращает либо список устройств с выходными портами для приема данных, либо список устройств с входными портами для отправки данных. MIDI-устройство может иметь как входные порты, так и выходные порты.

Котлин

private fun getMidiDevices(isOutput: Boolean) : List {
    if (isOutput) {
        return mMidiManager.devices.filter { it.outputPortCount > 0 }
    } else {
        return mMidiManager.devices.filter { it.inputPortCount > 0 }
    }
}

Ява

private List getMidiDevices(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;
}

Для использования функций AMidi в вашем коде C/C++ необходимо включить AMidi/AMidi.h и связать с библиотекой amidi . Оба они могут быть найдены в Android NDK .

Сторона Java должна передать один или несколько объектов MidiDevice и номера портов нативному слою через вызов JNI. Затем нативный слой должен выполнить следующие шаги:

  1. Для каждого Java MidiDevice получите AMidiDevice с помощью AMidiDevice_fromJava() .
  2. Получите AMidiInputPort и/или AMidiOutputPort из AMidiDevice с помощью AMidiInputPort_open() и/или AMidiOutputPort_open() .
  3. Используйте полученные порты для отправки и/или получения MIDI-данных.

Остановить AMidi

Приложение Java должно подать сигнал нативному слою об освобождении ресурсов, когда оно больше не использует устройство MIDI. Это может быть связано с тем, что устройство MIDI было отключено или приложение завершает работу.

Чтобы освободить ресурсы MIDI, ваш код должен выполнить следующие задачи:

  1. Остановите чтение и/или запись в порты MIDI. Если вы использовали поток чтения для опроса ввода (см. Реализация цикла опроса ниже), остановите поток.
  2. Закройте все открытые объекты AMidiInputPort и/или AMidiOutputPort с помощью функций AMidiInputPort_close() и/или AMidiOutputPort_close() .
  3. Освободите AMidiDevice с помощью AMidiDevice_release() .

Прием MIDI-данных

Типичным примером MIDI-приложения, принимающего MIDI-данные, является «виртуальный синтезатор», который получает данные о производительности MIDI для управления синтезом звука.

Входящие MIDI-данные принимаются асинхронно. Поэтому лучше всего считывать MIDI в отдельном потоке, который непрерывно опрашивает один или несколько выходных портов MIDI. Это может быть фоновый поток или аудиопоток. AMidi не блокируется при считывании с порта и поэтому его безопасно использовать внутри обратного аудиовызова.

Настройте MidiDevice и его выходные порты

Приложение считывает входящие MIDI-данные с выходных портов устройства. Java-сторона вашего приложения должна определить, какое устройство и порты использовать.

Этот фрагмент создает MidiManager из MIDI-сервиса Android и открывает MidiDevice для первого найденного устройства. Когда MidiDevice открыт, экземпляру MidiManager.OnDeviceOpenedListener() поступает обратный вызов. Вызывается метод onDeviceOpened этого слушателя, который затем вызывает startReadingMidi() для открытия выходного порта 0 на устройстве. Это функция JNI, определенная в AppMidiManager.cpp . Эта функция поясняется в следующем фрагменте.

Котлин

//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)
    }
  }
}

Ява

//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);
    List midiDevices = 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);
    }
  }
}

Собственный код преобразует MIDI-устройство на стороне Java и его порты в ссылки, используемые функциями AMidi.

Вот функция JNI, которая создает AMidiDevice путем вызова AMidiDevice_fromJava() , а затем вызывает AMidiOutputPort_open() для открытия выходного порта на устройстве:

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-осциллограф, можно выполнять опрос в фоновом потоке с низким приоритетом (с соответствующими периодами сна).

Для приложений, которые генерируют аудио и предъявляют более строгие требования к производительности в реальном времени, можно опросить основной обратный вызов генерации аудио (обратный вызов BufferQueue для OpenSL ES, обратный вызов данных AudioStream в AAudio). Поскольку AMidiOutputPort_receive() неблокируемый, влияние на производительность очень незначительно.

Функция readThreadRoutine() вызываемая из функции startReadingMidi() выше, может выглядеть следующим образом:

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;

    // 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
        // ...
    }

    // Generate Audio
    // ...
}

Следующая диаграмма иллюстрирует работу приложения для чтения MIDI:

Отправить MIDI-данные

Типичным примером приложения для записи MIDI-файлов является MIDI-контроллер или секвенсор.

Настройте MidiDevice и его входные порты

Приложение записывает исходящие MIDI-данные на входные порты MIDI-устройства. Java-сторона вашего приложения должна определить, какое MIDI-устройство и порты использовать.

Этот код настройки ниже является вариацией примера получения выше. Он создает MidiManager из службы MIDI Android. Затем он открывает первый найденный MidiDevice и вызывает startWritingMidi() для открытия первого входного порта на устройстве. Это вызов JNI, определенный в AppMidiManager.cpp . Функция поясняется в следующем фрагменте.

Котлин

//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)
    }
  }
}

Ява

//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);
    List midiDevices = 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);
    }
  }
}

Вот функция JNI, которая создает AMidiDevice путем вызова AMidiDevice_fromJava() , а затем вызывает AMidiInputPort_open() для открытия входного порта на устройстве:

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 (например, для обновления пользовательского интерфейса). Чтобы сделать это, вам необходимо написать код на стороне Java и нативном слое:

  • Создайте метод обратного вызова на стороне Java.
  • Напишите функцию JNI, которая хранит информацию, необходимую для вызова обратного вызова.

Когда придет время обратного вызова, ваш собственный код может сконструировать

Вот метод обратного вызова на стороне Java, onNativeMessageReceive() :

Котлин

//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) }
}

Ява

//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);
            }
        });
}

Вот код C для функции JNI, которая устанавливает обратный вызов MainActivity.onNativeMessageReceive() . 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);
}

Дополнительные ресурсы

,

API AMidi доступен в Android NDK r20b и более поздних версиях. Он дает разработчикам приложений возможность отправлять и получать данные MIDI с помощью кода C/C++.

Приложения Android MIDI обычно используют API midi для связи с сервисом Android MIDI. Приложения MIDI в первую очередь зависят от MidiManager для обнаружения, открытия и закрытия одного или нескольких объектов MidiDevice и передачи данных на каждое устройство и с него через порты ввода и вывода MIDI устройства:

При использовании AMidi вы передаете адрес MidiDevice в слой собственного кода с помощью вызова JNI. Оттуда AMidi создает ссылку на AMidiDevice , который имеет большую часть функциональности MidiDevice . Ваш собственный код использует функции AMidi , которые напрямую взаимодействуют с AMidiDevice . AMidiDevice подключается напрямую к службе MIDI:

Используя вызовы AMidi, вы можете тесно интегрировать логику аудио/управления C/C++ вашего приложения с передачей MIDI. Меньше необходимости в вызовах JNI или обратных вызовах на сторону Java вашего приложения. Например, цифровой синтезатор, реализованный в коде C, может получать ключевые события напрямую от AMidiDevice , а не ждать вызова JNI для отправки событий со стороны Java. Или алгоритмический процесс составления может отправлять MIDI-исполнение напрямую на AMidiDevice без обратного вызова на сторону Java для передачи ключевых событий.

Хотя AMidi улучшает прямое подключение к устройствам MIDI, приложения по-прежнему должны использовать MidiManager для обнаружения и открытия объектов MidiDevice . AMidi может взять это оттуда.

Иногда вам может потребоваться передать информацию из слоя пользовательского интерфейса в собственный код. Например, когда события MIDI отправляются в ответ на нажатия кнопок на экране. Для этого создайте пользовательские вызовы JNI для своей собственной логики. Если вам нужно отправить данные обратно для обновления пользовательского интерфейса, вы можете сделать обратный вызов из собственного слоя, как обычно.

В этом документе показано, как настроить приложение с собственным кодом AMidi, давая примеры отправки и получения команд MIDI. Для полного рабочего примера ознакомьтесь с примером приложения NativeMidi .

Использовать AMidi

Все приложения, использующие AMidi, имеют одинаковые шаги настройки и закрытия, независимо от того, отправляют ли они MIDI, получают ли они его или и то, и другое.

Запустить AMidi

На стороне Java приложение должно обнаружить подключенное устройство MIDI, создать соответствующий MidiDevice и передать его в машинный код.

  1. Откройте для себя MIDI-оборудование с помощью класса Java MidiManager .
  2. Получите объект Java MidiDevice , соответствующий оборудованию MIDI.
  3. Передайте Java MidiDevice в машинный код с помощью JNI.

Откройте для себя оборудование и порты

Объекты входного и выходного порта не принадлежат приложению. Они представляют порты на устройстве MIDI . Чтобы отправить данные MIDI на устройство, приложение открывает MIDIInputPort , а затем записывает в него данные. И наоборот, чтобы получить данные, приложение открывает MIDIOutputPort . Для правильной работы приложение должно быть уверено, что открываемые им порты имеют правильный тип. Обнаружение устройств и портов выполняется на стороне Java.

Вот метод, который обнаруживает каждое MIDI-устройство и просматривает его порты. Он возвращает либо список устройств с выходными портами для приема данных, либо список устройств с входными портами для отправки данных. MIDI-устройство может иметь как входные порты, так и выходные порты.

Котлин

private fun getMidiDevices(isOutput: Boolean) : List {
    if (isOutput) {
        return mMidiManager.devices.filter { it.outputPortCount > 0 }
    } else {
        return mMidiManager.devices.filter { it.inputPortCount > 0 }
    }
}

Ява

private List getMidiDevices(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;
}

Для использования функций AMidi в вашем коде C/C++ необходимо включить AMidi/AMidi.h и связать с библиотекой amidi . Оба они могут быть найдены в Android NDK .

Сторона Java должна передать один или несколько объектов MidiDevice и номера портов нативному слою через вызов JNI. Затем нативный слой должен выполнить следующие шаги:

  1. Для каждого Java MidiDevice получите AMidiDevice с помощью AMidiDevice_fromJava() .
  2. Получите AMidiInputPort и/или AMidiOutputPort из AMidiDevice с помощью AMidiInputPort_open() и/или AMidiOutputPort_open() .
  3. Используйте полученные порты для отправки и/или получения MIDI-данных.

Остановить AMidi

Приложение Java должно подать сигнал нативному слою об освобождении ресурсов, когда оно больше не использует устройство MIDI. Это может быть связано с тем, что устройство MIDI было отключено или приложение завершает работу.

Чтобы освободить ресурсы MIDI, ваш код должен выполнить следующие задачи:

  1. Остановите чтение и/или запись в порты MIDI. Если вы использовали поток чтения для опроса ввода (см. Реализация цикла опроса ниже), остановите поток.
  2. Закройте все открытые объекты AMidiInputPort и/или AMidiOutputPort с помощью функций AMidiInputPort_close() и/или AMidiOutputPort_close() .
  3. Освободите AMidiDevice с помощью AMidiDevice_release() .

Прием MIDI-данных

Типичным примером MIDI-приложения, принимающего MIDI-данные, является «виртуальный синтезатор», который получает данные о производительности MIDI для управления синтезом звука.

Входящие MIDI-данные принимаются асинхронно. Поэтому лучше всего считывать MIDI в отдельном потоке, который непрерывно опрашивает один или несколько выходных портов MIDI. Это может быть фоновый поток или аудиопоток. AMidi не блокируется при считывании с порта и поэтому его безопасно использовать внутри обратного аудиовызова.

Настройте MidiDevice и его выходные порты

Приложение считывает входящие MIDI-данные с выходных портов устройства. Java-сторона вашего приложения должна определить, какое устройство и порты использовать.

Этот фрагмент создает MidiManager из MIDI-сервиса Android и открывает MidiDevice для первого найденного устройства. Когда MidiDevice открыт, экземпляру MidiManager.OnDeviceOpenedListener() поступает обратный вызов. Вызывается метод onDeviceOpened этого слушателя, который затем вызывает startReadingMidi() для открытия выходного порта 0 на устройстве. Это функция JNI, определенная в AppMidiManager.cpp . Эта функция поясняется в следующем фрагменте.

Котлин

//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)
    }
  }
}

Ява

//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);
    List midiDevices = 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);
    }
  }
}

Собственный код преобразует MIDI-устройство на стороне Java и его порты в ссылки, используемые функциями AMidi.

Вот функция JNI, которая создает AMidiDevice путем вызова AMidiDevice_fromJava() , а затем вызывает AMidiOutputPort_open() для открытия выходного порта на устройстве:

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-осциллограф, можно выполнять опрос в фоновом потоке с низким приоритетом (с соответствующими периодами сна).

Для приложений, которые генерируют аудио и предъявляют более строгие требования к производительности в реальном времени, можно опросить основной обратный вызов генерации аудио (обратный вызов BufferQueue для OpenSL ES, обратный вызов данных AudioStream в AAudio). Поскольку AMidiOutputPort_receive() неблокируемый, влияние на производительность очень незначительно.

Функция readThreadRoutine() вызываемая из функции startReadingMidi() выше, может выглядеть следующим образом:

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;

    // 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
        // ...
    }

    // Generate Audio
    // ...
}

Следующая диаграмма иллюстрирует работу приложения для чтения MIDI:

Отправить MIDI-данные

Типичным примером приложения для записи MIDI-файлов является MIDI-контроллер или секвенсор.

Настройте MidiDevice и его входные порты

Приложение записывает исходящие MIDI-данные на входные порты MIDI-устройства. Java-сторона вашего приложения должна определить, какое MIDI-устройство и порты использовать.

Этот код настройки ниже является вариацией примера получения выше. Он создает MidiManager из службы MIDI Android. Затем он открывает первый найденный MidiDevice и вызывает startWritingMidi() для открытия первого входного порта на устройстве. Это вызов JNI, определенный в AppMidiManager.cpp . Функция поясняется в следующем фрагменте.

Котлин

//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)
    }
  }
}

Ява

//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);
    List midiDevices = 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);
    }
  }
}

Вот функция JNI, которая создает AMidiDevice путем вызова AMidiDevice_fromJava() , а затем вызывает AMidiInputPort_open() для открытия входного порта на устройстве:

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 (например, для обновления пользовательского интерфейса). Чтобы сделать это, вам необходимо написать код на стороне Java и нативном слое:

  • Создайте метод обратного вызова на стороне Java.
  • Напишите функцию JNI, которая хранит информацию, необходимую для вызова обратного вызова.

Когда придет время обратного вызова, ваш собственный код может сконструировать

Вот метод обратного вызова на стороне Java, onNativeMessageReceive() :

Котлин

//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) }
}

Ява

//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);
            }
        });
}

Вот код C для функции JNI, которая устанавливает обратный вызов MainActivity.onNativeMessageReceive() . 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);
}

Дополнительные ресурсы