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
и передать его собственному коду.
- Откройте для себя 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-данных.
Остановить АМИди
Приложение 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);
}
Дополнительные ресурсы
- Ссылка на АМИди
- Полный пример приложения Native MIDI см. на github.