تحويل ملف mkbitmap إلى WebAssembly

في القسم ما هو WebAssembly ومن أين أتى؟، أوضحت كيف انتهى بنا الأمر إلى WebAssembly اليوم. في هذه المقالة، سأوضح لك أسلوبي في تجميع برنامج C حالي، mkbitmap، باستخدام WebAssembly. وهو أكثر تعقيدًا من مثال hello world، فهو يتضمّن العمل على الملفات والتواصل بين أرض WebAssembly وJavaScript والرسم على لوحة رسم، ولكن لا يزال من الممكن إدارته بما يكفي لعدم إرباكك.

هذه المقالة مكتوبة لمطوّري البرامج على الويب الذين يريدون تعلُّم WebAssembly، وتعرض خطوة بخطوة كيفية المتابعة إذا أردت تجميع ما مثل mkbitmap في WebAssembly. إنّ عدم تجميع تطبيق أو مكتبة عند التشغيل لأول مرة أمر طبيعي تمامًا، وهذا هو سبب عدم نجاح بعض الخطوات الموضّحة أدناه، لذلك اضطررتُ إلى التراجع عن العملية وإعادة المحاولة بشكل مختلف. لا تعرض المقالة أمر التجميع النهائي السحري كما لو كان قد سقط من السماء، بل تصف تقدمي الفعلي، بما في ذلك بعض الأمور المحبطة.

معلومات حول mkbitmap

يقرأ برنامج mkbitmap C الصورة ويطبّق عليها عملية واحدة أو أكثر من العمليات التالية، بهذا الترتيب: العكس، وفلترة التمريرات العالية، والتحجيم، ووضع الحدود. يمكن التحكّم في كل عملية وتفعيلها أو إيقافها بشكل فردي. ويتمثل الاستخدام الرئيسي لـ mkbitmap في تحويل صور الألوان أو التدرج الرمادي إلى تنسيق مناسب كإدخال لبرامج أخرى، لا سيما برنامج التتبُّع potrace الذي يشكّل أساس SVGcode. mkbitmap هو أداة للمعالجة المسبقة، وهو مفيد بشكل خاص في تحويل الرسومات الخطية التي تم مسحها ضوئيًا، مثل الرسوم المتحركة أو النصوص المكتوبة بخط اليد، إلى صور عالية الدقة ثنائية المستوى.

يمكنك استخدام mkbitmap من خلال تمرير عدد من الخيارات واسم ملف واحد أو عدة ملفات. للحصول على جميع التفاصيل، يُرجى الاطّلاع على صفحة الدليل للأداة:

$ mkbitmap [options] [filename...]
صورة كرتونية ملوّنة
الصورة الأصلية (المصدر).
تم تحويل صورة الرسوم المتحركة إلى تدرّج الرمادي بعد المعالجة المسبقة.
تم إجراء الترقية أولاً، ثم تطبيق الحد الأدنى: mkbitmap -f 2 -s 2 -t 0.48 (المصدر).

الحصول على الرمز‏

الخطوة الأولى هي الحصول على رمز المصدر mkbitmap. يمكنك العثور عليه على الموقع الإلكتروني للمشروع. في وقت كتابة هذا التقرير، كان الإصدار putrace-1.16.tar.gz هو الأحدث.

التجميع والتثبيت محليًا

الخطوة التالية هي تجميع الأداة وتثبيتها محليًا للتعرف على كيفية عملها. يحتوي ملف INSTALL على التعليمات التالية:

  1. cd إلى الدليل الذي يحتوي على رمز المصدر الخاص بالحزمة ونوعها ./configure لإعداد الحزمة لنظامك

    قد يستغرق تشغيل "configure" بعض الوقت. أثناء التشغيل، تطبع بعض الرسائل التي تخبر بها الميزات التي تتحقق منها.

  2. اكتب make لتجميع الحزمة.

  3. اختياريًا، اكتب make check لإجراء أي اختبارات ذاتية مثبّتة تلقائيًا على الحزمة، وذلك بشكل عام باستخدام البرامج الثنائية التي تم إلغاء تثبيتها والتي تم إنشاؤها حديثًا.

  4. اكتب make install لتثبيت البرامج وأي ملفات بيانات ووثائق. عند التثبيت في بادئة يملكها جذر، من أن يتم إعداد الحزمة وإنشاؤها كمستخدم عادي، والتنفيذ فقط بمرحلة make install باستخدام الامتيازات الجذر.

