Die AMidi API ist in Android NDK r20b und höher verfügbar. Es ermöglicht App-Entwicklern, MIDI-Daten mit C/C++-Code zu senden und zu 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 MidiManager
angewiesen, um ein oder mehrere MidiDevice
-Objekte zu finden, zu öffnen und zu schließen und Daten über die MIDI-Eingabe- und Ausgabe-Ports des Geräts an und von jedem Gerät zu übergeben:
Wenn Sie AMidi verwenden, übergeben Sie die Adresse einer MidiDevice
mit einem JNI-Aufruf an die native Codeebene. 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-/Steuerungslogik Ihrer App eng mit der MIDI-Übertragung verknüpfen. 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 ein algorithmischer Kompositionsprozess könnte eine MIDI-Wiedergabe direkt an eine AMidiDevice
senden, ohne zur Java-Seite zurückzukehren, 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 dann den Rest übernehmen.
Manchmal müssen Sie Informationen von der UI-Ebene an den nativen Code weitergeben. 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 funktionierendes Beispiel finden Sie in der Beispiel-App NativeMidi.
AMidi verwenden
Für alle Apps, die AMidi verwenden, 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.
- MIDI-Hardware mit der Java-Klasse
MidiManager
kennenlernen - Ruft ein Java-
MidiDevice
-Objekt ab, das der MIDI-Hardware entspricht. - Übergeben Sie das Java-
MidiDevice
mit JNI an den nativen Code.
Hardware und Ports entdecken
Die Eingabe- und Ausgabeportobjekte gehören nicht zur App. Sie stellen Ports auf dem MIDI-Gerät dar. 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
. Damit die App ordnungsgemäß funktioniert, muss sie sicher sein, dass die geöffneten Ports den richtigen Typ haben. Die Geräte- und Porterkennung erfolgt auf Java-Seite.
Hier ist eine Methode, die jedes MIDI-Gerät erkennt und seine Ports. Es wird entweder eine Liste von Geräten mit Ausgabeports für den Empfang von Daten oder eine Liste von Geräten mit Eingabeports für das 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 eine Verknüpfung mit der amidi
-Bibliothek herstellen. Sie finden sie im Android NDK.
Die Java-Seite sollte über einen JNI-Aufruf ein oder mehrere MidiDevice
-Objekte und Portnummern an die native Schicht übergeben. Die native Schicht sollte dann die folgenden Schritte ausführen:
- Rufen Sie für jeden Java-
MidiDevice
einenAMidiDevice
mitAMidiDevice_fromJava()
ab. - Rufen Sie eine
AMidiInputPort
und/oderAMidiOutputPort
vomAMidiDevice
mitAMidiInputPort_open()
und/oderAMidiOutputPort_open()
ab. - Verwenden Sie die erhaltenen Ports, um MIDI-Daten zu senden und/oder zu empfangen.
Stop AMidi
Die Java-App sollte der nativen Schicht signalisieren, Ressourcen freizugeben, wenn sie das MIDI-Gerät nicht mehr verwendet. Möglicherweise wurde das MIDI-Gerät nicht verbunden ist oder die App beendet wird.
Damit MIDI-Ressourcen freigegeben werden, sollte Ihr Code folgende Aufgaben ausführen:
- Beenden Sie das Lesen und/oder Schreiben an MIDI-Ports. Wenn Sie einen Lese-Thread verwendet haben, um nach Eingaben zu suchen (siehe unten Polling-Schleife implementieren), beenden Sie den Thread.
- Schließen Sie alle geöffneten
AMidiInputPort
- und/oderAMidiOutputPort
-Objekte mitAMidiInputPort_close()
- und/oderAMidiOutputPort_close()
-Funktionen. - Gib die
AMidiDevice
mitAMidiDevice_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 in einem separaten Thread zu lesen, der kontinuierlich einen oder mehrere MIDI-Ausgabeports abfragt. Dieses Dabei kann es sich um einen Hintergrund-Thread oder einen Audio-Thread handeln. AMidi blockiert nicht beim Lesen von einem Anschluss und kann daher in einem Audio-Callback sicher verwendet werden.
MidiDevice und seine Ausgabeports einrichten
Eine App liest eingehende MIDI-Daten von den Ausgabeports eines Geräts. Die Java-Seite Ihrer App muss festlegen, 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); 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 eine AMidiDevice
erstellt, indem AMidiDevice_fromJava()
aufgerufen wird, 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
Apps, die MIDI-Daten empfangen, müssen den Ausgabeport abfragen und antworten, wenn
AMidiOutputPort_receive()
gibt eine Zahl größer als null zurück.
Bei Anwendungen mit geringer Bandbreite, z. B. einem MIDI-Scope, können Sie die Abfrage in einem Hintergrund-Thread mit niedriger Priorität (mit entsprechenden Zeiträumen) durchführen.
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()
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;
}
}
}
Eine App, die eine native Audio-API wie OpenSL ES oder AAudio verwendet, kann dem Callback für die Audiogenerierung MIDI-Empfangscode hinzufügen. So gehts:
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 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 Beispiels für den Empfänger. Er erstellt die MidiManager
aus dem MIDI-Dienst von Android. Anschließend öffnet er die erste gefundene MidiDevice
und ruft startWritingMidi()
auf, 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
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); 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); } } }
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 gut verstanden und von der App selbst gesteuert wird, 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 jederzeit MIDI-Daten senden. Beachte, dass AMidi beim Schreiben von Daten blockiert.
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:
Callbacks
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 Code auf der Java-Seite und der nativen Schicht schreiben:
- Erstellen Sie eine Callback-Methode auf Java-Seite.
- Schreiben Sie eine JNI-Funktion, in der die Informationen zum Aufrufen des Callbacks gespeichert werden.
Wenn es Zeit für den Callback 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
ruft beim Start initNative()
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 es an der Zeit ist, Daten zurück an Java zu senden, 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);
}
Weitere Informationen
- AMidi-Referenz
- Die vollständige Native MIDI-Beispiel-App finden Sie auf GitHub.