Native MIDI API

AMidi API Android NDK r20b এবং পরবর্তী ভার্সনে উপলভ্য। এটি অ্যাপ ডেভেলপারদের C/C++ কোডের মাধ্যমে MIDI ডেটা পাঠানো এবং গ্রহণ করার ক্ষমতা দেয়।

Android MIDI অ্যাপ সাধারণত Android MIDI পরিষেবার সাথে যোগাযোগ করতে midi API ব্যবহার করে। MIDI অ্যাপ মূলত MidiManager-এর উপর নির্ভর করে, যাতে এক বা একাধিক MidiDevice অবজেক্ট খুঁজে পাওয়া, খোলা ও বন্ধ করা যায় এবং ডিভাইসের MIDI ইনপুট ও আউটপুট পোর্টের মাধ্যমে প্রতিটি ডিভাইসে ডেটা পাঠানো ও গ্রহণ করা যায়:

আপনি AMidi ব্যবহার করার সময় JNI কলের মাধ্যমে নেটিভ কোড লেয়ারে MidiDevice-এর অ্যাড্রেস পাস করেন। সেখান থেকে, AMidi একটি AMidiDevice-এর রেফারেন্স তৈরি করে যার মধ্যে MidiDevice-এর বেশিরভাগ কার্যকারিতা রয়েছে। আপনার নেটিভ কোড AMidi ফাংশন ব্যবহার করে যা সরাসরি AMidiDevice-এর সাথে যোগাযোগ করে। AMidiDevice সরাসরি MIDI পরিষেবার সাথে কানেক্ট করে:

AMidi কল ব্যবহার করে, আপনি MIDI ট্রান্সমিশনের সাথে আপনার অ্যাপের C/C++ অডিও/কন্ট্রোল লজিক ঘনিষ্ঠভাবে ইন্টিগ্রেট করতে পারবেন। আপনার অ্যাপের Java সাইডে JNI কল বা কলব্যাকের প্রয়োজন কম হয়। যেমন, C কোডে ইমপ্লিমেন্ট করা ডিজিটাল সিন্থেসাইজার সরাসরি AMidiDevice থেকে কী ইভেন্ট পেতে পারে, Java সাইড থেকে ইভেন্ট পাঠানোর জন্য JNI কলের অপেক্ষা করতে হয় না। অথবা, অ্যালগরিদম কম্পোজিং প্রসেস, কী ইভেন্ট ট্রান্সমিট করার জন্য জাভা সাইডে কল ব্যাক না করেই, সরাসরি AMidiDevice-এ MIDI পারফর্ম্যান্স পাঠাতে পারে।

যদিও AMidi, MIDI ডিভাইসের সাথে সরাসরি কানেকশন উন্নত করে, তবুও অ্যাপকে MidiDevice অবজেক্ট খুঁজতে ও খুলতে MidiManager ব্যবহার করতে হবে। AMidi সেখান থেকে শুরু করতে পারে।

কখনও কখনও আপনাকে UI লেয়ার থেকে নেটিভ কোডে তথ্য পাস করতে হতে পারে। যেমন, স্ক্রিনে বোতামের উত্তরে MIDI ইভেন্ট পাঠানো হলে। এটি করতে আপনার নেটিভ লজিকে কাস্টম JNI কল তৈরি করুন। UI আপডেট করার জন্য আপনাকে ডেটা ফেরত পাঠাতে হলে, আপনি নেটিভ লেয়ার থেকে আগের মতোই কল ব্যাক করতে পারবেন।

এই ডকুমেন্ট থেকে আপনি জানতে পারবেন যে কীভাবে AMidi নেটিভ কোড অ্যাপ সেট-আপ করতে হয় এবং MIDI কমান্ড পাঠানো ও রিসিভ করা, দুটিরই উদাহরণ দেওয়া হয়েছে। সম্পূর্ণ কাজের উদাহরণ দেখতে NativeMidi স্যাম্পেল অ্যাপ চেক করুন।

AMidi ব্যবহার করুন

AMidi ব্যবহার করে এমন সব অ্যাপের সেট-আপ ও বন্ধ করার ধাপ একই, সেগুলি MIDI পাঠাক বা গ্রহণ করুক অথবা দুটিই করুক।

AMidi চালু করুন

