Interfejs API AMidi jest dostępny w Androidzie NDK r20b i nowszych. Umożliwia deweloperom aplikacji wysyłanie i odbieranie danych MIDI za pomocą kodu C/C++.
Aplikacje MIDI na Androida zwykle używają interfejsu API midi do komunikacji z usługą MIDI na Androidzie. Aplikacje MIDI korzystają głównie z interfejsu MidiManager do wykrywania, otwierania i zamykania co najmniej 1 obiektu MidiDevice oraz do przesyłania danych do i z każdego urządzenia przez jego porty wejściowe i wyjściowe MIDI:
Gdy używasz AMidi, przekazujesz adres MidiDevice do warstwy kodu natywnego za pomocą wywołania JNI. Następnie AMidi tworzy odniesienie do AMidiDevice, który ma większość funkcji MidiDevice. Kod natywny używa funkcji AMidi, które komunikują się bezpośrednio z AMidiDevice. AMidiDevice łączy się bezpośrednio z usługą MIDI:
Za pomocą wywołań AMidi możesz ściśle zintegrować logikę dźwięku/sterowania C/C++ aplikacji z transmisją MIDI. Zmniejsza się potrzeba wywołań JNI lub wywołań zwrotnych do części aplikacji napisanej w języku Java. Na przykład syntezator cyfrowy zaimplementowany w kodzie C może odbierać zdarzenia klawiszy bezpośrednio z AMidiDevice, zamiast czekać na wywołanie JNI, które wyśle zdarzenia z części napisanej w języku Java. Algorytmiczny proces komponowania może też wysyłać wykonanie MIDI bezpośrednio do AMidiDevice bez wywoływania strony Javy w celu przesłania zdarzeń klawiszy.
Chociaż AMidi ulepsza bezpośrednie połączenie z urządzeniami MIDI, aplikacje nadal muszą używać MidiManager do wykrywania i otwierania obiektów MidiDevice. AMidi zajmie się resztą.
Czasami może być konieczne przekazanie informacji z warstwy interfejsu do kodu natywnego. Na przykład gdy zdarzenia MIDI są wysyłane w odpowiedzi na przyciski na ekranie. W tym celu utwórz niestandardowe wywołania JNI do logiki natywnej. Jeśli chcesz odesłać dane, aby zaktualizować interfejs, możesz wywołać funkcję z warstwy natywnej jak zwykle.
Z tego dokumentu dowiesz się, jak skonfigurować aplikację z kodem natywnym AMidi. Znajdziesz tu też przykłady wysyłania i odbierania poleceń MIDI. Pełny, działający przykład znajdziesz w przykładowej aplikacji NativeMidi.
Korzystanie z AMidi
Wszystkie aplikacje korzystające z AMidi mają te same kroki konfiguracji i zamykania, niezależnie od tego, czy wysyłają, czy odbierają dane MIDI, czy też robią obie te rzeczy.
Uruchamianie AMidi
Po stronie Javy aplikacja musi wykryć podłączony sprzęt MIDI, utworzyć odpowiedni obiekt MidiDevice i przekazać go do kodu natywnego.
- Wykrywaj sprzęt MIDI za pomocą klasy Java
MidiManager. - Uzyskaj obiekt
MidiDevicew języku Java odpowiadający sprzętowi MIDI. - Przekaż obiekt
MidiDevicew języku Java do kodu natywnego za pomocą JNI.
Odkrywanie sprzętu i portów
Obiekty portów wejściowych i wyjściowych nie należą do aplikacji. Reprezentują porty na urządzeniu MIDI. Aby wysłać dane MIDI do urządzenia, aplikacja otwiera MIDIInputPort, a następnie zapisuje w nim dane. Z kolei aby odbierać dane, aplikacja otwiera MIDIOutputPort. Aby aplikacja działała prawidłowo, musi mieć pewność, że otwierane przez nią porty są odpowiedniego typu. Wykrywanie urządzeń i portów odbywa się po stronie Javy.
Oto metoda, która wykrywa każde urządzenie MIDI i sprawdza jego porty. Zwraca listę urządzeń z portami wyjściowymi do odbierania danych lub listę urządzeń z portami wejściowymi do wysyłania danych. Urządzenie MIDI może mieć zarówno porty wejściowe, jak i wyjściowe.
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; }
Aby używać funkcji AMidi w kodzie C/C++, musisz uwzględnić AMidi/AMidi.h i połączyć z biblioteką amidi. Oba te elementy znajdziesz w Android NDK.
Strona Javy powinna przekazywać co najmniej 1 obiekt MidiDevice i numery portów do warstwy natywnej za pomocą wywołania JNI. Warstwa natywna powinna wykonać te czynności:
- Dla każdego obiektu
MidiDevicew języku Java uzyskaj obiektAMidiDevice, używającAMidiDevice_fromJava(). - Uzyskaj
AMidiInputPortlubAMidiOutputPortodAMidiDevicezAMidiInputPort_open()lubAMidiOutputPort_open(). - Używaj uzyskanych portów do wysyłania lub odbierania danych MIDI.
Zatrzymywanie AMidi
Aplikacja Java powinna sygnalizować warstwie natywnej, że ma zwolnić zasoby, gdy nie używa już urządzenia MIDI. Może to być spowodowane odłączeniem urządzenia MIDI lub zamknięciem aplikacji.
Aby zwolnić zasoby MIDI, kod powinien wykonać te zadania:
- Zatrzymaj odczytywanie lub zapisywanie w portach MIDI. Jeśli do odpytywania o dane wejściowe używasz wątku odczytu (patrz Implementowanie pętli odpytywania poniżej), zatrzymaj go.
- Zamknij wszystkie otwarte obiekty
AMidiInputPortlubAMidiOutputPortza pomocą funkcjiAMidiInputPort_close()lubAMidiOutputPort_close(). - Zwolnij
AMidiDeviceza pomocąAMidiDevice_release().
Odbieranie danych MIDI
Typowym przykładem aplikacji MIDI, która odbiera dane MIDI, jest „syntezator wirtualny”, który odbiera dane o skuteczności MIDI w celu sterowania syntezą dźwięku.
Przychodzące dane MIDI są odbierane asynchronicznie. Dlatego najlepiej jest odczytywać dane MIDI w osobnym wątku, który stale odpytuje co najmniej 1 port wyjściowy MIDI. Może to być wątek w tle lub wątek audio. AMidi nie blokuje odczytu z portu, dlatego można go bezpiecznie używać w wywołaniu zwrotnym audio.
Konfigurowanie urządzenia MIDI i jego portów wyjściowych
Aplikacja odczytuje przychodzące dane MIDI z portów wyjściowych urządzenia. Strona Java aplikacji musi określić, którego urządzenia i których portów użyć.
Ten fragment kodu tworzy obiekt MidiManager z usługi MIDI Androida i otwiera obiekt MidiDevice dla pierwszego znalezionego urządzenia. Po otwarciu MidiDevice wywołanie zwrotne jest odbierane przez instancję MidiManager.OnDeviceOpenedListener(). Wywoływana jest metoda onDeviceOpened tego detektora, która następnie wywołuje metodę startReadingMidi(), aby otworzyć port wyjściowy 0 na urządzeniu. Jest to funkcja JNI zdefiniowana w pliku AppMidiManager.cpp. Ta funkcja jest wyjaśniona w następnym fragmencie kodu.
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); } } }
Kod natywny tłumaczy urządzenie MIDI po stronie Javy i jego porty na odwołania używane przez funkcje AMidi.
Oto funkcja JNI, która tworzy obiekt AMidiDevice, wywołując funkcję AMidiDevice_fromJava(), a następnie wywołuje funkcję AMidiOutputPort_open(), aby otworzyć port wyjściowy na urządzeniu:
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...
}
Wdrażanie pętli odpytywania
Aplikacje, które odbierają dane MIDI, muszą odpytywać port wyjściowy i reagować, gdy AMidiOutputPort_receive() zwraca liczbę większą niż 0.
W przypadku aplikacji o niskiej przepustowości, takich jak zakres MIDI, możesz przeprowadzać sondowanie w wątku w tle o niskim priorytecie (z odpowiednimi uśpieniami).
W przypadku aplikacji, które generują dźwięk i mają bardziej rygorystyczne wymagania dotyczące wydajności w czasie rzeczywistym, możesz przeprowadzać sondowanie w głównym wywołaniu zwrotnym generowania dźwięku (wywołanie zwrotne BufferQueue w OpenSL ES, wywołanie zwrotne danych AudioStream w AAudio).
AMidiOutputPort_receive() nie blokuje działania, więc ma bardzo niewielki wpływ na wydajność.
Funkcja readThreadRoutine() wywołana z funkcji startReadingMidi() powyżej może wyglądać tak:
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;
}
}
}
Aplikacja korzystająca z natywnego interfejsu API audio (np. OpenSL ES lub AAudio) może dodać kod odbioru MIDI do wywołania zwrotnego generowania dźwięku w ten sposób:
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…
// ...
}
Poniższy diagram przedstawia przepływ aplikacji do odczytu MIDI:

