नेटिव MIDI API

AMidi एपीआई, Android NDK r20b और इसके बाद के वर्शन में उपलब्ध है. यह ऐप्लिकेशन डेवलपर को C/C++ कोड की मदद से, MIDI डेटा भेजने और पाने की सुविधा देता है.

Android MIDI ऐप्लिकेशन, आम तौर पर Android MIDI सेवा के साथ कम्यूनिकेट करने के लिए, midi एपीआई का इस्तेमाल करते हैं. एमआईडीआई ऐप्लिकेशन मुख्य रूप से MidiManager पर निर्भर करते हैं. इससे एक या उससे ज़्यादा MidiDevice ऑब्जेक्ट को खोजने, खोलने, और बंद करने में मदद मिलती है. साथ ही, डिवाइस के एमआईडीआई इनपुट और आउटपुट पोर्ट के ज़रिए, हर डिवाइस से डेटा को पास किया जाता है:

AMidi का इस्तेमाल करने पर, आपको JNI कॉल की मदद से नेटिव कोड लेयर को MidiDevice का पता पास करना होता है. इसके बाद, AMidi एक AMidiDevice का रेफ़रंस बनाता है. इसमें MidiDevice की ज़्यादातर सुविधाएं होती हैं. आपका नेटिव कोड, AMidi फ़ंक्शन का इस्तेमाल करता है. ये फ़ंक्शन, AMidiDevice से सीधे तौर पर कम्यूनिकेट करते हैं. AMidiDevice, MIDI सेवा से सीधे तौर पर कनेक्ट होता है:

AMidi कॉल का इस्तेमाल करके, अपने ऐप्लिकेशन के C/C++ ऑडियो/कंट्रोल लॉजिक को एमआईडीआई ट्रांसमिशन के साथ आसानी से इंटिग्रेट किया जा सकता है. आपके ऐप्लिकेशन के Java साइड पर, जेएनआई कॉल या कॉलबैक की ज़रूरत कम होती है. उदाहरण के लिए, C कोड में लागू किया गया डिजिटल सिंथेसाइज़र, सीधे AMidiDevice से मुख्य इवेंट पा सकता है. इसके लिए, उसे Java साइड से इवेंट भेजने के लिए जेएनआई कॉल का इंतज़ार नहीं करना पड़ता. इसके अलावा, एल्गोरिदम के हिसाब से कंपोज़ करने की प्रोसेस, एमआईडीआई परफ़ॉर्मेंस को सीधे AMidiDevice पर भेज सकती है. इसके लिए, मुख्य इवेंट को ट्रांसमिट करने के लिए, Java साइड को कॉल बैक करने की ज़रूरत नहीं पड़ती.

हालांकि, AMidi से एमआईडीआई डिवाइसों के साथ डायरेक्ट कनेक्शन बेहतर होता है, लेकिन ऐप्लिकेशन को MidiDevice ऑब्जेक्ट खोजने और खोलने के लिए, अब भी MidiManager का इस्तेमाल करना होगा. इसके बाद, AMidi इसे आगे प्रोसेस कर सकता है.

कभी-कभी आपको यूज़र इंटरफ़ेस (यूआई) लेयर से नेटिव कोड में जानकारी पास करनी पड़ सकती है. उदाहरण के लिए, जब स्क्रीन पर मौजूद बटन के जवाब में MIDI इवेंट भेजे जाते हैं. इसके लिए, अपने नेटिव लॉजिक के लिए कस्टम जेएनआई कॉल बनाएं. अगर आपको यूज़र इंटरफ़ेस (यूआई) को अपडेट करने के लिए डेटा वापस भेजना है, तो नेटिव लेयर से हमेशा की तरह कॉल बैक किया जा सकता है.

इस दस्तावेज़ में, AMidi नेटिव कोड ऐप्लिकेशन को सेट अप करने का तरीका बताया गया है. इसमें एमआईडीआई कमांड भेजने और पाने, दोनों के उदाहरण दिए गए हैं. काम करने का पूरा उदाहरण देखने के लिए, NativeMidi सैंपल ऐप्लिकेशन देखें.

AMidi का इस्तेमाल करना

