عمل تطبيقي : الإظهار الطيفي للصوت
TP : visualisation spectrale du son
هذا العمل التطبيقي سيقترح عليكم التعامل مع الـSDL و الـFMOD في نفس الوقت. هذه المرّة، لن نعمل على لعبة. كما نعرف فالـSDL مخوصصة للألعاب لكن يمكن استعمالها في ميادين أخرى. سيقوم هذا الدرس بإثبات أنها صالحة لأجل أشياء أخرى.
سنحقق هنا إظهاراً للطيف الصوتي بالـSDL. يتوقّف هذا على إظهار تركيبة الصوت الذي نشغّله مثلاُ موسيقى. نجد هذه الخاصية في كثير من قارئي الأصوات. إنه أمرٌ ممتع و ليس بقدر الصعوبة التي يبدو عليها !
سيسمح لكم هذا الدرس بالعمل على مفاهيم قُمنا باستكشافها مؤخّراً :
-
التحكّم في الوقت.
-
المكتبة FMOD .
سنتعرّف علاوة على ذلك، على كيفية التعديل على مساحة بيكسل ببيكسل . الصورة التالية تعطيكم مظهراً للبرنامج الذي سنكتبه في هذا الدرس .

هو نوع الإظهار الذي نجدع في قارئي الأصوات كـWinamp، Windows Media Player أو AmaroK. كما قلتُ لكم إن الأمر ليس صعبٌ التحقيق. على عكس العمل التطبيقي الخاص بـMario Sokoban، هذه المرّة ستقومون بأنسفكم بالعمل. سيمثّل هذا بالنسبة إليكم تمريناً جيداً .
التعليمات :
التعليمات بسيطة. إتّبعوها خطوة بخطوة بالترتيب، و لن تواجهوا أي مشاكل .
1- قراءة ملف MP3 :
لكي تبدؤوا، يجب عليكم إنشاء برنامج يقوم بقراءة ملف MP3. ليس عليكم سوى إعادة الأغنية Home للمجموعة Hype و التي استعملناها في الدرس الخاص بـFMOD لتلخيص كيفية عمل تشغيل الموسيقى.
إذا اتّبعتم جيّدا الدرس حول FMOD، لا تحتاجون أكثر من بعضة دقائق لكي تقوموا بالعملية . أنصحكم بالمناسبة أن تقوموا بنقل الملف MP3 إلى مجلّد المشروع.
2- إسترجاع المعلومات الطيفية للصوت :
لكي نعرف كيف يعمل الإظهار الطيفي للصوت، من الواجب أن أشرح لكم كيفية العمل داخلياً ( بشكل تقريبي فقط و إلا سندخل في درس رياضيات ) .
يمكن أن يتم تقسيم الصوت إلى ترددات frequences. بعض الترددات منخفضة، بعضها متوسطة و بعضها مرتفعة . ما سنقوم به في عملية الإظهار هو إظهار كمية كلّ واحدة من الترددات على شكل شرائط و كلّ ما يكون الشريط كبيراً، كلما يكون التردد مستعملاً أكثر ( الصورة التالية ) :