باتّباع هذه الخطوات، يُفترَض أن ينتهي بك الأمر إلى ملفَّين قابلَين للتنفيذ، وهما potrace وmkbitmap، والهدف الثاني هو محور هذه المقالة. يمكنك التحقق من أنه يعمل بشكل صحيح عن طريق تشغيل mkbitmap --version. في ما يلي ناتج جميع الخطوات الأربع من جهازي، والتي تم قطعها بشكل كبير للإيجاز:

الخطوة 1، ./configure:

 $ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands

الخطوة 2، make:

$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.

الخطوة 3، make check:

$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS:  8
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.

الخطوة 4، sudo make install:

$ sudo make install
Password:
Making install in src
 .././install-sh -c -d '/usr/local/bin'
  /bin/sh ../libtool   --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.

للتحقق من نجاح العملية، شغِّل "mkbitmap --version":

$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

إذا ظهرت لك تفاصيل الإصدار، يعني ذلك أنّه تم تجميع mkbitmap وتثبيته بنجاح. بعد ذلك، اجعل ما يعادل هذه الخطوات يعمل مع WebAssembly.

تجميع mkbitmap في WebAssembly

Emscripten هي أداة لتجميع برامج C/C++ في WebAssembly. تنص وثائق مشاريع البناء في Emscripten على ما يلي:

يُعد إنشاء مشاريع كبيرة باستخدام Emscripten أمرًا سهلاً للغاية. توفّر Emscripten نصَّين برمجيَّين بسيطَين لإعداد ملفات makefiles لاستخدام emcc كبديل عن تثبيت gcc، وفي معظم الحالات، لن يطرأ أي تغيير على بقية نظام الإصدار الحالي لمشروعك.

بعد ذلك، تنتقل الوثائق (مع تعديلها قليلاً للإيجاز):

ننصحك بأخذ الأوامر التالية في الاعتبار عند إنشاء الملفات عادةً باستخدام الأوامر التالية:

./configure
make

لإنشاء باستخدام Emscripten، يمكنك بدلاً من ذلك استخدام الأوامر التالية:

emconfigure ./configure
emmake make

وبالتالي، يصبح ./configure emconfigure ./configure في الأساس وmake emmake make. يوضح ما يلي كيفية إجراء ذلك باستخدام mkbitmap.

الخطوة 0، make clean:

$ make clean
Making clean in src
 rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo

الخطوة 1، emconfigure ./configure:

$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands

الخطوة 2، emmake make:

$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.

إذا سارت الأمور على ما يرام، من المفترض أن يكون هناك ملفات .wasm الآن في مكان ما في الدليل. يمكنك العثور عليها من خلال تشغيل find . -name "*.wasm":

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

يبدو أنّ الخيارين الأخيرين واعدين، لذا سيضم cd إلى دليل src/. يتوفّر الآن أيضًا ملفان جديدان مطابقان، هما mkbitmap وpotrace. لهذه المقالة، mkbitmap فقط هي ذات الصلة. إنّ حقيقة عدم توفّر الإضافة .js هي أمر محيّر بعض الشيء، لكنّها في الواقع ملفات JavaScript يمكن التحقّق منها من خلال طلب head سريع:

$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define   var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};

// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)

أعِد تسمية ملف JavaScript إلى mkbitmap.js من خلال طلب mv mkbitmap mkbitmap.jsmv potrace potrace.js على التوالي إذا أردت ذلك). حان الوقت الآن لإجراء الاختبار الأول لمعرفة ما إذا كان قد تم تنفيذه عن طريق تنفيذ الملف باستخدام Node.js على سطر الأوامر من خلال تشغيل node mkbitmap.js --version:

