Interfejs API AMidi jest dostępne na Androidzie NDK r20b i nowszych. Zapewnia aplikację, wysyłanie i odbieranie danych MIDI za pomocą kodu w C/C++.
Aplikacje MIDI na Androida zwykle używają
interfejsu API midi
do komunikacji z
Usługa Android MIDI. Aplikacje MIDI zależą przede wszystkim
MidiManager
, aby odkrywać, otwierać
i zamknij
MidiDevice
obiektów oraz
przekazywania danych do i z każdego urządzenia za pomocą
Wejście MIDI
Porty wyjściowe:
Korzystając z AMidi, przekazujesz adres kodu MidiDevice
do kodu natywnego
z wywołaniem JNI. Następnie Amidi tworzy odwołanie do pliku AMidiDevice
który 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 ściśle zintegrować logikę dźwięku/sterowania w aplikacji w C/C++
z transmisją MIDI. Wywołania JNI i wywołania zwrotne do
Strona aplikacji w języku Java. Na przykład syntezator cyfrowy zaimplementowany w kodzie C mógłby
otrzymywać kluczowe zdarzenia bezpośrednio z AMidiDevice
, zamiast czekać na JNI
aby wysyłać zdarzenia od strony Javy. Albo algorytm
może wysłać dane o wydajności MIDI bezpośrednio do AMidiDevice
bez wywoływania
po stronie Javy, by przesyłać kluczowe zdarzenia.
Choć AMidi ulepsza bezpośrednie połączenie z urządzeniami MIDI, aplikacje muszą
użyj MidiManager
do wykrywania i otwierania obiektów MidiDevice
. AMidi może
a potem jej ściągnąć.
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 wysłać dane, aby zaktualizować interfejs, możesz wywołać funkcję zwrotną z warstwy natywnej jak zwykle.
Ten dokument pokazuje, jak skonfigurować aplikację do kodu natywnego AMidi z przykładami wysyłania i odbierania poleceń MIDI. Pełny działający przykład znajdziesz NativeMidi przykładową aplikację.
Użyj AMidi
Wszystkie aplikacje korzystające z AMidi mają te same kroki konfiguracji i zamknięcia. wysyłanie i odbieranie MIDI.
Uruchom AMidi
W Javie aplikacja musi wykryć
podłączony sprzęt MIDI, utwórz odpowiedni MidiDevice
,
i przekazać ją 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 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 niej dane. Aby uzyskać dane, aplikacja musi:
otwiera plik MIDIOutputPort
. Aby aplikacja działała prawidłowo, musi mieć odpowiednie porty
są właściwego 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. Przyczyną może być to, że urządzenie MIDI rozłączono lub dlatego, że aplikacja się zamyka.
Aby zwolnić zasoby MIDI, kod powinien wykonać te czynności:
- Zatrzymaj odczytywanie i/lub zapisywanie w portach 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ąAMidiInputPort_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 odpytuje 1 lub port wyjściowy MIDI. Ten może 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()
Metoda onDeviceOpened
tego
jest wywoływany detektor, który następnie wywołuje startReadingMidi()
, aby otworzyć port wyjściowy 0
na urządzeniu. Ten
to funkcja JNI zdefiniowana w AppMidiManager.cpp
. Ta funkcja jest
co wyjaśniamy 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łań używanych przez funkcje AMidi.
Oto funkcja JNI, która tworzy AMidiDevice
przez wywołanie
AMidiDevice_fromJava()
, a następnie wywołuje aplikację AMidiOutputPort_open()
, aby ją otworzyć
portu wyjściowego 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 przeprowadzać sondowanie w środowisku o niskim priorytecie wątek w tle (z odpowiednimi ustawieniami snu).
W przypadku aplikacji, które generują dźwięk i mają bardziej rygorystyczną wydajność w czasie rzeczywistym
można przeprowadzać ankiety w głównym wywołaniu zwrotnym generowania dźwięku (
BufferQueue
wywołanie zwrotne dla OpenSL ES, wywołania zwrotnego danych AudioStream w AAudio).
Element AMidiOutputPort_receive()
nie blokuje elementów, więc jest bardzo mało danych
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 używająca natywnego interfejsu API audio (np. OpenSL ES) 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. Od strony Javy aplikacji musi określić urządzenie i porty MIDI, których ma używać.
Kod konfiguracji widoczny poniżej jest odmianą kodu odebranego powyżej. Tworzy: MidiManager
z usługi MIDI na 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łaj dane 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 przypadku sekwencera) generowanie transmisję MIDI można przeprowadzić 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
Chociaż nie jest to wyłącznie funkcja AMidi, Twój kod natywny może wymagać po stronie Javy (aby na przykład zaktualizować interfejs użytkownika). Aby to zrobić, napisz 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()
Wywołania MainActivity
Javy
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, kod natywny pobierze wywołanie zwrotne. 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.