AMidi का इस्तेमाल करने वाले सभी ऐप्लिकेशन को सेट अप और बंद करने का तरीका एक जैसा होता है. भले ही, वे MIDI भेजते हों, पाते हों या दोनों.

AMidi का इस्तेमाल शुरू करना

Java साइड पर, ऐप्लिकेशन को अटैच किए गए MIDI हार्डवेयर का पता लगाना होगा. साथ ही, उससे जुड़ा MidiDevice बनाना होगा और उसे नेटिव कोड को पास करना होगा.

  1. Java MidiManager क्लास की मदद से, एमआईडीआई हार्डवेयर का पता लगाएं.
  2. एमआईडीआई हार्डवेयर से जुड़ा Java MidiDevice ऑब्जेक्ट पाएं.
  3. JNI की मदद से, Java MidiDevice को नेटिव कोड में पास करें.

हार्डवेयर और पोर्ट के बारे में जानें

इनपुट और आउटपुट पोर्ट ऑब्जेक्ट, ऐप्लिकेशन से जुड़े नहीं होते. ये मिडी डिवाइस पर मौजूद पोर्ट को दिखाते हैं. किसी डिवाइस पर MIDI डेटा भेजने के लिए, ऐप्लिकेशन MIDIInputPort खोलता है और फिर उसमें डेटा लिखता है. इसके उलट, डेटा पाने के लिए कोई ऐप्लिकेशन MIDIOutputPort खोलता है. ऐप्लिकेशन को सही तरीके से काम करने के लिए, यह पक्का करना होगा कि वह जिन पोर्ट को खोलता है वे सही टाइप के हों. डिवाइस और पोर्ट की खोज, Java की मदद से की जाती है.

यहां एक तरीका बताया गया है, जिससे हर एमआईडीआई डिवाइस का पता लगाया जा सकता है और उसके पोर्ट देखे जा सकते हैं. यह डेटा पाने के लिए आउटपुट पोर्ट वाले डिवाइसों की सूची या डेटा भेजने के लिए इनपुट पोर्ट वाले डिवाइसों की सूची दिखाता है. एमआईडीआई डिवाइस में इनपुट और आउटपुट, दोनों पोर्ट हो सकते हैं.

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

अपने C/C++ कोड में AMidi फ़ंक्शन इस्तेमाल करने के लिए, आपको AMidi/AMidi.h को शामिल करना होगा और amidi लाइब्रेरी से लिंक करना होगा. ये दोनों Android NDK में मिल सकते हैं.

Java साइड को JNI कॉल के ज़रिए नेटिव लेयर को एक या उससे ज़्यादा MidiDevice ऑब्जेक्ट और पोर्ट नंबर पास करने चाहिए. इसके बाद, नेटिव लेयर को ये काम करने चाहिए:

  1. हर Java MidiDevice के लिए, AMidiDevice_fromJava() का इस्तेमाल करके AMidiDevice पाएं.
  2. AMidiDevice से AMidiInputPort और/या AMidiOutputPort हासिल करें. इसके लिए, AMidiInputPort_open() और/या AMidiOutputPort_open() की ज़रूरत होगी.
  3. एमआईडीआई डेटा भेजने और/या पाने के लिए, मिले पोर्ट का इस्तेमाल करें.

AMidi को बंद करना

जब Java ऐप्लिकेशन, एमआईडीआई डिवाइस का इस्तेमाल करना बंद कर दे, तो उसे नेटिव लेयर को सिग्नल भेजना चाहिए, ताकि वह संसाधनों को रिलीज़ कर सके. ऐसा इसलिए हो सकता है, क्योंकि MIDI डिवाइस डिसकनेक्ट हो गया है या ऐप्लिकेशन बंद हो रहा है.

एमआईडीआई संसाधनों को रिलीज़ करने के लिए, आपके कोड को ये टास्क पूरे करने होंगे:

  1. एमआईडीआई पोर्ट से डेटा पढ़ना और/या लिखना बंद करें. अगर इनपुट के लिए पोल करने के लिए, रीडिंग थ्रेड का इस्तेमाल किया जा रहा था (नीचे पोलिंग लूप लागू करें देखें), तो थ्रेड को रोकें.
  2. AMidiInputPort_close() और/या AMidiOutputPort_close() फ़ंक्शन का इस्तेमाल करके, खुले हुए AMidiInputPort और/या AMidiOutputPort ऑब्जेक्ट बंद करें.
  3. AMidiDevice को AMidiDevice_release() के साथ रिलीज़ करें.

