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

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

MIDI-приложения Android обычно используют midi API для взаимодействия с MIDI-сервисом Android. 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 data в 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() . MainActivity Java вызывает 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);
}

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