API AMidi доступен в 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 может взять это оттуда.
Иногда вам может потребоваться передать информацию из слоя пользовательского интерфейса в собственный код. Например, когда события 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 в 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()
. 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.
API AMidi доступен в 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 может взять это оттуда.
Иногда вам может потребоваться передать информацию из слоя пользовательского интерфейса в собственный код. Например, когда события 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 в 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()
. 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.