एमआईडीआई डेटा पाना

एमआईडीआई डेटा पाने वाले एमआईडीआई ऐप्लिकेशन का एक सामान्य उदाहरण "वर्चुअल सिंथेसाइज़र" है. यह ऑडियो सिंथेसिस को कंट्रोल करने के लिए, एमआईडीआई परफ़ॉर्मेंस डेटा पाता है.

आने वाला MIDI डेटा, एसिंक्रोनस तरीके से मिलता है. इसलिए, एमआईडीआई को एक अलग थ्रेड में पढ़ना सबसे सही रहता है. यह थ्रेड, एक या उससे ज़्यादा एमआईडीआई आउटपुट पोर्ट को लगातार पोल करता है. यह कोई बैकग्राउंड थ्रेड या ऑडियो थ्रेड हो सकता है. AMidi, किसी पोर्ट से डेटा पढ़ते समय ब्लॉक नहीं होता. इसलिए, ऑडियो कॉलबैक में इसका इस्तेमाल करना सुरक्षित है.

MidiDevice और उसके आउटपुट पोर्ट सेट अप करना

कोई ऐप्लिकेशन, डिवाइस के आउटपुट पोर्ट से आने वाले एमआईडीआई डेटा को पढ़ता है. आपके ऐप्लिकेशन के Java साइड को यह तय करना होगा कि किस डिवाइस और पोर्ट का इस्तेमाल करना है.

यह स्निपेट, Android की MIDI सेवा से MidiManager बनाता है और पहले डिवाइस के लिए MidiDevice खोलता है. MidiDevice खुलने पर, MidiManager.OnDeviceOpenedListener() के इंस्टेंस को कॉलबैक मिलता है. इस लिसनर के onDeviceOpened तरीके को कॉल किया जाता है. इसके बाद, डिवाइस पर आउटपुट पोर्ट 0 को खोलने के लिए, startReadingMidi() को कॉल किया जाता है. यह AppMidiManager.cpp में तय किया गया एक JNI फ़ंक्शन है. इस फ़ंक्शन के बारे में अगले स्निपेट में बताया गया है.

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

नेटिव कोड, Java-साइड एमआईडीआई डिवाइस और उसके पोर्ट को उन रेफ़रंस में बदलता है जिनका इस्तेमाल AMidi फ़ंक्शन करते हैं.

यहां जेएनआई फ़ंक्शन दिया गया है, जो AMidiDevice_fromJava() को कॉल करके AMidiDevice बनाता है. इसके बाद, डिवाइस पर आउटपुट पोर्ट खोलने के लिए, AMidiOutputPort_open() को कॉल करता है:

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

}

पोलिंग लूप लागू करना

एमआईडीआई डेटा पाने वाले ऐप्लिकेशन को आउटपुट पोर्ट को पोल करना चाहिए और AMidiOutputPort_receive() के शून्य से ज़्यादा संख्या दिखाने पर जवाब देना चाहिए.

कम बैंडविड्थ वाले ऐप्लिकेशन, जैसे कि MIDI स्कोप के लिए, कम प्राथमिकता वाले बैकग्राउंड थ्रेड में पोल किया जा सकता है. इसके लिए, सही स्लीप मोड का इस्तेमाल करें.

ऑडियो जनरेट करने वाले और रीयल टाइम में बेहतर परफ़ॉर्मेंस देने वाले ऐप्लिकेशन के लिए, मुख्य ऑडियो जनरेशन कॉलबैक (OpenSL ES के लिए BufferQueue कॉलबैक, AAudio में AudioStream डेटा कॉलबैक) में पोल किया जा सकता है. AMidiOutputPort_receive(), नॉन-ब्लॉकिंग है. इसलिए, इससे परफ़ॉर्मेंस पर बहुत कम असर पड़ता है.

ऊपर दिए गए startReadingMidi() फ़ंक्शन से कॉल किया गया readThreadRoutine() फ़ंक्शन, कुछ ऐसा दिख सकता है:

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

