Interfejs API AMidi jest dostępne na 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 do komunikacji z usługą MIDI na Androida używają interfejsu API midi
. Aplikacje MIDI zależą głównie od MidiManager
, aby wykrywać, otwierać i zamykać co najmniej jeden obiekt MidiDevice
oraz przesyłać dane do i z każdego urządzenia przez porty MIDI wejścia i wyjścia:
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
. W Twoim kodzie natywnym
Funkcje AMidi, które się komunikują
bezpośrednio z AMidiDevice
. AMidiDevice
łączy się bezpośrednio z
Usługa MIDI:
Za pomocą wywołań AMidi możesz zintegrować logikę sterowania dźwiękiem/kontroli w C/C++ z transmisją MIDI. Nie musisz już tak często wywoływać metod JNI ani funkcji zwrotnych po stronie aplikacji w języku Java. Na przykład cyfrowy syntezator zaimplementowany w kodzie C może otrzymywać kluczowe zdarzenia bezpośrednio z AMidiDevice
, zamiast czekać na wywołanie metody JNI, aby wysłać zdarzenia z poziomu Javy. Albo algorytm do tworzenia kompozycji może wysyłać dane MIDI bezpośrednio do AMidiDevice
bez wywoływania kodu Java w celu przesyłania kluczowych zdarzeń.
Mimo że AMidi umożliwia bezpośrednie połączenie z urządzeniami MIDI, aplikacje nadal muszą używać MidiManager
do wykrywania i otwierania obiektów MidiDevice
. AMidi może
a potem jej ściągnąć.
Czasami trzeba przekazać informacje z poziomu interfejsu do kodu natywnego. Na przykład wtedy, 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 chcesz przesłać dane, aby zaktualizować interfejs, możesz to zrobić z poziomu natywnej warstwy.
Z tego dokumentu dowiesz się, jak skonfigurować aplikację natywnych kodów AMidi. Znajdziesz tu przykłady wysyłania i odbierania poleceń MIDI. Pełny działający przykład znajdziesz NativeMidi przykładową aplikację.
Używanie AMidi
Wszystkie aplikacje korzystające z AMidi mają te same kroki konfiguracji i zamknięcia. wysyłanie i odbieranie MIDI.
Rozpocznij AMidi
Na stronie Javy aplikacja musi wykryć podłączone urządzenie MIDI, utworzyć odpowiedni obiekt MidiDevice
i przekazać go do kodu natywnego.
- Poznaj sprzęt MIDI na zajęciach z Java
MidiManager
. - Uzyskaj obiekt Java
MidiDevice
odpowiadający sprzętowi MIDI. - Przekaż kod Java
MidiDevice
do kodu natywnego za pomocą JNI.
Sprzęt i porty
Obiekty portu wejściowego i wyjściowego nie należą do aplikacji. Reprezentują porty
na urządzeniu Midi. Aby wysłać dane MIDI na urządzenie, aplikacja otwiera MIDIInputPort
, a następnie zapisuje w nim dane. Aby uzyskać dane, aplikacja musi:
otwiera plik MIDIOutputPort
. Aby działać prawidłowo, aplikacja musi mieć pewność, że porty, które otwiera, 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 sygnału lub listę urządzeń z portami wejściowymi do wysyłania danych. Urządzenie MIDI może mają 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łącz je z biblioteką amidi
. Oba te formaty można znaleźć
w Android NDK.
Strona Javy powinna przekazywać co najmniej 1 obiekt MidiDevice
i numer portu do
w warstwie natywnej za pomocą wywołania JNI. Warstwa natywna powinna wtedy wykonać
następujące kroki:
- Dla każdej biblioteki Java
MidiDevice
uzyskajAMidiDevice
za pomocąAMidiDevice_fromJava()
. - Uzyskaj
AMidiInputPort
lubAMidiOutputPort
z:AMidiDevice
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ą, że ma zwolnić zasoby, jeśli tak nie jest. przy korzystaniu z 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 czynności:
- Zatrzymaj odczyt lub zapis do portów MIDI. Jeśli korzystasz z odczytu wątek do sondowania w poszukiwaniu danych wejściowych (zobacz poniżej Implementowanie pętli odpytań), Zatrzymać wątek.
- Zamknij wszystkie otwarte obiekty
AMidiInputPort
lubAMidiOutputPort
za pomocą funkcjiAMidiInputPort_close()
lubAMidiOutputPort_close()
. - Zwolnij
AMidiDevice
zAMidiDevice_release()
.
Odbieranie danych MIDI
Typowym przykładem aplikacji MIDI, która odbiera dane MIDI, jest „syntezator wirtualny”, który odbiera dane dotyczące wykonania MIDI w celu sterowania syntezą dźwięku.
Przychodzące dane MIDI są odbierane asynchronicznie. Dlatego najlepiej odczytywać dane MIDI w osobnym wątku, który stale odczytuje jeden lub więcej portów wyjściowych MIDI. Może to być wątek w tle lub wątek audio. AMidi nie blokuje się podczas odczytu z portu, więc można go bezpiecznie używać wewnątrz oddzwanianie.
Konfigurowanie urządzenia MidiDevice i jego portów wyjściowych
Aplikacja odczytuje przychodzące dane MIDI z portów wyjściowych urządzenia. Od strony Javy aplikacji musi określić urządzenie i porty, które mają być używane.
Ten fragment kodu tworzy
MidiManager
z usługi MIDI na Androidzie i otwiera się
MidiDevice
dla pierwszego znalezionego urządzenia. Gdy MidiDevice
był
otwarto wywołanie zwrotne do wystąpienia
MidiManager.OnDeviceOpenedListener()
Wywoływana jest metoda onDeviceOpened
tego listenera, która następnie wywołuje startReadingMidi()
, aby otworzyć port wyjściowy 0 na urządzeniu. Jest to funkcja JNI zdefiniowana w 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 przekształca urządzenie MIDI po stronie Java i jego porty w odniesienia 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ą sprawdzać port wyjściowy i reagować, gdy AMidiOutputPort_receive()
zwraca liczbę większą od zera.
W przypadku aplikacji o niskiej przepustowości, takich jak zakres MIDI, możesz przeprowadzać ankiety w niskiej priorytetowej nici w tle (z odpowiednimi opóźnieniami).
W przypadku aplikacji, które generują dźwięk i mają bardziej rygorystyczne wymagania dotyczące wydajności w czasie rzeczywistym, możesz przeprowadzać ankiety w głównym wywoływaniu zwrotnym generowania dźwięku (wywołanie zwrotneBufferQueue
w OpenSL ES, wywołanie zwrotne danych AudioStream w AAudio).
Ponieważ AMidiOutputPort_receive()
nie blokuje, 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 dźwięku (np. OpenSL ES lub AAudio) może dodać kod odbioru MIDI do wywołania obsługi 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łanie danych MIDI
Typowym przykładem aplikacji do zapisu MIDI jest kontroler lub sekwencer MIDI.
Konfigurowanie obiektu MidiDevice i jego portów wejściowych
Aplikacja zapisuje wychodzące dane MIDI do portów wejściowych urządzenia MIDI. Od strony Javy aplikacji musi określić urządzenie i porty MIDI, których ma używać.
Ten kod konfiguracji poniżej jest odmianą przykładu odbierania powyżej. Tworzy ona MidiManager
z usługi MIDI w Androidzie. Następnie otwiera pierwsząMidiDevice
, jaką znajdzie,
Wywołuje metodę startWritingMidi()
, by otworzyć pierwszy port wejściowy urządzenia. To jest
Wywołanie JNI zostało zdefiniowane w zadaniu AppMidiManager.cpp
. Funkcja jest objaśniona na
następny fragment.
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 aplikację AMidiInputPort_open()
, aby ją 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;
}
Wysyłanie danych MIDI
Ponieważ czas wychodzących danych MIDI jest zrozumiały i kontrolowany przez aplikacji, transmisję danych można przeprowadzić w głównym wątku aplikacji MIDI. Jednak ze względu na wydajność (jak w sekwencjomacie) generowanie i przesyłanie danych MIDI może odbywać się w osobnym wątku.
Aplikacje mogą wysyłać dane MIDI, gdy jest to wymagane. Pamiętaj, że AMidi blokuje, gdy zapisu danych.
Oto przykładowa metoda JNI, która odbiera bufor poleceń MIDI i pisze to:
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
Mimo że nie jest to funkcja AMidi, kod natywny może wymagać przekazania danych z powrotem do strony Java (np. w celu zaktualizowania interfejsu użytkownika). Aby to zrobić, musisz napisać kod po stronie Java i natywnej:
- Utwórz metodę wywołania zwrotnego po stronie Javy.
- Napisać funkcję JNI, która przechowuje informacje potrzebne do wywołania funkcji wywołania zwrotnego.
Gdy nadejdzie czas wywołania zwrotnego, Twój 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 funkcji MainActivity.onNativeMessageReceive()
. Java MainActivity
wywołuje 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 przesłanie danych 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 reference
- Zobacz pełną przykładową aplikację natywnej MIDI na githubie.