Natywny interfejs API MIDI

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.

  1. Odkryj sprzęt MIDI z klasą MidiManager w języku Java.
  2. Uzyskaj obiekt Java MidiDevice odpowiadający sprzętowi MIDI.
  3. 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 List getMidiDevices(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:

  1. Dla każdej biblioteki Java MidiDevice uzyskaj AMidiDevice za pomocą AMidiDevice_fromJava().
  2. Uzyskaj AMidiInputPort lub AMidiOutputPort z: AMidiDevice z AMidiInputPort_open() lub AMidiOutputPort_open().
  3. 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:

  1. 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.
  2. Zamknij wszystkie otwarte obiekty AMidiInputPort lub AMidiOutputPort za pomocą AMidiInputPort_close() lub AMidiOutputPort_close().
  3. 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);
    List midiDevices = 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), &timestamp);
        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), &timestamp);
    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);
    List midiDevices = 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