नेटिव ऑडियो एपीआई (जैसे, OpenSL ES या AAudio) का इस्तेमाल करने वाला कोई ऐप्लिकेशन, ऑडियो जनरेट करने वाले कॉलबैक में MIDI सिग्नल पाने वाला कोड जोड़ सकता है. इसके लिए, यह तरीका अपनाएं:

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

इस डायग्राम में, MIDI फ़ाइल को पढ़ने वाले ऐप्लिकेशन का फ़्लो दिखाया गया है:

एमआईडीआई डेटा भेजना

एमआईडीआई लिखने वाले ऐप्लिकेशन का एक सामान्य उदाहरण, एमआईडीआई कंट्रोलर या सीक्वेंसर है.

MidiDevice और उसके इनपुट पोर्ट सेट अप करना

कोई ऐप्लिकेशन, आउटगोइंग एमआईडीआई डेटा को एमआईडीआई डिवाइस के इनपुट पोर्ट में सेव करता है. आपके ऐप्लिकेशन के Java साइड को यह तय करना होगा कि किस एमआईडीआई डिवाइस और पोर्ट का इस्तेमाल करना है.

यहां दिया गया सेटअप कोड, ऊपर दिए गए उदाहरण का एक वैरिएशन है. यह Android की एमआईडीआई सेवा से MidiManager बनाता है. इसके बाद, यह पहलेMidiDevice को खोलता है और डिवाइस पर पहले इनपुट पोर्ट को खोलने के लिए, startWritingMidi() को कॉल करता है. यह AppMidiManager.cpp में तय किया गया जेएनआई कॉल है. इस फ़ंक्शन के बारे में अगले स्निपेट में बताया गया है.

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

यहां जेएनआई फ़ंक्शन दिया गया है, जो AMidiDevice_fromJava() को कॉल करके AMidiDevice बनाता है. इसके बाद, डिवाइस पर इनपुट पोर्ट खोलने के लिए AMidiInputPort_open() को कॉल करता है:

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 डेटा का समय, ऐप्लिकेशन खुद तय करता है और उसे कंट्रोल करता है. इसलिए, डेटा ट्रांसमिशन, MIDI ऐप्लिकेशन के मुख्य थ्रेड में किया जा सकता है. हालांकि, परफ़ॉर्मेंस को बेहतर बनाने के लिए (जैसे कि सीक्वेंसर में), एमआईडीआई को जनरेट और ट्रांसमिट करने की प्रोसेस को अलग थ्रेड में किया जा सकता है.

ऐप्लिकेशन, ज़रूरत पड़ने पर एमआईडीआई डेटा भेज सकते हैं. ध्यान दें कि डेटा लिखते समय, AMidi ब्लॉक हो जाता है.

यहां JNI तरीके का एक उदाहरण दिया गया है, जो MIDI निर्देशों का बफ़र लेता है और उसे लिखता है:

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

इस डायग्राम में, MIDI डेटा लिखने वाले ऐप्लिकेशन का फ़्लो दिखाया गया है:

कॉलबैक

हालांकि, यह ज़रूरी नहीं है कि यह AMidi की सुविधा हो, लेकिन आपके नेटिव कोड को डेटा वापस Java साइड पर भेजना पड़ सकता है. उदाहरण के लिए, यूज़र इंटरफ़ेस (यूआई) को अपडेट करने के लिए. इसके लिए, आपको Java साइड और नेटिव लेयर में कोड लिखना होगा:

  • Java साइड पर कॉलबैक का तरीका बनाएं.
  • एक JNI फ़ंक्शन लिखें, जो कॉलबैक को शुरू करने के लिए ज़रूरी जानकारी को सेव करता है.

कॉल बैक करने का समय आने पर, आपका नेटिव कोड

यहां 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);
            }
        });
}

यहां JNI फ़ंक्शन के लिए C कोड दिया गया है. यह फ़ंक्शन, MainActivity.onNativeMessageReceive() के लिए कॉलबैक सेट अप करता है. डिवाइस चालू होने पर, Java MainActivity initNative() को कॉल करता है:

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

जब Java को डेटा वापस भेजने का समय आता है, तो नेटिव कोड, कॉलबैक पॉइंटर को वापस लाता है और कॉलबैक बनाता है:

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

अन्य संसाधन