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
и передать его машинному коду.
- Откройте для себя 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 } } }
Ява
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-данных.
Остановить 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
. Описание этой функции приведено в следующем фрагменте кода.
Котлин
//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); 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 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), ×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
. Функция поясняется в следующем фрагменте.
Котлин
//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); 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) } }
Ява
//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);
}
Дополнительные ресурсы
- AMidi ссылка
- Полный пример приложения Native MIDI смотрите на github.