AMidi API доступен в 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 can
take it from there.
Иногда вам может потребоваться передать информацию с уровня пользовательского интерфейса в нативный код. Например, когда события MIDI отправляются в ответ на нажатие кнопок на экране. Для этого создайте пользовательские вызовы JNI к нативной логике. Если вам нужно отправить данные обратно, чтобы обновить интерфейс, вы можете вызвать обратный вызов из нативного уровня, как обычно.
В этом документе показано, как настроить приложение с нативным кодом AMidi, и приведены примеры отправки и получения команд MIDI. Полный рабочий пример можно найти в примере приложения NativeMidi.
Использование AMidi
Все приложения, использующие AMidi, имеют одинаковые шаги настройки и закрытия, независимо от того, отправляют ли они или получают MIDI, или и то, и другое.
Запуск AMidi
В Java-коде приложение должно обнаружить подключенное MIDI-устройство, создать соответствующий объект MidiDevice и передать его в нативный код.
- Обнаружение MIDI-устройств с помощью класса Java
MidiManager. - Получите объект Java
MidiDevice, соответствующий MIDI-устройству. - Передайте объект
MidiDeviceJava в нативный код с помощью JNI.
Обнаружение оборудования и портов
Объекты входного и выходного портов не принадлежат приложению. Они представляют порты на MIDI-устройстве. Чтобы отправить MIDI-данные на устройство, приложение открывает MIDIInputPort и записывает в него данные. И наоборот, чтобы получить данные, приложение открывает MIDIOutputPort. Чтобы приложение работало правильно, оно должно быть уверено, что открывает порты нужного типа. Обнаружение устройств и портов выполняется на стороне Java.
Вот метод, который обнаруживает каждое MIDI-устройство и просматривает его порты. Возвращает список устройств с выходными портами для получения данных или список устройств с входными портами для отправки данных. У MIDI-устройства могут быть как входные, так и выходные порты.
Kotlin
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. Затем на нативном уровне необходимо выполнить следующие действия:
- Для каждого объекта
MidiDeviceJava получите объектAMidiDeviceс помощьюAMidiDevice_fromJava(). - Получите
AMidiInputPortи/илиAMidiOutputPortотAMidiDeviceсAMidiInputPort_open()и/илиAMidiOutputPort_open(). - Используйте полученные порты для отправки и/или получения MIDI-данных.
Остановить AMidi
Приложение Java должно сигнализировать нативному уровню о необходимости освободить ресурсы, когда оно больше не использует MIDI-устройство. Возможно, устройство MIDI было отключено или приложение закрывается.
Чтобы освободить ресурсы MIDI, ваш код должен выполнить следующие задачи:
- Остановить чтение и/или запись в MIDI-порты. Если вы использовали поток чтения для опроса ввода (см. раздел Реализация цикла опроса ниже), остановите поток.
- Закройте все открытые объекты
AMidiInputPortи/илиAMidiOutputPortс помощью функцийAMidiInputPort_close()и/илиAMidiOutputPort_close(). - Выпустите
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. Эта функция объясняется в следующем фрагменте.
Kotlin
//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-контроллер или секвенсор.
Настройка MidiDevice и его входных портов
Приложение записывает исходящие MIDI-данные в порты ввода MIDI-устройства. Java-код приложения должен определить, какие MIDI-устройства и порты использовать.
Приведенный ниже код настройки – это вариант примера получения, приведенного выше. Он создает MidiManager из MIDI-сервиса Android. Затем он открывает первое найденное устройство MidiDevice и вызывает startWritingMidi(), чтобы открыть первый входной порт на устройстве. Это вызов JNI, определенный в AppMidiManager.cpp. Функция объясняется в следующем фрагменте.
Kotlin
//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, которая сохраняет информацию, необходимую для вызова обратного вызова.
Когда придет время обратного вызова, нативный код может создать
Ниже приведен метод обратного вызова onNativeMessageReceive() на стороне Java.
Kotlin
//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.