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 w C/C++.
Aplikacje na Androida MIDI zwykle komunikują się z usługą Android MIDI za pomocą interfejsu API midi
. Aplikacje MIDI zależą przede wszystkim od MidiManager
, aby wykrywać, otwierać i zamykać obiekty MidiDevice
oraz przekazywać dane do i z każdego urządzenia przez porty wejściowe i wyjściowe MIDI:
Gdy używasz AMidi, przekazujesz adres MidiDevice
do natywnej warstwy kodu za pomocą wywołania JNI. Następnie Amidi tworzy odniesienie do AMidiDevice
, który ma większość funkcji MidiDevice
. Twój kod natywny korzysta z 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 aplikacji C/C++
z transmisją MIDI. Wywołania JNI i wywołania zwrotne do strony aplikacji w języku Java są mniejsze. Na przykład cyfrowy syntezator zaimplementowany w kodzie C może na przykład odbierać kluczowe zdarzenia bezpośrednio z AMidiDevice
, a nie czekać na wywołanie JNI, które wyśle je po stronie Javy. Z kolei algorytmiczny proces komponowania może wysyłać wydajność MIDI bezpośrednio do AMidiDevice
bez wywoływania strony Javy w celu przesyłania kluczowych zdarzeń.
Choć AMidi ulepsza 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 to stamtąd zabrać.
Czasami konieczne może być przekazanie informacji z warstwy interfejsu do kodu natywnego. Na przykład 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 musisz przesłać dane z powrotem, aby zaktualizować interfejs, możesz jak zwykle wywołać odpowiedź z warstwy natywnej.
W tym dokumencie pokazujemy, jak skonfigurować aplikację do kodu natywnego AMidi. Zawiera on przykłady zarówno wysyłania, jak i odbierania poleceń MIDI. Pełny przykład działania znajdziesz w przykładowej aplikacji NativeMidi.
Użyj 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 tryby.
Uruchom AMidi
W przypadku Javy aplikacja musi wykryć podłączony sprzęt MIDI, utworzyć odpowiedni MidiDevice
i przekazać go do kodu natywnego.
- Odkryj sprzęt MIDI z klasą
MidiManager
w języku Java. - Uzyskaj obiekt Java
MidiDevice
odpowiadający sprzętowi MIDI. - Przekaż kod Java
MidiDevice
do kodu natywnego 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 MIDIInputPort
i zapisuje na nim dane. 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 dodać AMidi/AMidi.h
i połączyć z biblioteką amidi
. Te materiały znajdziesz w pakiecie Android NDK.
Strona Javy powinna przekazywać co najmniej 1 obiekt MidiDevice
i portować numery portów do warstwy natywnej za pomocą wywołania JNI. Warstwa natywna powinna następnie wykonać te czynności:
- Dla każdej biblioteki Java
MidiDevice
uzyskajAMidiDevice
za pomocąAMidiDevice_fromJava()
. - Uzyskaj
AMidiInputPort
lubAMidiOutputPort
zAMidiDevice
zAMidiInputPort_open()
lubAMidiOutputPort_open()
. - Używaj uzyskanych portów do wysyłania i/lub odbierania danych MIDI.
Zatrzymaj AMidi
Aplikacja w Javie powinna sygnalizować warstwę natywną o zwolnieniu zasobów, gdy nie korzysta już z urządzenia MIDI. Przyczyną mogło być odłączenie urządzenia MIDI lub zamykanie aplikacji.
Aby zwolnić zasoby MIDI, kod powinien wykonać te czynności:
- Zatrzymaj odczytywanie i/lub zapisywanie w portach MIDI. Jeśli ankieta dotycząca danych wejściowych jest wykonywana za pomocą wątku czytania (zobacz poniżej Implementowanie pętli odpytywania), zatrzymaj wątek.
- Zamknij wszystkie otwarte obiekty
AMidiInputPort
lubAMidiOutputPort
z funkcjamiAMidiInputPort_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 wydajności MIDI, by sterować syntezą dźwięku.
Przychodzące dane MIDI są odbierane asynchronicznie. Dlatego najlepiej odczytywać MIDI w osobnym wątku, który stale sonduje 1 port wyjściowy lub 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.
Konfigurowanie urządzenia MidiDevice i jego portów wyjściowych
Aplikacja odczytuje przychodzące dane MIDI z portów wyjściowych urządzenia. Aplikacja po stronie Javy musi określać, których urządzeń i portów ma używać.
Ten fragment tworzy plik MidiManager
z usługi MIDI na Androidzie i otwiera MidiDevice
dla pierwszego znalezionego urządzenia. Po otwarciu MidiDevice
następuje wywołanie zwrotne do instancji MidiManager.OnDeviceOpenedListener()
. Wywoływana jest metoda onDeviceOpened
tego odbiornika, która wywołuje metodę startReadingMidi()
, aby otworzyć port wyjściowy 0 na urządzeniu. To jest funkcja JNI zdefiniowana w AppMidiManager.cpp
. Funkcja ta została objaśniona 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 Java i jego porty na odwołania używane przez funkcje AMidi.
Oto funkcja JNI, która tworzy AMidiDevice
przez wywołanie AMidiDevice_fromJava()
, a następnie wywołuje AMidiOutputPort_open()
, by 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 otrzymujące dane MIDI muszą sondować port wyjściowy i odpowiadać, gdy AMidiOutputPort_receive()
zwraca liczbę większą niż 0.
W przypadku aplikacji o niskiej przepustowości, np. zakresu MIDI, możesz przeprowadzać sondowanie w wątku w tle o niskim priorytecie (z odpowiednimi uśpieniem).
W przypadku aplikacji generujących dźwięk i mających bardziej rygorystyczne wymagania dotyczące wydajności w czasie rzeczywistym można przeprowadzać sondowanie w głównym wywołaniu zwrotnym generowania dźwięku (wywołaniu zwrotnym BufferQueue
dla OpenSL ES, czyli wywołania zwrotnego danych AudioStream w AAudio).
Parametr AMidiOutputPort_receive()
nie blokuje elementów, 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 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 czytania MIDI:
Wysyłaj dane MIDI
Typowym przykładem aplikacji do zapisu MIDI jest kontroler lub sekwencer MIDI.
Skonfiguruj urządzenie MidiDevice i jego porty wejściowe
Aplikacja zapisuje wychodzące dane MIDI do portów wejściowych urządzenia MIDI. Aplikacja po stronie Javy musi określać, których urządzeń MIDI i portów używać.
Kod konfiguracji widoczny poniżej jest odmianą kodu odebranego powyżej. Utworzy to MidiManager
z usługi MIDI na Androidzie. Następnie otwiera pierwszy MidiDevice
, który znajdzie i wywołuje metodę startWritingMidi()
, by otworzyć pierwszy port wejściowy urządzenia. Jest to wywołanie JNI zdefiniowane w AppMidiManager.cpp
. Funkcja została omówiona
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
przez wywołanie AMidiDevice_fromJava()
, a następnie wywołuje AMidiInputPort_open()
, by 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 wychodzących danych MIDI jest dobrze zrozumiały i kontrolowany przez samą aplikację, przesyłanie danych można przeprowadzać w głównym wątku aplikacji MIDI. Jednak ze względu na wydajność (jak w przypadku sekwencera) generowanie i przesyłanie MIDI można przeprowadzić w osobnym wątku.
Aplikacje mogą wysyłać dane MIDI, gdy jest to wymagane. Pamiętaj, że AMidi blokuje podczas zapisywania danych.
Oto przykładowa metoda JNI, która odbiera bufor poleceń MIDI i je 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
Chociaż nie jest to wyłącznie funkcja AMidi, Twój kod natywny może wymagać przekazywania danych z powrotem na stronę Javy (np. w celu zaktualizowania interfejsu użytkownika). W tym celu musisz napisać kod po stronie Javy i w warstwie natywnej:
- Utwórz metodę wywołania zwrotnego po stronie Javy.
- Utwórz funkcję JNI, która przechowuje informacje potrzebne do wywołania wywołania zwrotnego.
W chwili wywołania zwrotnego kod natywny tworzy
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()
. Java MainActivity
wywołuje initNative()
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 przyjdzie czas na wysłanie danych z powrotem do Javy, natywny kod pobierze 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 githubie.