على يسار النافذة، نقوم بإظهار الترددات المنخفضة، و على اليمين الترددات المرتفعة.
لكن كيف نسترجع كميّة كلّ تردد ؟
ستهتم FMOD بهذا العمل. يمكننا استدعاء الدالة FMOD_Channel_GetSpectrum ذات النموذج :
FMOD_RESULT FMOD_Channel_GetSpectrum(
FMOD_CHANNEL * channel,
float * spectrumarray,
int numvalues,
int channeloffset,
FMOD_DSP_FFT_WINDOW windowtype
);
و هاهي الخاصيات التي تحتاجها الدالة :
-
القناة التي تشتغل فيها الموسيقى. يجب إذا استرجاع مؤشّر نحو هذه القناة .
-
جدول float. يجب أن يتم حجز الذاكرة من أجل هذا الجدول مسبّقاً، بشكل ثابت أو ديناميكي، لكي نسمح لـFMOD بملئه اوتوماتيكياً.
-
حجم الجدول. يجب أن يكون حجم الجدول إجبارياً عبارة عن قوّة للرقم 2، مثلا 512 .
-
تسمح هذه الخاصية بتعريف بأي مخرج نحن مهتمون. مثلاً لو أننا في stéréo، تعني القيمة 0 اليسار و القيمة 1 اليمين.
-
هذه الخاصية معقّدة قليلاً، و لا تهمّنا حقيقة في هذا الدرس. سنكتفي بإعطائها القيمة FMOD_DSP_FFT_WINDOW_RECT.
تذكير : النمط float هو نمط عشري، بنفس مستوى double. الإختلاف بين الإثنين يكمن في كون الـdouble أكثر دقّة من الآخر، لكن في حالتنا يكفينا الـfloat. هذا الأخير مستعمل من طرف FMOD هنا. و لذلك، هو ما سنستعمله نحن أيضاً .
بشكل واضح، نعرّف جدول الـfloat :
ثم، حين يتم تشغيل الموسيقى، نطلب من FMOD بملئ جدول الأطياف بالقيام مثلاً بـ:
FMOD_Channel_GetSpectrum(canal, spectre, 512, 0, FMOD_DSP_FFT_WINDOW_RECT);
يمكننا بعد ذلك تصفّح الجدول لكي نتحصّل على قيم الأطياف :
spectre[0] // Fréquence la plus basse (à gauche)
spectre[1]
spectre[2]
...
spectre[509]
spectre[510]
spectre[511] // Fréquence la plus haute (à droite)
كلّ تردد هو عبارة عن عدد عشري محصور بين 0 (لا شيء) و 1 (قيمة قصوى). ينصّ عملكم على إظهار كلّ شريط سواء كان قصيراً أو كبيراً بدلالة القيمة التي تحتويها كلّ من خانات الجدول.
مثلاً، إذا كانت القيمة هي 0.5 يجدر بكم رسم شريط يكون علّوه مساوياً لنصف علوّ النافذة. إذا كانت القيمة هي 1، فسيأخذ الشريط كلّ علو النافذة.
بشكل عام، تكون القيم ضعيفة ( أكثر قرباً من الصفر على الواحد ). أنصحكم بضرب كلّ القيم بالعدد 20 لكي تروا الطيف بشكل أفضل.
إحذروا : إذا قمتم بهذا، تأكدوا بأنكم لن تتجاوزوا القيمة 1 ( قوموا بتدوير القيمة إلى 1 إذا احجتم إذا ذلك ). إذا وجدتم أنكم تتعاملون مع أعداد تفوق الواحد، ستظهر لكم مشاكل لاحقاً في إظهار الطيف.
لكن يجدر بالشرائط أن تتحرّك في نفس الوقت الذي يتم فيه تشغيل الصوت، أليس كذلك؟ بما أن الصوت يتحرّك كلّ الوقت، يجب تحديث الصورة الرسومية، ما العمل؟
سؤال جيد. في الواقع، الجدول الخاص 512 float الذي ترجعه لنا FMOD يتغيّر كل 25 مث ( لكي نكون في نفس الفصل الزمني بالنسبة للصوت الحالي ). يجب إذا في الكود المصدري أن تعيدوا قراءة جدول الـ512 float ( بإعادة استدعاء FMOD_Channel_GetSpectrum كلّ 25 مث )، ثم تقوموا بتحديث الصورة الرسومية ذات الشرائط . أعيدوا قراءة الدرس حول التحكّم في الوقت بالـSDL لكي تتذكّروا كيفية عمل ذلك. لديكم الخيار بين GetTicks و callbacks. استعملوا ما تروه أكثر سهولة .
3- تحقيق التدرّج اللوني:
في محاولات أولى، يمكنكم تحقيق الشرائط بلون موحّد. يمكنكم إذا إنشاء مساحات. يجب إذا أن تكون هناك 512 مساحة : واحدة من أجل كلّ شريط. كلّ مساحة تأخذ إذا بيكسل واحد كعُرض. و يختلف علوّ الشرائط بدلالة كثافة كلّ تردد.
أنصحكم كما في كلّ مرة، أن تقوموا ببعض التحسينات : يجب على الشريط أن يميل للأحمركلّما زادت كثافة الصوت. أي أنه على الشريط ان يكون أخضراً من الأسفل و أحمراً من الأعلى.
لكن... المساحة الواحدة لا يمكنها أن تأخذ سوى لوناً واحداً لو أننا نستعمل الدالة SDL_FillRect. إذا لا يمكننا إنشاء تدرّح لوني!
في الواقع، يمكننا بالتأكيد إنشاء مساحات بعًرض 1 بيكسل و علو 1 بيكسل من أجل كلّ لون في التدرّج. لكن هذا سيأخذ بنا إلى أنشاء مساحات عديدة و لن يكون التحكّم فيها مثالياً !
كيف يمكن لنا أن نرسم بيكسل ببيكسل؟
لم أعلّمكم هذا من قبل، هذه التقنية لا تستحقّ درساً كاملاً. ستجدون أنها في الواقع ليست صعبة. في الواقع، لا تقترح الـSDL أية دالة للرسم بيكسل ببيكسل. لكن لنا الحق في أن نكتبها بأنفسنا. لكي نقوم بهذا، يجب إتّباع هذه الخطوات النموذجية بالترتيب :
-
استدعوا الدالة SDL_LockSurfacepour لنعلن للـSDL أننا سنقوم بالتعديل على المساحة يدوياً. هذا "يعطّل" المساحة للـSDL و ستكونون وحدكم قادرين على التحكّم فيها مادامت المساحة معطّلة.
هنا، انصحكم بأن تعملوا بمساحى واحدة فقط : الشاشة. إذا أردتم رسم بيكسل في منطقة محددة من الشاشة، يجب عليكم تعطيل المساحة ecran:
-
يمكنكم بعد ذلك تغيير محتوى كلّ بيكسل من المساحة. بما أن الـSDL لا تقترح أية دالة للقيام بهذا، يجب أن نكتبها بأنفسنا في البرنامج.
سأعطيكم هذه الدالة، و التي استهرجتها من الملفات التوجيهية للـSDL. هي معقدّة أكثر لأنها تعمل على المساحة مباشرة و تتحكم في كلّ أعماق اللون الممكنة ( بيت بالبيكسل ). لا تحتاجون لحفظها أو فهمها، قوموا بنسخها ببساطة في البرنامج لكي تتمكّنوا من استعمالها :
void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
{
int bpp = surface->format->BytesPerPixel;
Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
switch(bpp) {
case 1:
*p = pixel;
break;
case 2:
*(Uint16 *)p = pixel;
break;
case 3:
if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
p[0] = (pixel >> 16) & 0xff;
p[1] = (pixel >> 8) & 0xff;
p[2] = pixel & 0xff;
} else {
p[0] = pixel & 0xff;
p[1] = (pixel >> 8) & 0xff;
p[2] = (pixel >> 16) & 0xff;
}
break;
case 4:
*(Uint32 *)p = pixel;
break;
}
}
هي سهلة الإستعمال, ابعثوا لها الخاصيات التالية :
* المؤشّر نحو المساحة التي تريدون التعديل عليها ( يجب أن تكون معطّلة من طرف SDL_LockSurface )
* وضعية الفاصلة الخاصة بالبيكسل الذي نريد التعديل عليه في المساحة (x) .
* وضعية الترتيبة الخاصة بالبيكسل الذي نريد التعديل عليه في المساحة (y) .
* اللون الجديد الذي نعطيه للبيكسل. يجب أن يكون هذا اللون بصيغة Uint32 يمكنكم إذا توليده بالإستعانة بالدالة SDL_MapRGB التي تتقنونها جيداًُ الآن.
-
أخيراً، حينما تنتهون من العمل على المساحة، يجب ألا تنسوا أن تزيلوا تعطيلها باستدعاء SDL_UnlockSurface .
SDL_UnlockSurface(ecran);
كود ملخّص للمثال :
لو نلخّص، ستجدون بأن كلّ شيء سهل. هذا الكود يرسم بيكسل أحمر في منتصف المساحة ecran ( أي في منتصف النافذة ).
SDL_LockSurface(ecran);
setPixel(ecran, ecran->w / 2, ecran->h / 2, SDL_MapRGB(ecran->format, 255, 0, 0));
SDL_UnlockSurface(ecran);
بهذه الإنطلاقة، يجدر بكم أن تتمكنوا من تحقيق التدرّج اللون من الأخضر للأحمر ( يجب أن تستعملوا الحلقات التكرارية ) .
التصحيح :
إذا، كيف وجدتم الموضوع؟ ليس صعب الفهم، يجب فقط القيام ببعض الحسابات، خاصة من أجل تحقيق التدرّج اللوني. مستوى التمرين هو مستوى عام، يجب فقط أن تفكّروا أكثر. بعض الأشخاص ياخذون وقتاً أطول من آخرين لإيجاد التصحيح. إذا لم تتمكّنوا من حلّ التمرين، هذا ليس سيئاً. ما يهمّ هو أن ننتهي بالوصول إلى هدفنا. مهما كان المشروع الذي تعملون عليه، يتكون هناك بالتأكيد اوقات نجد فيها أنه لا ينقصنا أن نجيد البرمجة لكي نتمكّن من حلّ المشكل، يجب أيضاً أن نكون منطقيين و نجيد التفكير . سأعطيكم الكود المصدري الكامل أسفله. لقد علّقت عليه بشكل كافي :
#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <fmodex/fmod.h>
#define LARGEUR_FENETRE 512
#define HAUTEUR_FENETRE 400
#define RATIO (HAUTEUR_FENETRE / 255.0)
#define DELAI_RAFRAICHISSEMENT 25
#define TAILLE_SPECTRE 512
void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel);
int main(int argc, char *argv[])
{
SDL_Surface *ecran = NULL;
SDL_Event event;
int continuer = 1, hauteurBarre = 0, tempsActuel = 0, tempsPrecedent = 0, i = 0, j = 0;
float spectre[TAILLE_SPECTRE];
FMOD_SYSTEM *system;
FMOD_SOUND *musique;
FMOD_CHANNEL *canal;
FMOD_RESULT resultat;
FMOD_System_Create(&system);
FMOD_System_Init(system, 1, FMOD_INIT_NORMAL, NULL);
resultat = FMOD_System_CreateSound(system, "hype_home.mp3",
FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM, 0, &musique);
if (resultat != FMOD_OK)
{
fprintf(stderr, "Impossible de lire le fichier mp3\n");
exit(EXIT_FAILURE);
}
FMOD_System_PlaySound(system, FMOD_CHANNEL_FREE, musique, 0, NULL);
FMOD_System_GetChannel(system, 0, &canal);
SDL_Init(SDL_INIT_VIDEO);
ecran = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32, SDL_SWSURFACE | SDL_DOUBLEBUF);
SDL_WM_SetCaption("Visualisation spectrale du son", NULL);
while (continuer)
{
SDL_PollEvent(&event); // On doit utiliser PollEvent car il ne faut pas attendre d'évènement de l'utilisateur pour mettre à jour la fenêtre
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
}
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));
tempsActuel = SDL_GetTicks();
if (tempsActuel - tempsPrecedent < DELAI_RAFRAICHISSEMENT)
{
SDL_Delay(DELAI_RAFRAICHISSEMENT - (tempsActuel - tempsPrecedent));
}
tempsPrecedent = SDL_GetTicks();
FMOD_Channel_GetSpectrum(canal, spectre, TAILLE_SPECTRE, 0,
FMOD_DSP_FFT_WINDOW_RECT);
SDL_LockSurface(ecran);
for (i = 0 ; i < LARGEUR_FENETRE ; i++)
{
hauteurBarre = spectre[i] * 20 * HAUTEUR_FENETRE;
if (hauteurBarre > HAUTEUR_FENETRE)
hauteurBarre = HAUTEUR_FENETRE;
for (j = HAUTEUR_FENETRE - hauteurBarre ; j <HAUTEUR_FENETRE ; j++)
{
setPixel(ecran, i, j, SDL_MapRGB(ecran->format, 255 - (j / RATIO), j / RATIO, 0));
}
}
SDL_UnlockSurface(ecran);
SDL_Flip(ecran);
}
FMOD_Sound_Release(musique);
FMOD_System_Close(system);
FMOD_System_Release(system);
SDL_Quit();
return EXIT_SUCCESS;
}
void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
{
int bpp = surface->format->BytesPerPixel;
Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
switch(bpp) {
case 1:
*p = pixel;
break;
case 2:
*(Uint16 *)p = pixel;
break;
case 3:
if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
p[0] = (pixel >> 16) & 0xff;
p[1] = (pixel >> 8) & 0xff;
p[2] = pixel & 0xff;
} else {
p[0] = pixel & 0xff;
p[1] = (pixel >> 8) & 0xff;
p[2] = (pixel >> 16) & 0xff;
}
break;
case 4:
*(Uint32 *)p = pixel;
break;
}
}
يجدر بكم أن تتحصّلوا على النتيجة التالية :

