Native MIDI API

Die AMidi API ist ab Android NDK r20b verfügbar. Dadurch erhalten die App die Möglichkeit haben, MIDI-Daten mit C/C++code zu senden und zu empfangen.

Android MIDI-Apps verwenden in der Regel die midi API für die Kommunikation mit dem Android-MIDI-Dienst MIDI-Apps hängen hauptsächlich von der MidiManager zum Entdecken, Öffnen, und schließen Sie eine oder mehrere MidiDevice-Objekte und Daten an und von jedem Gerät über die MIDI-Eingang und Ausgabe-Ports:

Wenn du AMidi verwendest, wird die Adresse eines MidiDevice an den nativen Code übergeben. mit einem JNI-Aufruf erstellen. Anschließend erstellt AMidi einen Verweis auf ein AMidiDevice. die über die meisten Funktionen eines MidiDevice verfügt. Dein nativer Code verwendet AMidi-Funktionen, die kommunizieren direkt mit AMidiDevice. Die AMidiDevice ist direkt mit der MIDI-Dienst:

Mit AMidi-Aufrufen können Sie die C/C++ Audio-/Steuerlogik Ihrer App eng einbinden. mit MIDI-Übertragung. JNI-Aufrufe oder Callbacks an die Java-Seite der Anwendung zu erstellen. Ein in C-Code implementierter digitaler Synthesizer könnte beispielsweise Schlüsselereignisse direkt von einem AMidiDevice zu empfangen, anstatt auf eine JNI zu warten -Aufruf, um die Ereignisse von der Java-Seite nach unten zu senden. Oder eine algorithmische Komposition kann eine MIDI-Performance direkt an AMidiDevice senden, ohne an die Java-Seite übertragen werden, um die Schlüsselereignisse zu übertragen.

Obwohl AMidi die direkte Verbindung zu MIDI-Geräten verbessert, müssen Apps trotzdem MidiManager verwenden, um MidiDevice-Objekte zu finden und zu öffnen. AMidi kann von dort aus.

Manchmal müssen Sie Informationen von der UI-Ebene an die nativen Code. Wenn MIDI-Ereignisse beispielsweise als Reaktion auf Schaltflächen auf dem Bildschirm. Erstellen Sie dazu benutzerdefinierte JNI-Aufrufe an Ihre native Logik. Wenn Sie Daten zurücksenden müssen, um die UI zu aktualisieren, können Sie von der nativen Ebene aus wie gewohnt.

In diesem Dokument wird anhand von Beispielen gezeigt, wie Sie eine native AMidi-Code-App einrichten. sowohl beim Senden als auch beim Empfangen von MIDI-Befehlen. Ein vollständiges Praxisbeispiel finden Sie in der NativeMidi Beispiel-App.

AMidi verwenden

Für alle Apps, die AMidi nutzen, gelten dieselben Einrichtungs- und Schließschritte, unabhängig davon, ob sie MIDI senden oder empfangen oder beides.

AMidi starten

Auf der Java-Seite muss die App MIDI-Hardware verbunden, erstelle eine entsprechende MidiDevice, und an den nativen Code übergeben.

  1. Erkennen Sie MIDI-Hardware mit der Java-Klasse MidiManager.
  2. Ruft ein Java-MidiDevice-Objekt ab, das der MIDI-Hardware entspricht.
  3. Übergeben Sie das Java-MidiDevice mit JNI an nativen Code.

Hardware und Ports entdecken

Die Eingabe- und Ausgabeportobjekte gehören nicht zur Anwendung. Sie stellen Ports dar. auf dem MIDI-Gerät. Um MIDI-Daten an ein Gerät zu senden, öffnet die App ein MIDIInputPort und schreibt dann Daten in sie. Um hingegen Daten zu empfangen, muss eine App öffnet ein MIDIOutputPort. Um richtig zu funktionieren, muss die App prüfen, ob die Ports der Typ geöffnet ist. Die Geräte- und Porterkennung erfolgt auf Java-Seite.