Java-র ক্ষেত্রে, অ্যাপকে অবশ্যই MIDI হার্ডওয়্যারের অ্যাটাচ করা অংশ খুঁজে বের করতে হবে, সংশ্লিষ্ট MidiDevice তৈরি করতে হবে এবং নেটিভ কোডে পাস করতে হবে।

  1. Java MidiManager ক্লাসের মাধ্যমে MIDI হার্ডওয়্যার খুঁজে নিন।
  2. MIDI হার্ডওয়্যারের সাথে সম্পর্কিত একটি Java MidiDevice অবজেক্ট পান।
  3. JNI-এর মাধ্যমে নেটিভ কোডে Java MidiDevice পাস করুন।

হার্ডওয়্যার ও পোর্ট সম্পর্কে জানুন

ইনপুট ও আউটপুট পোর্ট অবজেক্ট অ্যাপের সাথে সম্পর্কিত নয়। এগুলি মিডি ডিভাইসের পোর্টকে বোঝায়। কোনও ডিভাইসে MIDI ডেটা পাঠাতে, কোনও অ্যাপ একটি MIDIInputPort খোলে এবং তারপরে এতে ডেটা লেখে। বিপরীতভাবে, ডেটা পেতে, একটি অ্যাপ MIDIOutputPort খোলে। সঠিকভাবে কাজ করার জন্য, অ্যাপটিকে নিশ্চিত করতে হবে যে এটি যে পোর্টগুলি খোলে সেগুলি সঠিক ধরনের। ডিভাইস ও পোর্ট শনাক্তকরণ জাভা সাইডে করা হয়।

এখানে একটি পদ্ধতি দেওয়া হল যা প্রতিটি MIDI ডিভাইস খুঁজে বের করে এবং এর পোর্টগুলি দেখে। এটি ডেটা রিসিভ করার জন্য আউটপুট পোর্ট সহ ডিভাইসের তালিকা অথবা ডেটা পাঠানোর জন্য ইনপুট পোর্ট সহ ডিভাইসের তালিকা দেখায়। একটি MIDI ডিভাইসে ইনপুট পোর্ট এবং আউটপুট পোর্ট, দুটিই থাকতে পারে।

Kotlin

private fun getMidiDevices(isOutput: Boolean) : List {
    if (isOutput) {
        return mMidiManager.devices.filter { it.outputPortCount > 0 }
    } else {
        return mMidiManager.devices.filter { it.inputPortCount > 0 }
    }
}

জাভা

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-তে পাওয়া যাবে।

জাভা সাইডকে এক বা একাধিক MidiDevice অবজেক্ট এবং পোর্ট নম্বর JNI কলের মাধ্যমে নেটিভ লেয়ারে পাস করতে হবে। নেটিভ লেয়ারকে তারপর নিম্নলিখিত ধাপগুলি সম্পূর্ণ করতে হবে:

  1. প্রতিটি Java MidiDevice-এর জন্য AMidiDevice_fromJava() ব্যবহার করে একটি AMidiDevice পান।
  2. AMidiDevice থেকে AMidiInputPort এবং/অথবা AMidiOutputPort পান AMidiInputPort_open() এবং/অথবা AMidiOutputPort_open() সহ।
  3. MIDI ডেটা পাঠাতে এবং/অথবা পেতে প্রাপ্ত পোর্ট ব্যবহার করুন।

AMidi বন্ধ করুন

MIDI ডিভাইস আর ব্যবহার না করলে, জাভা অ্যাপের উচিত নেটিভ লেয়ারকে রিসোর্স রিলিজ করার জন্য সিগন্যাল পাঠানো। MIDI ডিভাইস ডিসকানেক্ট হয়ে যাওয়ার কারণে অথবা অ্যাপ বন্ধ হয়ে যাওয়ার কারণে এটি হতে পারে।

MIDI রিসোর্স রিলিজ করতে, আপনার কোডকে এইসব টাস্ক করতে হবে:

  1. MIDI পোর্টে রিডিং এবং/অথবা রাইটিং বন্ধ করুন। ইনপুটের জন্য পোল করতে আপনি যদি রিডিং থ্রেড ব্যবহার করে থাকেন (নিচে পোলিং লুপ প্রয়োগ করুন দেখুন), তাহলে থ্রেডটি বন্ধ করুন।
  2. AMidiInputPort_close() এবং/অথবা AMidiOutputPort_close() ফাংশন ব্যবহার করে খোলা থাকা যেকোনও AMidiInputPort এবং/অথবা AMidiOutputPort অবজেক্ট বন্ধ করুন।
  3. AMidiDevice_release()-এর সাথে AMidiDevice রিলিজ করুন।

MIDI ডেটা রিসিভ করুন