$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

لقد جمّعت mkbitmap بنجاح في WebAssembly. والآن الخطوة التالية هي جعله يعمل في المتصفح.

mkbitmap باستخدام WebAssembly في المتصفح

انسخ الملفين mkbitmap.js وmkbitmap.wasm إلى دليل جديد يُسمى mkbitmap وأنشئ ملف HTML النموذجي index.html الذي يحمِّل ملف JavaScript mkbitmap.js.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <script src="mkbitmap.js"></script>
  </body>
</html>

شغِّل خادمًا محليًا يعرض دليل mkbitmap وافتحه في متصفحك. من المفترض أن تظهر رسالة تطلب منك إدخال البيانات. وهذا ما نتوقّعه، لأنّه وفقًا لصفحة الأداة، "[i]إذا لم يتم تقديم أي وسيطات لاسم الملف، تعمل خريطة mkbitmap كفلتر، حيث تتم القراءة من الإدخال العادي"، علمًا أنّها تعتبر prompt() تلقائيًا بالنسبة إلى Emscripten.

تطبيق mkbitmap يعرض طلبًا يطلب إدخال البيانات.

منع التنفيذ التلقائي

لإيقاف تنفيذ mkbitmap على الفور وجعله ينتظر بيانات من المستخدم بدلاً من ذلك، عليك فهم كائن Module في Emscripten. Module هو كائن JavaScript عالمي يحتوي على سمات تستدعيها الرمز البرمجي الذي أنشأه Emscripten في مراحل مختلفة من تنفيذه. يمكنك توفير تنفيذ Module للتحكم في تنفيذ الرمز. عند بدء تشغيل تطبيق Emscripten، يفحص القيم في عنصر Module ويطبّقها.

في حالة mkbitmap، اضبط Module.noInitialRun على true لمنع التشغيل الأولي الذي أدى إلى ظهور الطلب. يمكنك إنشاء نص برمجي باسم script.js، وتضمينه قبل <script src="mkbitmap.js"></script> في index.html وإضافة الرمز التالي إلى script.js. وعند إعادة تحميل التطبيق الآن، من المفترض أن تختفي المطالبة.

var Module = {
  // Don't run main() at page load
  noInitialRun: true,
};

يمكنك إنشاء بنية وحدات مع المزيد من علامات الإنشاء

لإدخال إدخال إلى التطبيق، يمكنك استخدام دعم نظام الملفات في Emscripten باللغة Module.FS. ينص قسم تضمين دعم نظام الملفات في المستندات على ما يلي:

تقرّر Emscripten ما إذا كان سيتم تضمين دعم نظام الملفات تلقائيًا. لا تحتاج العديد من البرامج إلى ملفات، كما أن دعم نظام الملفات ليس ضئيلاً، ولذلك يتجنب Emscripten تضمينه عندما لا يرى سببًا. وهذا يعني أنّه إذا لم يصل رمز C/C++ إلى الملفات، لن يتم تضمين الكائن FS وواجهات برمجة التطبيقات الأخرى لنظام الملفات في الإخراج. ومن ناحية أخرى، إذا كان رمز C/C++ يستخدم الملفات، فسيتم تضمين دعم نظام الملفات تلقائيًا.

للأسف، mkbitmap هي إحدى الحالات التي لا يتضمن فيها Emscripten تلقائيًا دعم نظام الملفات، لذا يجب أن تخبره صراحةً بذلك. وهذا يعني أنه عليك اتباع الخطوتَين emconfigure وemmake الموضحتَين سابقًا، مع بعض العلامات الأخرى التي تم ضبطها عبر الوسيطة CFLAGS. قد تكون العلامات التالية مفيدة لمشروعات أخرى أيضًا.

وفي هذه الحالة بالذات، يجب أيضًا ضبط العلامة --host على wasm32 لإبلاغ النص البرمجي configure بأنّك تجمعه لاستخدام WebAssembly.

يبدو الأمر emconfigure الأخير على النحو التالي:

$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'

