Die AMidi API ist ab Android NDK r20b verfügbar. Es ermöglicht App-Entwicklern, MIDI-Daten mit C/C++ Code zu senden und zu empfangen.
Android-MIDI-Apps verwenden normalerweise die midi
API, um mit dem Android-MIDI-Dienst zu kommunizieren. MIDI-Apps sind in erster Linie auf die MidiManager
angewiesen, um ein oder mehrere MidiDevice
-Objekte zu erkennen, zu öffnen und zu schließen und Daten über die MIDI-Eingabe- und -Ausgabe des Geräts an und von jedem Gerät zu übertragen:
Wenn Sie AMidi verwenden, übergeben Sie die Adresse eines MidiDevice
mit einem JNI-Aufruf an die native Codeebene. Von dort aus erstellt AMidi einen Verweis auf ein AMidiDevice
, das die meisten Funktionen eines MidiDevice
hat. Ihr nativer Code verwendet AMidi-Funktionen, die direkt mit einem AMidiDevice
kommunizieren. Das AMidiDevice
stellt eine direkte Verbindung zum MIDI-Dienst her:
Mit AMidi-Aufrufen können Sie die C/C++ Audio-/Steuerlogik Ihrer App eng in die MIDI-Übertragung einbinden. JNI-Aufrufe oder Rückrufe auf die Java-Seite Ihrer App sind weniger erforderlich. Beispielsweise könnte ein in C-Code implementierter digitaler Synthesizer Schlüsselereignisse direkt von einem AMidiDevice
empfangen, anstatt auf einen JNI-Aufruf zu warten, um die Ereignisse von der Java-Seite nach unten zu senden. Oder ein algorithmischer Erstellungsprozess könnte eine MIDI-Leistung direkt an eine AMidiDevice
senden, ohne die Java-Seite zur Übertragung der Schlüsselereignisse aufzurufen.
Obwohl AMidi die direkte Verbindung zu MIDI-Geräten verbessert, müssen Apps weiterhin die MidiManager
verwenden, um MidiDevice
-Objekte zu erkennen und zu öffnen. AMidi kann
sie von dort übernehmen.
Manchmal müssen Informationen von der UI-Ebene an den nativen Code übergeben werden. Dies ist beispielsweise der Fall, wenn MIDI-Ereignisse als Reaktion auf Schaltflächen auf dem Bildschirm gesendet werden. 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 wie gewohnt von der nativen Ebene aus einen Callback durchführen.
In diesem Dokument wird anhand von Beispielen sowohl das Senden als auch der Empfang von MIDI-Befehlen beschrieben, wie Sie eine native AMidi-Code-App einrichten. Ein vollständiges funktionierendes Beispiel finden Sie in der Beispiel-App NativeMidi.
AMidi verwenden
Alle Apps, die AMidi verwenden, haben dieselben Einrichtungs- und Schließschritte, unabhängig davon, ob sie MIDI senden oder empfangen, oder beides.
AMidi starten
Auf der Java-Seite muss die Anwendung ein angehängtes MIDI-Hardwareelement erkennen, eine entsprechende MidiDevice
erstellen und an den nativen Code übergeben.
- Erkennen Sie MIDI-Hardware mit der Java-Klasse
MidiManager
. - Ruft ein Java-
MidiDevice
-Objekt ab, das der MIDI-Hardware entspricht. - Übergeben Sie das Java-
MidiDevice
mit JNI an nativen Code.
Hardware und Ports entdecken
Die Eingabe- und Ausgabeportobjekte gehören nicht zur App. Sie stellen die Ports auf dem Midi-Gerät dar. Zum Senden von MIDI-Daten an ein Gerät öffnet eine App ein MIDIInputPort
und schreibt dann Daten darauf. Umgekehrt öffnet eine App zum Empfangen von Daten ein MIDIOutputPort
. Damit die Anwendung richtig funktioniert, muss sie darauf achten, dass die geöffneten Ports vom richtigen Typ sind. Die Geräte- und Porterkennung erfolgt auf Java-Seite.
Hier sehen Sie eine Methode, die jedes MIDI-Gerät erkennt und sich seine Ports ansieht. Sie gibt entweder eine Liste von Geräten mit Ausgabeports zum Empfang von Daten oder eine Liste von Geräten mit Eingabeports zum Senden von Daten zurück. 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 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; }
Zur Verwendung von AMidi-Funktionen in Ihrem C/C++ Code müssen Sie AMidi/AMidi.h
einfügen und eine Verknüpfung mit der amidi
-Bibliothek herstellen. Beide findest du im Android NDK.
Auf der Java-Seite sollten ein oder mehrere MidiDevice
-Objekte und Portnummern über einen JNI-Aufruf an die native Ebene übergeben werden. Die native Ebene sollte dann folgende Schritte ausführen:
- Rufen Sie für jeden Java-
MidiDevice
einenAMidiDevice
mitAMidiDevice_fromJava()
ab. - Du erhältst ein
AMidiInputPort
und/oderAMidiOutputPort
aus demAMidiDevice
mitAMidiInputPort_open()
und/oderAMidiOutputPort_open()
. - 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 das MIDI-Gerät nicht mehr verwendet wird. Das könnte daran liegen, dass die Verbindung zum MIDI-Gerät getrennt wurde oder die App beendet wird.
Um MIDI-Ressourcen freizugeben, sollte Ihr Code die folgenden Aufgaben ausführen:
- Lesen und/oder Schreiben in MIDI-Ports beenden. Wenn Sie einen Lesethread zum Abfragen von Eingaben verwendet haben (siehe Abfrageschleife implementieren unten), beenden Sie den Thread.
- Schließen Sie alle geöffneten
AMidiInputPort
- und/oderAMidiOutputPort
-Objekte mit den FunktionenAMidiInputPort_close()
und/oderAMidiOutputPort_close()
. - Geben Sie
AMidiDevice
mitAMidiDevice_release()
frei.
MIDI-Daten empfangen
Ein typisches Beispiel für eine MIDI-App, die MIDI empfängt, ist ein „virtueller Synthesizer“, der MIDI-Leistungsdaten empfängt, um die Audiosynthese zu steuern.
Eingehende MIDI-Daten werden asynchron empfangen. Daher ist es am besten, MIDI in einem separaten Thread zu lesen, der kontinuierlich einen oder MIDI-Ausgabeports abfragt. Dies kann ein Hintergrundthread oder ein Audiothread sein. AMidi blockiert beim Lesen von einem Port nicht und kann daher ohne Bedenken innerhalb eines Audio-Callbacks verwendet werden.
MidiDevice und dessen Ausgabeports einrichten
Eine App liest eingehende MIDI-Daten von den Ausgabeports eines Geräts. Die Java-Seite Ihrer Anwendung muss bestimmen, welches Gerät und welche Ports verwendet werden sollen.
Mit diesem Snippet wird die MidiManager
aus dem MIDI-Dienst von Android erstellt und ein MidiDevice
für das erste gefundene Gerät geöffnet. Wenn MidiDevice
geöffnet wurde, wird ein Callback an eine Instanz von MidiManager.OnDeviceOpenedListener()
empfangen. Es wird die Methode onDeviceOpened
dieses Listeners aufgerufen, die dann startReadingMidi()
aufruft, um den Ausgabeport 0 auf dem Gerät zu öffnen. Dies ist eine in AppMidiManager.cpp
definierte JNI-Funktion. Diese Funktion wird im nächsten Snippet erläutert.
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); } } }
Der native Code übersetzt das Java-seitige MIDI-Gerät und seine Ports in Referenzen, die von AMidi-Funktionen verwendet werden.
Hier ist die JNI-Funktion, die ein AMidiDevice
durch Aufrufen von AMidiDevice_fromJava()
und dann AMidiOutputPort_open()
aufruft, um einen Ausgabeport auf dem Gerät zu öffnen:
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
Anwendungen, die MIDI-Daten empfangen, müssen den Ausgabeport abfragen und antworten, wenn AMidiOutputPort_receive()
eine Zahl größer null zurückgibt.
Für Anwendungen mit niedriger Bandbreite, z. B. einem MIDI-Bereich, können Sie einen Hintergrundthread mit niedriger Priorität (mit entsprechenden Ruhezeiten) abfragen.
Bei Apps, die Audiodaten generieren und für die strengere Anforderungen an die Leistung in Echtzeit gelten, können Sie den Haupt-Callback zur Audiogenerierung (den BufferQueue
-Callback für OpenSL ES, der AudioStream-Daten-Callback in AAudio) abfragen.
Da AMidiOutputPort_receive()
nicht blockiert, wirkt sich das nur geringfügig auf die Leistung aus.
Die readThreadRoutine()
-Funktion, die über die obige startReadingMidi()
-Funktion aufgerufen wird, könnte so 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), ×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;
}
}
}
Eine App, die eine native Audio-API verwendet (z. B. OpenSL ES oder AAudio), kann 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), ×tamp);
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 Ihrer App muss bestimmen, welches MIDI-Gerät und welche Ports verwendet werden sollen.
Der Einrichtungscode unten ist eine Variation des oben empfangenen Beispiels. Sie erstellt die MidiManager
aus dem MIDI-Dienst von Android. Anschließend wird der erste gefundene MidiDevice
geöffnet und startWritingMidi()
aufgerufen, um den ersten Eingabeport auf dem Gerät zu öffnen. Dies ist ein in AppMidiManager.cpp
definierter JNI-Aufruf. Die Funktion wird im nächsten Snippet erläutert.
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); } } }
Hier ist die JNI-Funktion, die ein AMidiDevice
durch Aufrufen von AMidiDevice_fromJava()
und dann AMidiInputPort_open()
aufruft, um einen Eingabeport auf dem Gerät zu öffnen:
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 App selbst verstanden und gesteuert wird, kann die Datenübertragung im Hauptthread der MIDI-App erfolgen. Aus Leistungsgründen (wie bei einem Sequencer) kann die Generierung und Übertragung von MIDI jedoch in einem separaten Thread erfolgen.
Apps können MIDI-Daten senden, wann immer dies erforderlich ist. Beachten Sie, dass AMidi beim Schreiben von Daten blockiert.
Hier ist ein Beispiel für eine JNI-Methode, die einen Puffer mit MIDI-Befehlen empfängt und ausschreibt:
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
Obwohl es sich nicht ausschließlich um eine AMidi-Funktion handelt, muss Ihr nativer Code möglicherweise Daten zurück an die Java-Seite übergeben, um beispielsweise die UI zu aktualisieren. Dazu müssen Sie Code auf der Java-Seite und auf der nativen Ebene schreiben:
- 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 Callback von MainActivity.onNativeMessageReceive()
einrichtet. Java MainActivity
ruft initNative()
beim Start auf:
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 die Callback-Zeiger ab 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);
}
Zusätzliche Ressourcen
- AMidi-Referenz
- Sehen Sie sich die vollständige Native MIDI-Beispiel-App auf GitHub an.