Wysyłaj dane MIDI
Typowym przykładem aplikacji do zapisu MIDI jest kontroler lub sekwencer MIDI.
Konfigurowanie urządzenia MIDI i jego portów wejściowych
Aplikacja zapisuje wychodzące dane MIDI w portach wejściowych urządzenia MIDI. Strona Java aplikacji musi określić, którego urządzenia MIDI i których portów używać.
Poniższy kod konfiguracji jest odmianą powyższego przykładu odbierania. Tworzy MidiManager z usługi MIDI Androida. Następnie otwiera pierwsze znalezione urządzenieMidiDevice i wywołuje funkcję startWritingMidi(), aby otworzyć pierwszy port wejściowy na urządzeniu. Jest to wywołanie JNI zdefiniowane w AppMidiManager.cpp. Funkcja jest wyjaśniona w następnym fragmencie kodu.
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); } } }
Oto funkcja JNI, która tworzy obiekt AMidiDevice, wywołując metodę AMidiDevice_fromJava(), a następnie wywołuje metodę AMidiInputPort_open(), aby otworzyć port wejściowy na urządzeniu:
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;
}
Wysyłaj dane MIDI
Ponieważ czas wysyłania danych MIDI jest dobrze znany i kontrolowany przez samą aplikację, transmisja danych może odbywać się w głównym wątku aplikacji MIDI. Jednak ze względu na wydajność (jak w przypadku sekwencera) generowanie i transmisja MIDI mogą odbywać się w osobnym wątku.
Aplikacje mogą wysyłać dane MIDI, gdy jest to wymagane. Pamiętaj, że AMidi blokuje zapisywanie danych.
Oto przykładowa metoda JNI, która odbiera bufor poleceń MIDI i zapisuje go:
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);
}
Poniższy diagram przedstawia przepływ aplikacji do zapisu MIDI:

Wywołania zwrotne
Chociaż nie jest to funkcja AMidi, kod natywny może wymagać przekazania danych z powrotem do Javy (np. w celu zaktualizowania interfejsu). Aby to zrobić, musisz napisać kod po stronie Javy i w warstwie natywnej:
- Utwórz metodę wywołania zwrotnego po stronie Javy.
- Napisz funkcję JNI, która przechowuje informacje potrzebne do wywołania funkcji zwrotnej.
Gdy nadejdzie czas na wywołanie zwrotne, kod natywny może utworzyć
Oto metoda wywołania zwrotnego po stronie Javy, onNativeMessageReceive():
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); } }); }
Oto kod C funkcji JNI, która konfiguruje wywołanie zwrotne do MainActivity.onNativeMessageReceive(). Wywołania Javy MainActivity
initNative() podczas uruchamiania:
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");
}
Gdy nadejdzie czas na odesłanie danych do Javy, kod natywny pobiera wskaźniki wywołania zwrotnego i tworzy wywołanie zwrotne:
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);
}
Dodatkowe materiały
- Dokumentacja AMidi
- Zobacz pełną przykładową aplikację Native MIDI na GitHubie.