لا تنسَ تشغيل emmake make مرة أخرى ونسخ الملفات التي تم إنشاؤها حديثًا إلى مجلد mkbitmap.

يمكنك تعديل السمة index.html بحيث يتم تحميل وحدة ES (script.js) فقط، التي سيتم استيراد الوحدة mkbitmap.js منها بعد ذلك.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <!-- No longer load `mkbitmap.js` here -->
    <script src="script.js" type="module"></script>
  </body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  console.log(Module);
};

run();

عند فتح التطبيق الآن في المتصفِّح، من المفترض أن يظهر لك العنصر Module المسجَّل الدخول إلى وحدة تحكُّم أدوات مطوّري البرامج، وستتم إزالة الطلب لأنّ دالة main() في mkbitmap لم تعُد تستدعي في البداية.

تطبيق mkbitmap بشاشة بيضاء ويعرض عنصر الوحدة النمطي المسجّل في وحدة التحكّم في أدوات مطوّري البرامج

تنفيذ الدالة الرئيسية يدويًا

الخطوة التالية هي استدعاء دالة main() في mkbitmap يدويًا عن طريق تشغيل Module.callMain(). تستخدم الدالة callMain() مجموعة من الوسيطات التي تتطابق واحدة تلو الأخرى مع ما تنقله في سطر الأوامر. إذا قمت بتشغيل mkbitmap -v في سطر الأوامر، فستستدعي Module.callMain(['-v']) في المتصفح. يؤدي هذا الإجراء إلى تسجيل رقم إصدار mkbitmap في وحدة تحكّم "أدوات مطوّري البرامج".

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  Module.callMain(['-v']);
};

run();

تطبيق mkbitmap بشاشة بيضاء ويعرض رقم إصدار mkbitmap المسجّل في وحدة التحكّم في أدوات مطوّري البرامج

إعادة توجيه الإخراج العادي

الناتج العادي (stdout) هو وحدة التحكّم تلقائيًا. ومع ذلك، يمكنك إعادة توجيهها إلى شيء آخر، على سبيل المثال، دالة تخزن الإخراج إلى متغير. وهذا يعني أنّه يمكنك إضافة المخرجات إلى HTML من خلال إعداد السمة Module.print.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  let consoleOutput = 'Powered by ';
  const Module = await loadWASM({
    print: (text) => (consoleOutput += text),
  });
  Module.callMain(['-v']);
  document.body.textContent = consoleOutput;
};

run();

تطبيق mkbitmap يعرض رقم إصدار mkbitmap.

استرجاع ملف الإدخال إلى نظام ملفات الذاكرة

للحصول على ملف الإدخال في نظام ملفات الذاكرة، تحتاج إلى مكافئ mkbitmap filename في سطر الأوامر. لفهم كيفية تعاملي مع هذا الأمر، أريد أولاً بعض المعلومات الأساسية حول توقّع mkbitmap للمدخلات التي يقدّمها وإنشاء النتائج.

تنسيقات الإدخال المسموح بها باللغة mkbitmap هي PNM (PBM وPGM وPPM) وBMP. تنسيقات الإخراج هي PBM للصور النقطية وPGM للخرائط الرمادية. إذا تم توفير الوسيطة filename، ستنشئ mkbitmap تلقائيًا ملف إخراج يتم الحصول على اسمه من اسم ملف الإدخال من خلال تغيير اللاحقة إلى .pbm. على سبيل المثال، بالنسبة إلى اسم ملف الإدخال example.bmp، سيكون اسم ملف الإخراج example.pbm.

يوفر Emscripten نظام ملفات افتراضي يحاكي نظام الملفات المحلي، بحيث يمكن تجميع التعليمات البرمجية الأصلية التي تستخدم واجهات برمجة تطبيقات الملفات المتزامنة وتشغيلها مع تغيير بسيط أو بدون تغيير. لكي يقرأ mkbitmap ملف إدخال كما لو تم تمريره كوسيطة سطر أوامر filename، عليك استخدام الكائن FS الذي يوفّره Emscripten.