MIDI রিসিভ করে এমন MIDI অ্যাপের একটি সাধারণ উদাহরণ হল "ভার্চুয়াল সিন্থেসাইজার" যা অডিও সিন্থেসিস কন্ট্রোল করার জন্য MIDI পারফর্ম্যান্স ডেটা রিসিভ করে।

ইনকামিং MIDI ডেটা অ্যাসিঙ্ক্রোনাসভাবে রিসিভ করা হয়। তাই, MIDI-কে আলাদা থ্রেডে পড়া সবচেয়ে ভালো যা এক বা একাধিক MIDI আউটপুট পোর্টকে ক্রমাগত পোল করে। এটি ব্যাকগ্রাউন্ড থ্রেড বা অডিও থ্রেড হতে পারে। AMidi কোনও পোর্ট থেকে রিড করার সময় ব্লক করে না এবং তাই অডিও কলব্যাকের মধ্যে ব্যবহার করা নিরাপদ।

MidiDevice এবং এর আউটপুট পোর্ট সেট-আপ করুন

কোনও অ্যাপ ডিভাইসের আউটপুট পোর্ট থেকে ইনকামিং MIDI ডেটা পড়ে। আপনার অ্যাপের জাভা সাইডকে অবশ্যই নির্ধারণ করতে হবে যে কোন ডিভাইস এবং পোর্ট ব্যবহার করতে হবে।

এই স্নিপেট 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)
    }
  }
}

জাভা

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

নেটিভ কোড জাভা-সাইড MIDI ডিভাইস এবং এর পোর্টগুলিকে AMidi ফাংশন দ্বারা ব্যবহৃত রেফারেন্সে অনুবাদ করে।

এখানে JNI ফাংশন দেওয়া হল যা 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...

}

পোলিং লুপ প্রয়োগ করুন

যেসব অ্যাপ MIDI ডেটা পায়, সেগুলিকে আউটপুট পোর্ট পোল করতে হবে এবং 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;
        }
  }
}

নেটিভ অডিও API (যেমন 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 রিডিং অ্যাপের ফ্লো ব্যাখ্যা করে:

MIDI ডেটা পাঠান

MIDI রাইটিং অ্যাপের একটি সাধারণ উদাহরণ হল MIDI কন্ট্রোলার বা সিকোয়েন্সার।

MidiDevice এবং এর ইনপুট পোর্ট সেট-আপ করুন

কোনও অ্যাপ, MIDI ডিভাইসের ইনপুট পোর্টে আউটগোয়িং MIDI ডেটা লেখে। আপনার অ্যাপের Java সাইডকে অবশ্যই নির্ধারণ করতে হবে যে কোন MIDI ডিভাইস এবং পোর্ট ব্যবহার করতে হবে।

নিচে দেখানো সেট-আপ কোডটি উপরে দেখানো রিসিভ করার উদাহরণের একটি ভেরিয়েশন। এটি Android-এর MIDI পরিষেবা থেকে MidiManager তৈরি করে। এটি তারপর প্রথম যেMidiDevice খুঁজে পায় সেটি খোলে এবং ডিভাইসে প্রথম ইনপুট পোর্ট খোলার জন্য startWritingMidi()-কে কল করে। এটি একটি JNI কল যা 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)
    }
  }
}

জাভা

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

এখানে JNI ফাংশন দেওয়া হল যা 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 ডেটার টাইমিং অ্যাপ নিজেই ভালভাবে বুঝতে ও কন্ট্রোল করতে পারে, তাই ডেটা ট্রান্সমিশন MIDI অ্যাপের মূল থ্রেডে করা যেতে পারে। তবে, পারফর্ম্যান্সের কারণে (যেমন সিকোয়েন্সারে) 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 ফিচার না হলেও, আপনার নেটিভ কোডকে জাভা সাইডে ডেটা ফেরত পাঠাতে হতে পারে (যেমন, UI আপডেট করার জন্য)। এটি করতে, আপনাকে অবশ্যই Java সাইড এবং নেটিভ লেয়ারে কোড লিখতে হবে:

  • Java-তে একটি কলব্যাক পদ্ধতি তৈরি করুন।
  • কলব্যাক চালু করার জন্য প্রয়োজনীয় তথ্য স্টোর করে এমন একটি JNI ফাংশন লিখুন।

কলব্যাক করার সময় হলে, আপনার নেটিভ কোড তৈরি করতে পারে

এখানে জাভা-সাইড কলব্যাক পদ্ধতি, 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) }
}

জাভা

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

অতিরিক্ত রিসোর্স