من المعلوم أن النتيجة المرئية أفضل لتقدير النتيجة، أنصحكم بالإطلاع عليها من هنا :
مشاهدة نتيجة إظهار الطيف الصوتي
لاحظوا أن ضغط الملف أنقص من جودة الصوت و عدد الصور في الثانية.
الأفضل هو أن تقوموا بتحميل البرنامج كاملاً ( مرفقاً بالكود المصدري ) لكي تجربوه عندكم. يمكنكم حينها تقدير البرنامج في ظروف أفضل.
تحميل البرنامج و الكود المصدري كاملين
يجب قطعاً أن يكون الملف Hype_Home.mp3 متواجداً في مجلّد المشروع لكي يشتغل البرنامج ( و إلا فسيتوقف حالاً ).
أفكار للتحسين :
يمكن دائما تحسين البرنامج. هنا، لدي مثلاً أفكار تمديد كثيرة يمكنها أن تصل بكم إلى إنشاء برنامج صغير لقراءة الملفات MP3 .
-
سيكون من الجيد أن نختار بأنفسنا الملف MP3 الذي نريد قراءته. يمكن مثلاً أن نقدّم لائحة تضم كلّ الملفات بذات الصيغة و المتواجدة في مجلّد المشروع. لم نرَ كيف نقوم بذلك، لكن يمكنكم وحدكم أن تكتشفوا ذلك. كتعليمة : استعملوا المكتبة dirent ( قوموا بتضمين الملف dirent.h ). قوموا بالبحث في الأنترنت عن طرق العمل بها.
-
إذا كان البرنامج قادر على التحكّم في لائحات الأغاني المشغّلة، سيكون أمراً أفضل. توجد كثير من صيغ اللائحات playlist و أشهرها الصيغة M3U ( يمكنكم إظهار عنوان الموسيقى التي انتم بصدد تشغيلها في النافذة مثلاً ( يجب استعمال SDL_ttf ) .
-
يمكنكم إظهار مؤشّر يشير إلى أي مرحلة من الأغنية وصل التشغيل، هذا ما يفعله أغلب قارئي الـMP3.
-
يمكنكم أيضاً أن تقترحوا التعديل على قوة الصوت .
-
إلى آخره ...
باختصار، هناك الكثير لفعله. لديكم إحتمال التمكّن من إنشاء قارئي أصوات ممتازة، ليس عليكم سوى كتابة الكود الخاص بها.