يعتمد الكائن FS على نظام ملفات في الذاكرة (يُشار إليه عادةً باسم MEMFS) ولديه وظيفة writeFile() تستخدمها لكتابة الملفات في نظام الملفات الافتراضي. يمكنك استخدام writeFile() كما هو موضَّح في نموذج الرمز البرمجي التالي.

للتحقّق من نجاح عملية كتابة الملف، شغِّل الدالة readdir() في العنصر FS باستخدام المَعلمة '/'. سيظهر لك example.bmp وعدد من الملفات التلقائية التي يتم إنشاؤها دائمًا تلقائيًا.

يُرجى العلم أنّه تمت إزالة المكالمة السابقة الموجَّهة إلى Module.callMain(['-v']) لطباعة رقم الإصدار. يرجع ذلك إلى أنّ Module.callMain() دالة يتوقع عادةً تشغيلها مرة واحدة فقط.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  console.log(Module.FS.readdir('/'));
};

run();

تطبيق mkbitmap يعرض مجموعة من الملفات في نظام ملفات الذاكرة، بما في ذلك example.bmp.

أول عملية تنفيذ فعلية

مع تنفيذ كل شيء في مكانه الصحيح، نفِّذ mkbitmap من خلال تشغيل Module.callMain(['example.bmp']). سجِّل محتوى مجلد '/' الخاص بـ MEMFS، ومن المفترض أن يظهر لك ملف إخراج example.pbm الذي تم إنشاؤه حديثًا بجانب ملف الإدخال example.bmp.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  console.log(Module.FS.readdir('/'));
};

run();

تطبيق mkbitmap يعرض مجموعة من الملفات في نظام ملفات الذاكرة، بما في ذلك example.bmp وexample.pbm.

إخراج ملف الإخراج من نظام ملفات الذاكرة

تتيح الدالة readFile() في الكائن FS إنشاء example.pbm في الخطوة الأخيرة من نظام ملفات الذاكرة. تعرض الدالة Uint8Array تحوله إلى كائن File وتحفظه على القرص، لأن المتصفحات لا تدعم بشكل عام ملفات PBM للعرض المباشر داخل المتصفح. (هناك طرق أكثر أناقة لحفظ ملف، ولكن استخدام <a download> الذي يتم إنشاؤه ديناميكيًا هو أكثر طريقة معتمدة على نطاق واسع). بعد حفظ الملف، يمكنك فتحه في عارض الصور المفضّل لديك.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
  const file = new File([output], 'example.pbm', {
    type: 'image/x-portable-bitmap',
  });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(file);
  a.download = file.name;
  a.click();
};

run();

تطبيق macOS Finder مع معاينة لملف .bmp للإدخال وملف المخرجات .pbm

إضافة واجهة مستخدم تفاعلية

حتى هذه النقطة، يتم ترميز ملف الإدخال بشكلٍ ثابت ويتم تشغيل mkbitmap باستخدام المعلَمات التلقائية. الخطوة الأخيرة هي السماح للمستخدم باختيار ملف إدخال ديناميكيًا وتعديل معلَمات mkbitmap، ثم تشغيل الأداة بالخيارات المحدَّدة.

// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);

ليس من الصعب تحليل تنسيق صورة PBM، ولذلك يمكنك استخدام بعض رموز JavaScript لعرض معاينة لصورة الإخراج. راجِع رمز المصدر الخاص بالعرض التوضيحي المضمّن أدناه للتعرّف على طريقة واحدة لإجراء ذلك.

الخلاصة

تهانينا، لقد جمعت mkbitmap بنجاح في WebAssembly، وجعلته يعمل في المتصفِّح. كانت هناك نهايات مسدودة وكان عليك تجميع الأداة أكثر من مرة حتى تنجح في استخدامها، ولكن كما كتبت أعلاه، هذا جزء من التجربة. يمكنك أيضًا تذكُّر علامة webassembly من StackOverflow إذا واجهتك مشكلة. استمتع بتجميع المحتوى الموسيقي!

شكر وتقدير

راجع هذه المقالة سام كليغ وراشيل أندرو.