Hier ist eine Methode, die jedes MIDI-Gerät erkennt und seine Ports. Sie gibt entweder eine Liste von Geräten mit Ausgabeports für den Empfang zurück. Daten oder eine Liste von Geräten mit Eingabeports zum Senden von Daten. Ein MIDI-Gerät kann sowohl Eingabe- als auch Ausgabeports haben.

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;
}

Um AMidi-Funktionen in Ihrem C/C++ Code zu verwenden, müssen Sie AMidi/AMidi.h und verknüpfen Sie sie mit der amidi-Bibliothek. Sie finden beides im Android NDK.

Auf der Java-Seite sollten ein oder mehrere MidiDevice-Objekte und Portnummern an die native Schicht über einen JNI-Aufruf. Die native Ebene sollte dann folgenden Schritten:

  1. Rufen Sie für jeden Java-MidiDevice einen AMidiDevice mit AMidiDevice_fromJava() ab.
  2. Du erhältst ein AMidiInputPort und/oder AMidiOutputPort aus dem AMidiDevice mit AMidiInputPort_open() und/oder AMidiOutputPort_open().
  3. Verwenden Sie die abgerufenen Ports zum Senden und/oder Empfangen von MIDI-Daten.

AMidi anhalten

Die Java-App sollte der nativen Ebene signalisieren, dass Ressourcen freigegeben werden sollen, wenn keine das MIDI-Gerät länger nutzen. Möglicherweise wurde das MIDI-Gerät nicht verbunden ist oder die App beendet wird.

Um MIDI-Ressourcen freizugeben, sollte Ihr Code die folgenden Aufgaben ausführen:

  1. Lesen und/oder Schreiben in MIDI-Ports beenden. Wenn du einen Messwert verwendet hast einen Thread zum Abrufen von Eingaben (siehe Abfrageschleife implementieren unten) den Thread beenden.
  2. Schließen Sie alle geöffneten AMidiInputPort- und/oder AMidiOutputPort-Objekte mit AMidiInputPort_close()- und/oder AMidiOutputPort_close()-Funktionen.
  3. Geben Sie AMidiDevice mit AMidiDevice_release() frei.

MIDI-Daten empfangen

Ein typisches Beispiel für eine MIDI-App, die MIDI empfängt, ist ein "virtueller Synthesizer" das MIDI-Leistungsdaten empfängt, um die Audiosynthese zu steuern.

Eingehende MIDI-Daten werden asynchron empfangen. Daher ist es am besten, MIDI-Daten in einem separaten Thread, der kontinuierlich einen oder MIDI-Ausgabeports abfragt. Dieses Dabei kann es sich um einen Hintergrund-Thread oder einen Audio-Thread handeln. Amidi-Schaf wird beim Lesen von einem Port nicht blockiert und kann daher im Innenbereich sicher verwendet werden. ein Audio-Callback sein.

MidiDevice und dessen Ausgabeports einrichten

Eine App liest eingehende MIDI-Daten von den Ausgabeports eines Geräts. Die Java-Seite Ihrer App müssen bestimmen, welches Gerät und welche Ports verwendet werden sollen.

Mit diesem Snippet wird der MidiManager aus dem MIDI-Dienst von Android und öffnet MidiDevice für das erste gefundene Gerät. Wenn das MidiDevice wenn ein Callback an eine Instanz von MidiManager.OnDeviceOpenedListener(). Die onDeviceOpened-Methode dieses Listener wird aufgerufen, der dann startReadingMidi() aufruft, um Ausgabeport 0 zu öffnen auf dem Gerät. Dieses ist eine in AppMidiManager.cpp definierte JNI-Funktion. Diese Funktion ist wie im nächsten Snippet beschrieben.

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);
    }
  }
}

Der native Code übersetzt das Java-seitige MIDI-Gerät und seine Ports in Referenzen, die von AMidi-Funktionen verwendet werden.

Dies ist die JNI-Funktion, die durch Aufrufen eines AMidiDevice AMidiDevice_fromJava() und ruft dann AMidiOutputPort_open() auf, um einen Ausgabeport am Gerät:

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...

}

Abfrageschleife implementieren

Apps, die MIDI-Daten empfangen, müssen den Ausgabeport abfragen und antworten, wenn AMidiOutputPort_receive() gibt eine Zahl größer als null zurück.

