A API AMidi está disponível no Android NDK r20b e versões mais recentes. Ela permite que os desenvolvedores de aplicativos enviem e recebam dados MIDI com código C/C++.
Os apps Android MIDI costumam usar a
API midi
para se comunicarem com o
serviço Android MIDI. Esses apps dependem principalmente do
MidiManager
para descobrir, abrir
e fechar um ou mais
objetos MidiDevice
, além de
transmitir dados de cada dispositivo e para eles pelas portas
de entrada
e saída MIDI:
Ao usar a AMidi, você transmite o endereço de um MidiDevice
para a camada de código nativo
com uma chamada JNI. A partir daí, a AMidi cria uma referência a um AMidiDevice
, que tem a maior parte da funcionalidade de um MidiDevice
. Seu código nativo usa funções da AMidi que se comunicam diretamente com um AMidiDevice
. O AMidiDevice
se conecta diretamente ao serviço MIDI:
Usando chamadas AMidi, você pode integrar a lógica de áudio/controle C/C++ do seu app de forma mais próxima
à transmissão MIDI. Há menos necessidade de chamadas JNI ou callbacks para a parte em Java do seu app. Por exemplo, um sintetizador digital implementado em código C poderia receber eventos de chave diretamente de AMidiDevice
, em vez de esperar uma chamada de JNI para enviar os eventos da parte em Java. Ou um processo de composição algorítmico poderia enviar um desempenho MIDI diretamente a um AMidiDevice
sem fazer callback para a parte em Java para transmitir os eventos-chave.
Embora a AMidi melhore a conexão direta a dispositivos MIDI, os apps ainda precisam usar o MidiManager
para descobrir e abrir objetos MidiDevice
. A AMidi pode assumir a partir de então.
Às vezes, pode ser necessário passar informações da camada de IU para o código nativo. Por exemplo, quando eventos MIDI são enviados em resposta a botões na tela. Para isso, crie chamadas JNI personalizadas para sua lógica nativa. Se precisar enviar dados de volta para atualizar a IU, você poderá fazer um callback da camada nativa, como de costume.
Este documento mostra como configurar um app de código nativo AMidi, com exemplos de envio e recebimento de comandos MIDI. Para um exemplo completo, confira o app de exemplo NativeMidi (link em inglês).
Usar a AMidi
Todos os apps que usam a AMidi têm as mesmas etapas de configuração e fechamento, quer eles enviem ou recebam MIDI, quer façam as duas coisas.
Iniciar a AMidi
Na parte em Java, o app precisa descobrir uma
peça do hardware MIDI, criar um MidiDevice
correspondente
e transmiti-lo para o código nativo.
- Descobrir o hardware MIDI com a classe
MidiManager
do Java. - Conseguir um objeto
MidiDevice
do Java correspondente ao hardware MIDI. - Transmitir o
MidiDevice
do Java para o código nativo com JNI.
Descobrir hardware e portas
Os objetos de porta de entrada e saída não pertencem ao aplicativo. Eles representam as portas
no dispositivo midi. Para enviar dados MIDI a um dispositivo, um app abre uma MIDIInputPort
e, em seguida, grava dados nela. Por outro lado, para receber dados, um app abre uma MIDIOutputPort
. Para funcionar corretamente, o app precisa ter certeza de que as portas do tipo correto serão abertas. A descoberta de dispositivos e portas é feita na parte em Java.
Veja um método que descobre cada dispositivo MIDI e analisa as portas deles. Ele retorna uma lista de dispositivos com portas de saída para receber dados ou uma lista de dispositivos com portas de entrada para enviar dados. Um dispositivo MIDI pode ter portas de entrada e de saída.
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; }
Para usar as funções AMidi no código C/C++, você precisa incluir
AMidi/AMidi.h
e vincular a biblioteca amidi
. Esses itens podem ser encontrados no Android NDK.
O lado Java precisa transmitir um ou mais objetos MidiDevice
e números de porta para
a camada nativa por uma chamada JNI. A camada nativa precisa executar as seguintes etapas:
- Para cada
MidiDevice
Java, consiga umAMidiDevice
usandoAMidiDevice_fromJava()
. - Consiga
AMidiInputPort
e/ouAMidiOutputPort
deAMidiDevice
comAMidiInputPort_open()
e/ouAMidiOutputPort_open()
. - Usar as portas recebidas para enviar e/ou receber dados MIDI.
Parar a AMidi
O app Java sinalizará a camada nativa para liberar recursos quando não estiver mais usando o dispositivo MIDI. Isso pode ocorrer porque o dispositivo MIDI foi desconectado ou porque o app está saindo.
Para liberar recursos MIDI, seu código precisa executar estas tarefas:
- Parar de ler e/ou gravar em portas MIDI. Se você estava usando uma linha de execução de leitura para pesquisar entradas (consulte abaixo Implementar um loop de pesquisa), interrompa a linha.
- Feche todos os objetos
AMidiInputPort
e/ouAMidiOutputPort
abertos com as funçõesAMidiInputPort_close()
e/ouAMidiOutputPort_close()
. - Libere o
AMidiDevice
comAMidiDevice_release()
.
Receber dados MIDI
Um exemplo típico de app que recebe MIDI é um "sintetizador virtual", que recebe dados de desempenho MIDI para controlar a síntese de áudio.
Dados MIDI são recebidos de forma assíncrona. Portanto, recomendamos ler esses dados em uma linha de execução diferente que monitore continuamente um deles ou as portas de saída MIDI. Pode ser uma linha de execução em segundo plano ou uma linha de execução de áudio. A AMidi não é bloqueada durante a leitura de uma porta, então ela pode ser usada com segurança dentro de um callback de áudio.
Configurar um MidiDevice e as portas de saída dele
Um app lê dados MIDI de entrada pelas portas de saída de um dispositivo. A parte em Java do seu app precisa determinar quais dispositivos e portas usar.
Este snippet cria o
MidiManager
usando o serviço MIDI Android e abre
um MidiDevice
para o primeiro dispositivo que detectar. Quando o MidiDevice
tiver sido aberto, um callback será recebido para uma instância do MidiManager.OnDeviceOpenedListener()
. O método onDeviceOpened
desse listener é chamado e, em seguida, chama startReadingMidi()
para abrir a porta de saída 0 no dispositivo. Essa é uma função JNI definida em AppMidiManager.cpp
. Essa função é explicada no snippet a seguir.
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); } } }
O código nativo traduz o dispositivo MIDI na parte em Java e as portas dele em referências usadas por funções AMidi.
Aqui está a função JNI que cria um AMidiDevice
ao chamar AMidiDevice_fromJava()
e, em seguida, chama AMidiOutputPort_open()
para abrir uma porta de entrada no dispositivo:
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...
}
Implementar um loop de pesquisa
Apps que recebem dados MIDI precisam pesquisar a porta de saída e responder quando
AMidiOutputPort_receive()
retornar um número maior que zero.
Para apps com baixa largura de banda, como um escopo MIDI, é possível pesquisar em uma linha de execução de prioridade baixa em segundo plano (com suspensões apropriadas).
Para apps que geram áudio e têm requisitos de desempenho em tempo real mais rigorosos, é possível pesquisar no callback de geração de áudio principal (o callback BufferQueue
para OpenSL ES, o callback de dados AudioStream em AAudio).
Como o AMidiOutputPort_receive()
não realiza bloqueios, há muito pouco impacto no desempenho.
A função readThreadRoutine()
, chamada pela função startReadingMidi()
acima, pode ter esta aparência:
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;
}
}
}
Um app que usa uma API de áudio nativa (como OpenSL ES ou AAudio) pode adicionar código de recebimento MIDI ao callback de geração de áudio, desta forma:
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…
// ...
}
O diagrama a seguir ilustra o fluxo de um app de leitura de MIDI:
Enviar dados MIDI
Um exemplo comum de app de gravação de MIDI é um controlador ou sequenciador MIDI.
Configurar um MidiDevice e as portas de entrada dele
Um app grava dados MIDI de saída nas portas de entrada de um dispositivo. A parte em Java do seu app precisa determinar quais dispositivos e portas MIDI usar.
O código de configuração abaixo é uma variação do exemplo de recebimento acima. Ele cria o MidiManager
a partir do serviço MIDI do Android. Ele abre o primeiro MidiDevice
que encontra e chama startWritingMidi()
para abrir a primeira porta de entrada no dispositivo. Essa é uma chamada JNI definida em AppMidiManager.cpp
. A função é explicada no snippet a seguir.
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); } } }
Aqui está a função JNI que cria um AMidiDevice
ao chamar AMidiDevice_fromJava()
e, em seguida, chama AMidiInputPort_open()
para abrir uma porta de entrada no dispositivo:
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;
}
Enviar dados MIDI
Como o tempo dos dados MIDI de saída é conhecido e controlado pelo próprio app, a transmissão de dados pode ser feita na linha de execução principal do app MIDI. No entanto, para melhorar o desempenho (como em um sequenciador), a geração e a transmissão de MIDI podem ser realizadas em uma linha de execução separada.
Os apps podem enviar dados MIDI sempre que necessário. Observe que a AMidi é bloqueada quando grava dados.
A seguir, há um exemplo de método JNI que recebe um buffer de comandos MIDI e o grava:
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);
}
O diagrama a seguir ilustra o fluxo de um app de gravação de MIDI:
Callbacks
Embora não seja estritamente um recurso AMidi, seu código nativo pode precisar passar dados de volta para a parte em Java (para atualizar a IU, por exemplo). Para fazer isso, você precisa adicionar um código à parte em Java e à camada nativa:
- Crie um método de callback na parte em Java.
- Escreva uma função JNI que armazene as informações necessárias para invocar o callback.
Na hora do callback, seu código nativo pode criar
Este é o método callback da parte em Java, 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); } }); }
Este é o código C para a função JNI que configura o callback para MainActivity.onNativeMessageReceive()
. O MainActivity
Java chama initNative()
na inicialização:
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");
}
Na hora de enviar dados de volta para o Java, o código nativo recupera os ponteiros do callback e cria o 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);
}
Outros recursos
- Referência da AMidi
- Veja o app de amostra Native MIDI completo no GitHub (link em inglês).