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

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

Приложения Android MIDI обычно используют midi API для связи со службой 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, или и то, и другое.

Запустить АМИди

На стороне 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-данных.

Остановить АМИди

Приложение 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);
}

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