Die AMidi API ist im Android NDK r20b und höher verfügbar. App-Entwickler können damit MIDI-Daten mit C/C++-Code senden und empfangen.
Android-MIDI-Apps verwenden in der Regel 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-Ports des Geräts an jedes Gerät zu senden und von jedem Gerät zu empfangen:
Wenn Sie AMidi verwenden, übergeben Sie die Adresse einer MidiDevice
mit einem JNI-Aufruf an die native Codeebene. AMidi erstellt dann eine Referenz zu einem 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-/Steuerungslogik Ihrer App eng in die MIDI-Übertragung einbinden. Es sind weniger JNI-Aufrufe oder Callbacks zur Java-Seite Ihrer App erforderlich. Ein in C-Code implementierter digitaler Synthesizer könnte beispielsweise Tastaturereignisse direkt von einem AMidiDevice
empfangen, anstatt auf einen JNI-Aufruf zu warten, um die Ereignisse von der Java-Seite zu senden. Oder ein algorithmischer Kompositionsprozess könnte eine MIDI-Performance direkt an ein AMidiDevice
senden, ohne die wichtigsten Ereignisse über die Java-Seite zu übertragen.
Obwohl AMidi die direkte Verbindung zu MIDI-Geräten verbessert, müssen Apps weiterhin MidiManager
verwenden, um MidiDevice
-Objekte zu erkennen und zu öffnen. AMidi kann dann weitermachen.
Manchmal müssen Sie Informationen von der UI-Ebene an den nativen Code übergeben. Das ist beispielsweise der Fall, wenn MIDI-Ereignisse als Reaktion auf Schaltflächen auf dem Bildschirm gesendet werden. Erstellen Sie dazu benutzerdefinierte JNI-Aufrufe für Ihre native Logik. Wenn Sie Daten zurücksenden müssen, um die Benutzeroberfläche zu aktualisieren, können Sie wie gewohnt aus der nativen Ebene zurückrufen.
In diesem Dokument wird beschrieben, wie Sie eine AMidi-App mit nativem Code einrichten. Es enthält Beispiele für das Senden und Empfangen von MIDI-Befehlen. Ein vollständiges Praxisbeispiel finden Sie in der Beispiel-App NativeMidi.
AMidi verwenden
Alle Apps, die AMidi verwenden, haben dieselben Einrichtungs- und Schließungsschritte, unabhängig davon, ob sie MIDI senden, empfangen oder beides.
AMidi starten
Auf der Java-Seite muss die App ein angeschlossenes MIDI-Gerät erkennen, ein entsprechendes MidiDevice
erstellen und es an den nativen Code übergeben.
- MIDI-Hardware mit der Java-Klasse
MidiManager
erkennen - Rufen Sie ein Java-
MidiDevice
-Objekt ab, das der MIDI-Hardware entspricht. - Übergeben Sie die Java-
MidiDevice
mit JNI an den nativen Code.
Hardware und Anschlüsse
Die Ein- und Ausgabeportobjekte gehören nicht zur App. Sie stellen Ports auf dem MIDI-Gerät dar. Wenn MIDI-Daten an ein Gerät gesendet werden sollen, öffnet eine App einen MIDIInputPort
und schreibt dann Daten hinein. Umgekehrt öffnet eine App eine MIDIOutputPort
, um Daten zu empfangen. Damit die App richtig funktioniert, muss sie sicher sein, dass die geöffneten Ports den richtigen Typ haben. Die Geräte- und Port-Erkennung erfolgt auf der Java-Seite.
Hier ist eine Methode, die jedes MIDI-Gerät erkennt und sich seine Ports ansieht. Es wird entweder eine Liste von Geräten mit Ausgabeports zum Empfangen von Daten oder eine Liste von Geräten mit Eingabeports zum Senden von Daten zurückgegeben. 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; }
Wenn Sie AMidi-Funktionen in Ihrem C/C++-Code verwenden möchten, müssen Sie AMidi/AMidi.h
einbinden und die amidi
-Bibliothek verknüpfen. Beide sind im Android NDK verfügbar.
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 die folgenden Schritte ausführen:
- Rufen Sie für jedes Java-
MidiDevice
einAMidiDevice
mitAMidiDevice_fromJava()
ab. AMidiInputPort
und/oderAMidiOutputPort
von derAMidiDevice
mitAMidiInputPort_open()
und/oderAMidiOutputPort_open()
abrufen.- Verwenden Sie die erhaltenen Ports, um MIDI-Daten zu senden und/oder zu empfangen.
AMidi beenden
Die Java-App sollte der nativen Ebene signalisieren, Ressourcen freizugeben, wenn sie das MIDI-Gerät nicht mehr verwendet. Das kann daran liegen, dass das MIDI-Gerät getrennt wurde oder die App beendet wird.
Damit MIDI-Ressourcen freigegeben werden, muss Ihr Code die folgenden Aufgaben ausführen:
- Das Lesen und/oder Schreiben von MIDI-Ports beenden. Wenn Sie einen Lesethread verwendet haben, um Eingaben abzufragen (siehe Polling-Schleife 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 mehrere MIDI-Ausgabeports abfragt. Das kann ein Hintergrund- oder Audio-Thread sein. AMidi blockiert nicht, wenn Daten von einem Port gelesen werden, und kann daher sicher in einem Audio-Callback verwendet werden.
MidiDevice und zugehörige Ausgänge einrichten
Eine App liest eingehende MIDI-Daten von den Ausgängen eines Geräts. Die Java-Seite Ihrer App muss festlegen, welches Gerät und welche Ports verwendet werden sollen.
Mit diesem Snippet wird die MidiManager
aus dem MIDI-Dienst von Android erstellt und eine 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. Die Methode onDeviceOpened
dieses Listeners wird aufgerufen, die dann startReadingMidi()
aufruft, um den Ausgabesender 0 auf dem Gerät zu öffnen. Dies ist eine JNI-Funktion, die in AppMidiManager.cpp
definiert ist. 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 MIDI-Gerät auf Java-Seite und seine Ports in Referenzen, die von AMidi-Funktionen verwendet werden.
Hier ist die JNI-Funktion, die ein AMidiDevice
durch Aufrufen von AMidiDevice_fromJava()
erstellt 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...
}
Polling-Schleife implementieren
Apps, die MIDI-Daten empfangen, müssen den Ausgabeport abfragen und reagieren, wenn AMidiOutputPort_receive()
eine Zahl größer als null zurückgibt.
Bei Apps mit geringer Bandbreite, z. B. einem MIDI-Scope, können Sie in einem Hintergrundthread mit niedriger Priorität (mit entsprechenden Pausen) abfragen.
Bei Apps, die Audio generieren und strengere Echtzeitanforderungen haben, können Sie im Haupt-Audio-Generierungs-Callback (dem BufferQueue
-Callback für OpenSL ES, dem AudioStream-Daten-Callback in AAudio) abfragen.
Da AMidiOutputPort_receive()
nicht blockierend ist, sind die Auswirkungen auf die Leistung sehr gering.
Die Funktion readThreadRoutine()
, die von der Funktion startReadingMidi()
oben 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;
}
}
}
In einer App, die eine native Audio-API wie OpenSL ES oder AAudio verwendet, kann dem Audio-Generierungs-Callback so MIDI-Empfangscode hinzugefügt werden:
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 zugehörige Eingabeports einrichten
Eine App schreibt ausgehende MIDI-Daten in die Eingabeports eines MIDI-Geräts. Die Java-Seite Ihrer App muss festlegen, welches MIDI-Gerät und welche Ports verwendet werden sollen.
Der folgende Einrichtungscode ist eine Variante des obigen Empfangsbeispiels. Sie erstellt die MidiManager
aus dem MIDI-Dienst von Android. Anschließend wird der ersteMidiDevice
gefunden und startWritingMidi()
aufgerufen, um den ersten Eingabeport auf dem Gerät zu öffnen. Dies ist ein JNI-Aufruf, der in AppMidiManager.cpp
definiert ist. 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()
erstellt 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 gut nachvollziehbar ist und von der App selbst gesteuert wird, kann die Datenübertragung im Hauptthread der MIDI-App erfolgen. Aus Leistungsgründen (wie bei einem Sequenzer) kann die Generierung und Übertragung von MIDI jedoch in einem separaten Thread erfolgen.
Apps können bei Bedarf MIDI-Daten senden. Beachten Sie, dass AMidi beim Schreiben von Daten blockiert wird.
Hier ist ein Beispiel für eine JNI-Methode, die einen Puffer mit MIDI-Befehlen empfängt und ihn ausgibt:
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:
Callbacks
Obwohl dies nicht unbedingt eine AMidi-Funktion ist, müssen Sie möglicherweise Daten von Ihrem nativen Code an die Java-Seite zurückgeben (z. B. um die Benutzeroberfläche zu aktualisieren). Dazu müssen Sie Code auf der Java-Seite und in der nativen Ebene schreiben:
- Erstellen Sie eine Callback-Methode auf der Java-Seite.
- Schreiben Sie eine JNI-Funktion, die die Informationen speichert, die zum Aufrufen des Callbacks erforderlich sind.
Wenn es Zeit für den Callback ist, kann Ihr nativer Code
Hier ist die Callback-Methode auf Java-Seite, 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 einen Callback für MainActivity.onNativeMessageReceive()
einrichtet. Java ruft MainActivity
initNative()
beim Start auf:
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");
}
Wenn Daten an Java zurückgesendet werden müssen, ruft der native Code die Callback-Pointer 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
- Die vollständige Native MIDI-Beispielanwendung finden Sie auf GitHub.