Interfejs AMidi jest dostępny w Android NDK w wersji 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 midi
API do komunikacji z usługą MIDI na Androidzie. Aplikacje MIDI zależą głównie od interfejsu MidiManager
, który umożliwia wykrywanie, otwieranie i zamykanie co najmniej jednego obiektu MidiDevice
oraz przesyłanie danych do i z każdego urządzenia za pomocą portów wejściowych i wyjściowych MIDI:
Gdy używasz AMidi, przekazujesz adres MidiDevice
do warstwy kodu natywnego za pomocą wywołania JNI. Następnie AMidi tworzy odwołanie do AMidiDevice
, które ma większość funkcji MidiDevice
. Twój 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ę audio/sterowania w C/C++ aplikacji z transmisją MIDI. Nie ma potrzeby wywoływania JNI ani wywołań zwrotnych do części aplikacji napisanej w języku Java. Na przykład syntezator cyfrowy zaimplementowany w kodzie C może otrzymywać zdarzenia klawiszy bezpośrednio z AMidiDevice
, zamiast czekać na wywołanie JNI, które przesyła zdarzenia z części napisanej w języku Java. Proces kompozycji algorytmicznej może też wysyłać wykonanie MIDI bezpośrednio do AMidiDevice
bez wywoływania z powrotem strony Java w celu przesłania kluczowych zdarzeń.
Chociaż AMidi poprawia bezpośrednie połączenie z urządzeniami MIDI, aplikacje nadal muszą używać interfejsu MidiManager
do wykrywania i otwierania obiektów MidiDevice
. AMidi może
zająć 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 naciśnięcie przycisków na ekranie. Aby to zrobić, utwórz niestandardowe wywołania JNI do logiki natywnej. Jeśli musisz odesłać dane, aby zaktualizować interfejs, możesz jak zwykle wywołać funkcję z warstwy natywnej.
W tym dokumencie pokazujemy, jak skonfigurować aplikację z kodem natywnym AMidi, podając przykłady wysyłania i odbierania poleceń MIDI. Kompletny przykład działania 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ą, odbierają czy wykonują obie te czynności.
Uruchom AMidi
Po stronie Javy aplikacja musi wykryć podłączony sprzęt MIDI, utworzyć odpowiedni obiekt MidiDevice
i przekazać go do kodu natywnego.
- Odkrywanie sprzętu MIDI za pomocą klasy Java
MidiManager
. - Uzyskaj obiekt Java
MidiDevice
odpowiadający sprzętowi MIDI. - Przekazywanie obiektu Java
MidiDevice
do kodu natywnego za pomocą JNI.
Poznaj sprzęt i porty
Obiekty portów wejściowych i wyjściowych nie należą do aplikacji. Reprezentują one porty na urządzeniu MIDI. Aby wysłać dane MIDI do urządzenia, aplikacja otwiera MIDIInputPort
, a następnie zapisuje w nim dane. Aby otrzymać 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 dołączyć AMidi/AMidi.h
i połączyć się z biblioteką amidi
. Oba te elementy znajdziesz w Android NDK.
Po stronie Javy należy przekazać co najmniej 1 obiekt MidiDevice
i numery portów do warstwy natywnej za pomocą wywołania JNI. Warstwa natywna powinna następnie wykonać te czynności:
- Dla każdego obiektu Java
MidiDevice
uzyskaj obiektAMidiDevice
za pomocą metodyAMidiDevice_fromJava()
. - Uzyskaj
AMidiInputPort
lubAMidiOutputPort
odAMidiDevice
za pomocąAMidiInputPort_open()
lubAMidiOutputPort_open()
. - Użyj uzyskanych portów do wysyłania lub odbierania danych MIDI.
Stop AMidi
Gdy aplikacja Java nie będzie już używać urządzenia MIDI, powinna wysłać do warstwy natywnej sygnał o zwolnieniu zasobów. Może to być spowodowane odłączeniem urządzenia MIDI lub zamknięciem aplikacji.
Aby zwolnić zasoby MIDI, kod powinien wykonać te zadania:
- Zatrzymanie odczytu lub zapisu na portach MIDI. Jeśli do sprawdzania danych wejściowych używasz wątku odczytu (patrz Implementowanie pętli sprawdzania poniżej), zatrzymaj go.
- Zamknij wszystkie otwarte obiekty
AMidiInputPort
lubAMidiOutputPort
za pomocą funkcjiAMidiInputPort_close()
lubAMidiOutputPort_close()
. - Zwolnij
AMidiDevice
za pomocąAMidiDevice_release()
.
Odbieranie danych MIDI
Typowym przykładem aplikacji MIDI, która odbiera MIDI, jest „wirtualny syntezator”, który odbiera dane o wykonaniu MIDI w celu sterowania syntezą dźwięku.
Przychodzące dane MIDI są odbierane asynchronicznie. Dlatego najlepiej jest odczytywać MIDI w osobnym wątku, który stale odpytuje jeden lub więcej portów wyjściowych 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 MidiDevice i jego portów wyjściowych
Aplikacja odczytuje przychodzące dane MIDI z portów wyjściowych urządzenia. Część aplikacji napisana w Javie musi określić, które urządzenie i porty mają być używane.
Ten fragment kodu tworzy MidiManager
z usługi MIDI na Androidzie i otwiera MidiDevice
dla pierwszego znalezionego urządzenia. Gdy MidiDevice
zostanie otwarty, wywołanie zwrotne zostanie odebrane w instancji MidiManager.OnDeviceOpenedListener()
. Wywoływana jest metoda onDeviceOpened
tego odbiornika, 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
. Funkcja ta 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 otrzymują dane MIDI, muszą odpytywać port wyjściowy i odpowiadać, gdy funkcja
AMidiOutputPort_receive()
zwraca liczbę większą od zera.
W przypadku aplikacji o niskiej przepustowości, takich jak zakres MIDI, możesz odpytywać w wątku w tle o niskim priorytecie (z odpowiednimi przerwami).
W przypadku aplikacji, które generują dźwięk i mają bardziej rygorystyczne wymagania dotyczące wydajności w czasie rzeczywistym, możesz odpytywać w głównym wywołaniu zwrotnym generowania dźwięku (wywołanie zwrotne BufferQueue
w OpenSL ES, wywołanie zwrotne danych AudioStream w AAudio).
Funkcja AMidiOutputPort_receive()
nie blokuje działania innych funkcji, więc ma bardzo niewielki wpływ na wydajność.
Funkcja readThreadRoutine()
wywoływana 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 odbierania 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…
// ...
}
Diagram poniżej przedstawia przepływ danych w aplikacji do odczytywania MIDI:
Wysyłanie danych MIDI
Typowym przykładem aplikacji do pisania MIDI jest kontroler lub sekwencer MIDI.
Konfigurowanie urządzenia MIDI i jego portów wejściowych
Aplikacja zapisuje wychodzące dane MIDI na portach wejściowych urządzenia MIDI. Część aplikacji napisana w Javie musi określić, których urządzeń i portów MIDI ma używać.
Poniższy kod konfiguracji jest odmianą przykładu odbioru podanego powyżej. Tworzy MidiManager
z usługi MIDI na Androidzie. 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łanie danych MIDI
Czas wysyłania danych MIDI jest dobrze znany i kontrolowany przez samą aplikację, więc transmisję danych można przeprowadzić w głównym wątku aplikacji MIDI. Ze względu na wydajność (np. w przypadku sekwencera) generowanie i przesyłanie MIDI może odbywać się w osobnym wątku.
Aplikacje mogą wysyłać dane MIDI w dowolnym momencie. 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 ilustruje przepływ w aplikacji do pisania MIDI:
Wywołania zwrotne
Chociaż nie jest to ściśle funkcja AMidi, kod natywny może wymagać przekazywania danych z powrotem do strony Java (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 zwrotnego.
Gdy nadejdzie czas wywołania zwrotnego, 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 Java 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 trzeba przesłać dane z powrotem 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
- AMidi – źródła wiedzy
- Zobacz pełną przykładową aplikację Native MIDI na GitHubie.