Für Apps mit geringer Bandbreite, wie z. B. einen MIDI-Bereich, können Sie Abfragen mit niedriger Priorität durchführen. Hintergrund-Thread (mit entsprechenden Schlafphasen)

Für Apps, die Audioinhalte generieren und eine strengere Echtzeit-Leistung haben Anforderungen erfüllt, könnt ihr den Haupt-Callback zur Audiogenerierung (die BufferQueue-Callback für OpenSL ES, der AudioStream-Daten-Callback in AAudio. Da AMidiOutputPort_receive() nicht blockiert ist, gibt es sehr wenig auf die Leistung auswirken.

Die Funktion readThreadRoutine(), die von der Funktion startReadingMidi() aufgerufen wird könnte wie folgt aussehen:

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;
        }
  }
}

Eine App, die eine native Audio-API verwendet (z. B. OpenSL ES oder AAudio) kann wie folgt MIDI-Empfangscode zum Audiogenerierungs-Callback hinzufügen:

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
    // ...
}

Das folgende Diagramm veranschaulicht den Ablauf einer MIDI-Lese-App:

MIDI-Daten senden

Ein typisches Beispiel für eine MIDI-Schreib-App ist ein MIDI-Controller oder Sequenzer.

MidiDevice und dessen Eingabeports einrichten

Eine App schreibt ausgehende MIDI-Daten an die Eingabeports eines MIDI-Geräts. Die Java-Seite deiner App muss bestimmen, welches MIDI-Gerät und welche Ports verwendet werden sollen.

Der Einrichtungscode unten ist eine Variation des oben empfangenen Beispiels. Die MidiManager wird erstellt. aus dem MIDI-Dienst von Android. Dann wird die erste gefundeneMidiDevice ruft startWritingMidi() auf, um den ersten Eingabeport auf dem Gerät zu öffnen. Dies ist ein JNI-Aufruf definiert in AppMidiManager.cpp. Die Funktion wird im für das nächste Snippet.

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);
    }
  }
}

Dies ist die JNI-Funktion, die durch Aufrufen eines AMidiDevice AMidiDevice_fromJava() und ruft dann AMidiInputPort_open() auf, um Eingangsport am Gerät:

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;
}

MIDI-Daten senden

Da das Timing der ausgehenden MIDI-Daten von der der App selbst kann die Datenübertragung im Hauptthread der MIDI-App erfolgen. Aus Leistungsgründen (z. B. bei Sequencer) sind jedoch die Generierung und Die Übertragung von MIDI kann in einem separaten Thread erfolgen.

Apps können MIDI-Daten senden, wann immer dies erforderlich ist. Beachten Sie, dass AMidi blockiert, wenn das Schreiben von Daten.

Hier sehen Sie ein Beispiel für eine JNI-Methode, die einen Zwischenspeicher für MIDI-Befehle empfängt schreibt es auf:

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);
}

Das folgende Diagramm veranschaulicht den Ablauf einer MIDI-Schreib-App:

Rückrufe

Auch wenn es sich nicht ausschließlich um eine AMidi-Funktion handelt, muss Ihr nativer Code möglicherweise Daten auf die Java-Seite zurück, um z. B. die Benutzeroberfläche zu aktualisieren. Dazu müssen Sie Schreiben Sie Code auf der Java-Seite und auf der nativen Schicht:

  • Erstellen Sie eine Callback-Methode auf der Java-Seite.
  • Schreiben Sie eine JNI-Funktion, die die zum Aufrufen des Callbacks erforderlichen Informationen speichert.

Wenn es Zeit für einen Rückruf ist, kann Ihr nativer Code

Hier ist die Java-seitige Callback-Methode 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);
            }
        });
}

Hier ist der C-Code für die JNI-Funktion, die den MainActivity.onNativeMessageReceive() Java-MainActivity-Aufrufe initNative() beim Start:

Hauptaktivität.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");
}

Wenn es an der Zeit ist, Daten an Java zurückzusenden, ruft der native Code den Callback ab. zeigt den Callback an und erstellt den Callback:

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);
}

Weitere Informationen