Natywny interfejs MIDI API

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.

  1. Poznaj sprzęt MIDI na zajęciach z Java MidiManager.
  2. Uzyskaj obiekt Java MidiDevice odpowiadający sprzętowi MIDI.
  3. 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 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. Może to być spowodowane odłączeniem urządzenia MIDI lub zamknięciem aplikacji.

Aby zwolnić zasoby MIDI, kod powinien wykonać te czynności:

  1. 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.
  2. Zamknij wszystkie otwarte obiekty AMidiInputPort lub AMidiOutputPort za pomocą funkcji AMidiInputPort_close() lub AMidiOutputPort_close().
  3. Zwolnij AMidiDeviceAMidiDevice_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);
    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 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), &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 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), &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ł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);
    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ł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