Interfejs AMidi API jest dostępny w Androidzie NDK r20b i nowszych. Umożliwia programistom wysyłanie i odbieranie danych MIDI za pomocą kodu C/C++.
Aplikacje MIDI na Androida komunikują się z usługą Android MIDI za pomocą interfejsu API midi
. Aplikacje MIDI wykorzystują przede wszystkim MidiManager
do wykrywania, otwierania i zamykania 1 lub większej liczby obiektów MidiDevice
oraz przekazywania danych do i z każdego urządzenia przez porty wejściowe i wyjściowe urządzenia MIDI:
Jeśli korzystasz z AMidi, przekazujesz adres MidiDevice
do warstwy kodu natywnego za pomocą wywołania JNI. Następnie AMidi tworzy odniesienie do elementu AMidiDevice
, który ma większość funkcji MidiDevice
. Twój kod natywny korzysta z funkcji AMidi, które komunikują się bezpośrednio z interfejsem AMidiDevice
. AMidiDevice
łączy się bezpośrednio z usługą MIDI:
Za pomocą połączeń AMidi możesz ściśle zintegrować logikę dźwięku i sterowania C/C++ aplikacji z transmisją MIDI. Nie potrzeba już wywołań JNI ani wywołań zwrotnych po stronie Java aplikacji. Na przykład syntezator cyfrowy zaimplementowany w kodzie C może odbierać kluczowe zdarzenia bezpośrednio z interfejsu AMidiDevice
, zamiast czekać na wywołanie JNI w celu wysłania zdarzeń ze strony Javy. Również algorytmiczny proces tworzenia może wysyłać wydajność MIDI bezpośrednio do interfejsu AMidiDevice
bez wywoływania po stronie Javy w celu przekazywania kluczowych zdarzeń.
Chociaż AMidi poprawia bezpośrednie połączenie z urządzeniami MIDI, aplikacje nadal muszą używać MidiManager
do wykrywania i otwierania obiektów MidiDevice
. AMidi może zrobić to stamtąd.
Czasami konieczne może być przekazanie informacji z warstwy interfejsu do kodu natywnego. np. gdy zdarzenia MIDI są wysyłane w odpowiedzi na przyciski na ekranie. Aby to zrobić, utwórz niestandardowe wywołania JNI swojej logiki natywnej. Jeśli musisz wysłać dane z powrotem, aby zaktualizować interfejs, możesz wykonać wywołanie zwrotne z warstwy natywnej.
Ten dokument pokazuje, jak skonfigurować aplikację do obsługi kodu natywnego AMidi z przykładami wysyłania i odbierania poleceń MIDI. Pełny 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ą i odbierają MIDI czy oba te sposoby.
Uruchom AMidi
Po stronie Javy aplikacja musi wykryć dołączony sprzęt MIDI, utworzyć odpowiedni obiekt MidiDevice
i przekazać go do kodu natywnego.
- Odkryj sprzęt MIDI dzięki klasie Java
MidiManager
. - Uzyskaj obiekt Java
MidiDevice
odpowiadający sprzętowi MIDI. - Przekaż
MidiDevice
do kodu natywnego w Javie za pomocą JNI.
Odkryj sprzęt i porty
Obiekty portów wejściowych i wyjściowych nie należą do aplikacji. Reprezentują porty urządzenia midi. Aby wysłać dane MIDI na urządzenie, aplikacja otwiera element MIDIInputPort
, a potem zapisuje w nim dane. I odwrotnie, 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 umieścić w kodzie AMidi/AMidi.h
i połączyć z biblioteką amidi
. Oba rodzaje danych znajdziesz w pakiecie NDK dla Androida.
Strona Java powinna przekazywać co najmniej 1 obiekt MidiDevice
i numer portu do warstwy natywnej za pomocą wywołania JNI. Warstwa natywna powinna następnie wykonać te czynności:
- Dla każdej funkcji
MidiDevice
w Javie uzyskaj elementAMidiDevice
za pomocą poleceniaAMidiDevice_fromJava()
. - Uzyskaj
AMidiInputPort
lubAMidiOutputPort
zAMidiDevice
za pomocąAMidiInputPort_open()
lubAMidiOutputPort_open()
. - Używaj uzyskanych portów do wysyłania lub odbierania danych MIDI.
Zatrzymaj AMidi
Aplikacja w Javie powinna sygnalizować warstwę natywną, aby zwalniała zasoby, gdy już nie korzysta z urządzenia MIDI. Może to wynikać z tego, że urządzenie MIDI zostało odłączone lub aplikacja się zamyka.
Aby zwolnić zasoby MIDI, Twój kod powinien wykonać te czynności:
- Zatrzymaj odczyt lub zapis na portach MIDI. Jeśli używasz wątku odczytu do odpytywania danych wejściowych (patrz sekcja Wdrażanie pętli odpytywania poniżej), zatrzymaj wątek.
- Zamknij wszystkie otwarte obiekty
AMidiInputPort
lubAMidiOutputPort
za pomocą funkcjiAMidiInputPort_close()
bądźAMidiOutputPort_close()
. - Zwolnij
AMidiDevice
, używając:AMidiDevice_release()
.
Odbieranie danych MIDI
Typowym przykładem aplikacji MIDI, która odbiera dźwięk MIDI, jest „wirtualny syntezator”, który odbiera dane o wydajności MIDI, aby sterować syntezą dźwięku.
Przychodzące dane MIDI są odbierane asynchronicznie. Dlatego najlepiej czytać MIDI w osobnym wątku, który stale odpytuje jeden port wyjściowy MIDI. Może to być wątek w tle lub wątek audio. AMidi nie blokuje odczytu z portu i dlatego można go bezpiecznie używać w wywołaniu zwrotnym audio.
Skonfiguruj urządzenie MidiDevice i jego porty wyjściowe
Aplikacja odczytuje przychodzące dane MIDI z portów wyjściowych urządzenia. Strona aplikacji w języku Java musi określać używane urządzenie i porty.
Ten fragment tworzy polecenie MidiManager
z usługi MIDI na Androidzie i otwiera MidiDevice
dla pierwszego znalezionego urządzenia. Po otwarciu interfejsu MidiDevice
zostanie odebrane wywołanie zwrotne do instancji MidiManager.OnDeviceOpenedListener()
. Wywoływana jest metoda onDeviceOpened
tego nasłuchującego, która wywołuje metodę startReadingMidi()
, aby otworzyć port wyjściowy 0 na urządzeniu. Jest to funkcja JNI zdefiniowana w AppMidiManager.cpp
. Została ona omówiona w następnym fragmencie.
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 odniesienia wykorzystywane przez funkcje AMidi.
Oto funkcja JNI, która tworzy AMidiDevice
, wywołując AMidiDevice_fromJava()
, a następnie wywołując AMidiOutputPort_open()
, aby otworzyć port wyjściowy urządzenia:
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 odbierające dane MIDI muszą sondować 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 przeprowadzić ankietę w wątku w tle o niskim priorytecie (z odpowiednią fazą snu).
W przypadku aplikacji generujących dźwięk i mających bardziej rygorystyczne wymagania dotyczące wydajności w czasie rzeczywistym możesz przeprowadzić ankietę w głównym wywołaniu zwrotnym generowania dźwięku (wywołanie zwrotne BufferQueue
w przypadku OpenSL ES, wywołanie zwrotne danych AudioStream w AAudio).
AMidiOutputPort_receive()
nie blokuje blokowania, więc ma bardzo niewielki wpływ na wydajność.
Funkcja readThreadRoutine()
wywołana z powyższej funkcji startReadingMidi()
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 używająca interfejsu API do obsługi natywnej dźwięku (np. OpenSL ES lub AAudio) może dodać kod odbierania MIDI do wywołania zwrotnego generującego dźwięk 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:
Wyślij dane MIDI
Typowym przykładem aplikacji do pisania MIDI jest kontroler lub sekwencer MIDI.
Skonfiguruj urządzenie MidiDevice i jego porty wejściowe
Aplikacja zapisuje wychodzące dane MIDI na porty wejściowe urządzenia MIDI. Strona aplikacji w języku Java musi określać, których urządzeń i portów MIDI używać.
Ten kod konfiguracji jest wariantem z przykładem konfiguracji powyżej. Tworzy MidiManager
z usługi MIDI na Androidzie. Następnie otwiera pierwszy znaleziony port MidiDevice
i wywołuje metodę startWritingMidi()
, aby otworzyć pierwszy port wejściowy na urządzeniu. Jest to wywołanie JNI zdefiniowane w AppMidiManager.cpp
. Funkcję tę objaśniamy
w następnym fragmencie.
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 AMidiDevice
, wywołując AMidiDevice_fromJava()
, a następnie wywołując AMidiInputPort_open()
, aby otworzyć port wejściowy urządzenia:
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;
}
Wyślij dane MIDI
Ponieważ dane MIDI są dobrze znane i kontrolowane przez samą aplikację, przesyłanie danych może odbywać się w wątku głównym aplikacji MIDI. Jednak ze względu na wydajność (jak w przypadku sekwencera) generowanie i przesyłanie MIDI może być wykonywane w oddzielnym wątku.
Aplikacje mogą wysyłać dane MIDI, gdy jest to konieczne. Pamiętaj, że AMidi blokuje podczas zapisywania danych.
Oto przykładowa metoda JNI, która odbiera bufor poleceń MIDI i go zapisuje:
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 pisania MIDI:
Wywołania zwrotne
Choć nie jest to tylko funkcja AMidi, Twój kod natywny może wymagać przekazywania danych z powrotem do języka Java (np. w celu zaktualizowania interfejsu użytkownika). 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 przechowującą informacje potrzebne do wywołania 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 initNative()
w Javie MainActivity
przy uruchamianiu:
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 wysł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
- Informacje o AMIDi
- Zobacz pełną przykładową aplikację natywnej MIDI na github.