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
Все приложения, использующие AM-DII, имеют одинаковые этапы настройки и закрытия, независимо от того, отправляют они MIDI-сигнал, принимают его или и то, и другое.
Запуск AMidi
На стороне Java приложение должно обнаружить подключенное MIDI-устройство, создать соответствующий MidiDevice и передать его в нативный код.
- Откройте для себя MIDI-оборудование с помощью класса Java
MidiManager. - Получите объект Java
MidiDevice, соответствующий MIDI-оборудованию. - Передайте 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 } } }
Java
private ListgetMidiDevices(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. Затем нативный слой должен выполнить следующие шаги:
- Для каждого Java
MidiDeviceполучитеAMidiDevice, используя функциюAMidiDevice_fromJava(). - Получите
AMidiInputPortи/илиAMidiOutputPortотAMidiDeviceс помощьюAMidiInputPort_open()и/илиAMidiOutputPort_open(). - Используйте полученные порты для отправки и/или приема MIDI-данных.
Остановить АМИди
Java-приложение должно сигнализировать нативному слою о необходимости освободить ресурсы, когда оно больше не использует MIDI-устройство. Это может произойти из-за отключения MIDI-устройства или завершения работы приложения.
Для освобождения MIDI-ресурсов ваш код должен выполнить следующие задачи:
- Прекратите чтение и/или запись в MIDI-порты. Если вы использовали поток чтения для опроса входных данных (см. раздел «Реализация цикла опроса» ниже), остановите этот поток.
- Закройте все открытые объекты
AMidiInputPortи/илиAMidiOutputPortс помощью функцийAMidiInputPort_close()и/илиAMidiOutputPort_close(). - Освободите
AMidiDeviceс помощьюAMidiDevice_release().
Приём MIDI-данных
Типичным примером MIDI-приложения, принимающего MIDI-данные, является «виртуальный синтезатор», который получает MIDI-данные для управления синтезом звука.
Входящие MIDI-данные принимаются асинхронно. Поэтому лучше всего считывать MIDI в отдельном потоке, который постоянно опрашивает один или несколько выходных MIDI-портов. Это может быть фоновый поток или поток обработки звука. AMidi не блокируется при чтении с порта и поэтому безопасен для использования внутри функции обратного вызова обработки звука.
Настройте MIDI-устройство и его выходные порты.
Приложение считывает входящие 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) } } }
Java
//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); ListmidiDevices = 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), ×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;
// 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
// ...
}
// Generate Audio…
// ...
}
Следующая диаграмма иллюстрирует работу приложения для чтения MIDI-данных:

Отправка MIDI-данных
Типичным примером приложения для записи MIDI-данных является MIDI-контроллер или секвенсор.
Настройте MIDI-устройство и его входные порты.
Приложение записывает исходящие 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) } } }
Java
//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); ListmidiDevices = 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) } }
Java
//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);
}
Дополнительные ресурсы
- Справочник AMidI
- Полную версию демонстрационного приложения Native MIDI можно посмотреть на GitHub.