إنشاء مسجل صوتي في C و C#

This article is available in English too, check it out here.

الكود: SMPREC – C.zip
الكود:
SimpleRec – C#.zip

المحتويات

محتويات هذا الدرس:

  • المحتويات
  • نظرة خاطفة
  • مقدمة
  • أوامر MCI
  • خيارات الانتظار، الإعلام، والتجربة
  • معالجة الأخطاء
  • التسجيل Recording
    • التجهيز Opening
    • بدأ التسجيل Recording
    • الإيقاف المؤقت Pausing
    • الاستمرار Resuming
    • الإيقاف Stopping
    • الحالة Status
    • الحفظ Saving
    • الإنهاء Closing
  • التشغيل Playing
    • التحميل Loading
    • بدأ التشغيل Playing
    • الإيقاف المؤقت Pausing
    • الاستمرار Resuming
    • معرفة المكان الحالي
    • الانتقال إلى مكان معين Seeking
    • الإنهاء Closing
  • الخلاصة
  • MCI والدوت نت
  • ملحق 1: تحديد خصائص الـ Device [جديد]
  • الأمثلة

نظرة خاطفة

درسنا في هذا اليوم يتكلم عن كيفية تسجيل الصوت أو بالأصح إنشاء مسجل صوتي في C و C#. سنبدأ أولا بشرح الأفكار والدوال التي سنحتاجها وكيفية برمجتها في لغة C ثم بعد ذلك سنتبعها بكود الدوت نت و C#.

بالإضافة إلى ذلك، فالدرس لا يتكلم فقط عن كيفية تسجيل الصوت، بل عن كيفية تشغيل الصوت أيضا والتحكم فيه.

أيضا يركز هذا الدرس على تقنية MCI أو Media Control Interface حيث هي الأسهل والأشهر والأقوى.

يمكنك أيضا تحميل مثالين برنامجين تسجيل صوت أحدهما بالـ C والآخر بالـ C#.

مقدمة

هناك العديد من التقنيات أو الطرق التي تستطيع من خلالها التعامل مع الصوتيات أو الـ Multimedia بصفة عامة. ومن أهم هذه التقنيات:

  • DirectX:
    يوفر DirectX مكتبات جاهزة سهلة الاستخدام للتحكم في الصوتيات والـ Multimedia. ويمكنك إضافة هذه المكتبات إلى مشروعك وبدأ العمل مباشرة.
  • Waveform:
    وهذه التقنية توفر إمكانيات متقدمة جدا للتعامل مع الصوتيات مثل معالجة بيانات الصوت وغيرها. وهذه التقنية متوفرة من خلال دوال الويندوز Windows API.
  • MCI (Media Control Interface):
    واجهة التحكم في الوسائط المتعدد MCI. وهذه التقنية هي الأسهل والأشهر والأقوى، وهي التي نركز عليها في درسنا اليوم.

للأسف لا توفر بيئة الدوت نت طريقة للتعامل مع الصوتيات أبدا أو مع أي من هذه التقنيات المذكورة. ومع ذلك يمكنك الحصول على مكتبات جاهزة للدوت نت من خلال المؤسسات المعروفة والمشهورة التي توفر لك هذه الخدمة.

لهذا، فإننا في درسنا اليوم سنحتاج إلى التعامل مع دوال الويندوز Windows API حيث أن تقنية MCI متوفرة من خلال هذا المصدر من خلال المكتبة WinMM.dll.

بالنسبة للغة C سوف نقوم بالتعامل مع هذه الدوال مباشرة. فقط يجب عليك إضافة ملف المكتبة WinMM.lib إلى مشروعك. أما لغة C# فإننا سوف نحتاج إلى تعريف الدوال Platform Invokation في الكود كي نستطيع من التعامل معها، فإنك لا يمكنك التعامل مع الـ Windows API من خلال الدوت نت مباشرة. وهذا ما سنتعلمه في هذا الدرس.

تقنية MCI أو Media Control Interface هي أحد التقنية عالية الجودة سهلة الاستخدام التي توفر إمكانيات التحكم في جميع أنواع الوسائط المتعددة Multimedia (من صوتيات ومرئيات، ملفات، أو CDs، وما إلى غير ذلك.)

ويمكنك التحكم في تقنية MCI باستخدام طريقتين:

  • أوامر Commands:
    لكل أمر رمزه الخاص. فهناك أمر خاص بالتسجيل، وأمر خاص بإيقاف التسجيل، وأمر خاص بالحفظ ونحوها. نلاحظ أن هذه الرموز لها أسماؤها الخاصة بها. فمثلا أمر التشغيل له الاسم MCI_PLAY والإيقاف له الإسم MCI_STOP فهذه الأوامر كلها عبارة عن ثوابت Constants.
  • رسائل نصية String Messages:
    لكل أمر اسمه الخاص مثل play و stop و pause وما إلى غير ذلك. وهذه الطريقة هي الأشهر في لغات السكربت Scripting Languages مثل VBScript. ولهذا فإننا سوف نتعرض في هذا الدرس إلى الأوامر الرقمية.

والآن إلى الكود. سوف نتعرف على أهم الدوال وأهم العناصر Objects التي سنحتاجها.

أوامر MCI

بما أننا لدينا طريقتين لإرسال الأوامر، واحدة لإرسال الأوامر الرقمية Commands، والأخرى لإرسال أوامر نصية Messages، فإننا أيضا لدينا دالتين، واحدة لإرسال الأوامر الرقمية وهي mciSendCommand()، والأخرى لإرسال الرسائل النصية وهي mciSendMessage()، وفي درسنا هذا سنتعرض للأولى.

تقوم الدالة mciSendCommand() بإرسال أوامر إلى تقنية أو محرك MCI، وهذه الدالة لها التعريف التالي:

MCIERROR mciSendCommand(
	MCIDEVICEID IDDevice,
	UINT        uMsg,
	DWORD       fdwCommand,
	DWORD_PTR   dwParam
);

تأخذ هذه الدالة أربعة مدخلات:

  • IDDevice:
    كود كارت الصوت الذي تريد التعامل معه. بالطبع هناك بعض الأجهزة التي لديها أكثر من كارت صوت Sound Card، فيمكنك من خلال هذا المدخل تحديد كارت الصوت الذي تريد التعامل معه، أو تحدد 0 للتعامل مع الكارت الرئيسي الذي يستخدمه الويندوز.
  • uMsg:
    كود أو رقم الأمر الذي تود تنفيذه. سوف تتعرض للأوامر تدريجيا أثناء قراءتك لهذا الدرس.
  • fdwCommand:
    الخيارات Flags الخاصة بهذا الأمر. كل أمر له خياراته الخاصة به، ولكن جميع الأوامر تشترك في خيارات ثلاثة وهي الانتظار Wait، الإعلام Notify، والتجربة Test. سوف نتعرض لهذا الخيارات لاحقا.
  • dwParam:
    المدخلات أو البيانات الخاصة بالأمر.

إذا فكل أمر له اسم، مدخلات أو بيانات، وله خيارات.

تقوم هذه الدالة بإرجاع 0 في حالة النجاح أو رقم الخطأ في حالة الفشل.

خيارات الانتظار، الإعلام، والتجربة

كل أمر له خياراته Flags الخاصة به، ولكن هناك 3 خيارات تنطبق على الجميع:

  • الانتظار MCI_WAIT:
    فعند إرسال الأمر باستخدام الدالة mciSendCommand() تقوم الدالة بالانتظار حتى يتم تنفيذ الأمر كاملا ثم الرجوع إلى المستخدم، وهذا ما نسميه عملية متزامنة Synchronous Operation. يفضل تنفيذ هذا الوضع أو الاختيار في حالة الأوامر مثل Stop و Pause حيث أنه لا يؤثر على الكود الخاص بك.
  • الإعلام MCI_NOTIFY:
    فعند إرسال الأمر باستخدام الدالة mciSendCommand() تقوم الدالة بالرجوع فورا وينفذ الأمر في الخلفية وتقوم تقنية MCI بإرسال رسالة الويندوز MM_MCINOTIFY إلى البرنامج فور انتهائها وتحتوي هذه الرسالة على نتيجة الأمر هل نجح تنفيذه أم حدث خطأ، وهذا ما نسميه عملية غير متزامنه Asynchronous Operation. يفذل تنفيذ هذا الوع في حالة الأوامر مثل Record و Play حتى لا يقوم برنامج بالتوقف تماما عن العمل حتى انتهاء علمية التسجيل أو انتهاء الملف.
  • التجربة MCI_TEST:
    فعند إرسال الأمر باستخدام الدالة mciSendCommand() تقوم الدالة بالتأكد هل يمكن تنفيذ الأمر أم لا ولا تقوم بتنفيذه. فمثلا عند محاولة التوقف Stop تقوم الدالة بالتأكد هل هناك عملية تسجيل أو تشغيل أو لا، فإذا كانت هناك عملية يمكن التوقف منها تقوم الدالة بإرجاع 0 وإلا فتقوم بإرجاع كود للخطأ. وفي كل الأحوال لا ينفذ الأمر.

معالجة الأخطاء

كل أمر يمكن أن ينفذ بطريقة صحيح ويمكن أن يحدث خطأ أثناء تنفيذه. فإذا نفذ بطريقة صحيحة ترجع الدالة mciSendCommand() قيمة 0، وإلا فترجع رقم الخطأ. فمثلا عند محاولتك تشغيل ملف غير موجود يحدث خطأ وترجع الدالة كود هذا الخطأ والممثل بالثابت MCIERR_FILE_NOT_FOUND.

ومن خلال رقم الخطأ هذا نستطيع معرفة الوصف Description للخطأ عن طريق الدالة mciGetErrorString() والتي لها التعريف التالي:

BOOL mciGetErrorString(
    DWORD fdwError,
    LPTSTR lpszErrorText,
    UINT cchErrorText
);

تأخذ الدالة رقم الخطأ وترجع نص الخطأ من خلال المدخل الثاني.

كود الـ C التالي يوضح كيفية إرجاع الوصف لخطأ معين:

TCHAR szBuffer[256];
DWORD dwErr;

dwErr = mciSendCommand(/* arguments */);

mciGetErrorStringW(dwErr, szBuffer, 256);
// cchErrorText =
//     sizeof(szBuffer) / sizeof(szBuffer[0]);

التسجيل Recording

كي تتم عملية التسجيل بالشكل الصحيح يجب عليك اتباع الخطوات التالية:

  1. تجهيز Open كارت الصوت للعمل (للإدخال Input.)
  2. بدأ عملية التسجيل Recording.
  3. إيقاف Stopping عملية التسجيل عند الانتهاء.
  4. حفظ Saving ما تم تسجيله في ملف صوتي.
  5. إنهاء Closing التعامل مع كارت الصوت ومع MCI.

بالطبع يجب عليك التأكد بين كل عملية وأخرى ما إذا كانت العملية السابقة تمت بنجاح أو حدث خطأ.

التجهيز Opening

وهي عملية فتح أو تجهيز كارت الصوت للعمل مع برنامجك وتتم عن طريق الأمر MCI_OPEN. وكما تعلم لكل أمر مدخلات وخيارات خاصة به. ومدخلات هذا الأمر هو عنصر Structure من نوع MCI_OPEN_PARMS. وهذا العنصر له التعريف التالي:

typedef struct {
    DWORD_PTR    dwCallback;
    MCIDEVICEID  wDeviceID;
    LPCSTR       lpstrDeviceType;
    LPCSTR       lpstrElementName;
    LPCSTR       lpstrAlias;
} MCI_OPEN_PARMS;

ما يهمنا في هذا التركيب هو العنصر الثالث والرابع، lpstrDeviceType و lpstrElementName. فالعنصر lpstrDeviceType يحدد نوع البيانات التي نريد التعامل معها مثلا بيانات صوتية waveaudio أو فيديو.

أما العنصر lpstrElementName فيحوي نصا فارغا في حالة التجهيز لعملية التسجيل أو عنوان ملف في حالة عملية التشغيل Play.

يجب عند استخدام هذا الأمر تحديد الخيارين MCI_OPEN_ELEMENT و MCI_OPEN_TYPE ويفضل بالطبع تحديد وضع الانتظار MCI_WAIT.

عند تنفيذ هذا الأمر ورجوع الدالة mciSendCommand() يكون العنصر dwCallbck يحوي كود كارت الصوت الذي تم استخدامه.

كود الـ C التالي يوضح كيفية فتح كارت الصوت وتجهيزه لعملية التسجيل:

MCI_OPEN_PARMS parmOpen;
WORD wDeviceID;

parmOpen.dwCallback       = 0;
parmOpen.wDeviceID        = 0;	// the default device
parmOpen.lpstrDeviceType  = TEXT("waveaudio");
parmOpen.lpstrElementName = TEXT("");
parmOpen.lpstrAlias       = 0;

mciSendCommand(0, // the default device
    MCI_OPEN,
    MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
    (DWORD)&parmOpen);

// Keep the device ID for future calls
wDeviceID = parmOpen.wDeviceID;

بدأ التسجيل Recording

بعد فتح الجهاز وتجهيزه لعملية التسجيل يمكنك الآن بدأ التسجيل ويتم ذلك عن طريق الأمر MCI_RECORD والذي يأخذ مدخلات من نوع MCI_RECORD_PARMS والتي لها التعريف التالي:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwFrom;
    DWORD     dwTo;
} MCI_RECORD_PARMS;

يحوي هذا التركيب Structure العناصر التالية:

  • dwCallback:
    مقبض النافذة Handle التي سوف ترسل إليها الرسالة MM_MCINOTIFY في حالة استخدام وضع الإعلام MCI_NOTIFY.
  • dwFrom:
    يمكنك من التسجيل من ثانية معينة مثلا 0 وتعني الأولى. في حالة تغيير القيمة من 0 إلى قيمة أخرى استخدم الخيار MCI_FROM.
  • dwTo:
    يمكنك من تحديد زمن انتهاء التسجيل، وهذا الزمن يكون مضروب عدد الثواني في 1000. فمثلا للتسجيل لمدة دقيقة نقوم بتحديد القيمة 0 للعنصر dwFrom والقيمة 60000 في العنصر dwTo. أما إذا أردتها مفتوحة فيمكنك تحديد القيمة 0 في العنصرين. في حالة تغيير القيمة من 0 إلى قيمة أخرى استخدم الخيار MCI_TO.

يفضل استخدام الوضع MCI_NOTIFY في هذا الأمر.

لاحظ كود C التالي والذي يقوم ببدأ عملية التسجيل لفترة مفتوحة:

MCI_RECORD_PARMS parmRec;

parmRec.dwCallback = 0;
parmRec.dwFrom	   = 0;
parmRec.dwTo	   = 0;

// We do not need a notification message
// we will send a Stop command, when
// we finish.
mciSendCommand(wDeviceID, MCI_RECORD, 0, (DWORD)&parmRec);

وهذا الكود يقوم بالتسجيل لفترة معينة:

MCI_RECORD_PARMS parmRec;

parmRec.dwCallback = hWnd;	// window handle
parmRec.dwFrom	   = 0;
parmRec.dwTo	   = 30000; // 30 seconds

// Notify me when you finish the 30 seconds
mciSendCommand(wDeviceID, MCI_RECORD,
    MCI_NOTIFY | MCI_TO, (DWORD)&parmRec);

الإيقاف المؤقت Pausing

ويتم هذا عن طريق الأمر MCI_PAUSE والذي يأخذ مدخلات من نوع MCI_GENERIC_PARMS:

typedef struct {
    DWORD_PTR dwCallback;
} MCI_GENERIC_PARMS;

الكود التالي يوضح كيفية عمل إيقاف مؤقت Pause لعملية التسجيل:

MCI_GENERIC_PARMS parmGen;

parmGen.dwCallback = 0;

mciSendCommand(wDeviceID, MCI_PAUSE, MCI_WAIT, (DWORD)&parmGen);

الاستمرار Resuming

الاستمرار Resuming من عملية التسجيل ويتم عن طريق الأمر MCI_RESUME والذي يأخذ أيضا العنصر MCI_GENERIC_PARMS كمدخلات مثل نظيره MCI_PAUSE.

الإيقاف Stopping

أي أيقاف Stop عملية التسجيل تمام. ويتم ذلك عن طريق الأمر MCI_STOP والذي يأخذ أيضا العنصر MCI_GENERIC_PARMS كمدخلات.

الحالة Status

كيف يمكننا معرفة الزمن الذي تم تسجيله؟ كيف يمكننا معرفة العملية التي يقوم MCI بتنفيذها الآن؟ يتم ذلك عن طريق الأمر MCI_STATUS وهو المسؤول عن الاستعلامات ويأخذ مدخلات من نوع MCI_STATUS_PARMS وهي معرفة كالتالي:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwReturn;
    DWORD     dwItem;
    DWORD     dwTrack;
} MCI_STATUS_PARMS;

بالطبع العنصر الأول dwCallback معروف وقد تم ذكره. أما العناصر الأخرى فيهمنا منها التالي:

  • dwReturn:
    القيمة المرتجعة الناتجة من الاستعلام.
  • dwItem:
    عبارة عن البيانات التي نريد الاستعلام عنها.

ويأخذ هذا الأمر خيارات من نوع MCI_WAIT و MCI_STATUS_ITEM.

والبيانات التي يمكن الاستعلام عنها والتي تحدد في dwItem هي:

  • MCI_STATUS_LENGTH:
    طول الملف الذي تم تسجيله أو الذي تم تشغيله. ترجع هذه القيمة في العنصر dwReturn وهي عبارة عن مضروب عدد الثواني في 1000.
  • MCI_STATUS_POSITION:
    المكان الحالي مثلا المكان الذي وصلنا إليه في عملية التشغيل أو التسجيل. ويكون أيضا مضروب عدد الثواني في 1000 ويرجع في العنصر dwReturn.
  • MCI_STATUS_MODE:
    الوضع الحالي. وترجع إحدى القيم التالية في العنصر dwReturn:

    • MCI_MODE_NOT_READY
    • MCI_MODE_PAUSE
    • MCI_MODE_PLAY
    • MCI_MODE_STOP
    • MCI_MODE_RECORD
    • MCI_MODE_SEEK
    • MCI_STATUS_READY:

ترجع قيمة TRUE إذا كانت MCI جاهزة للعمل أو لا.

المثال التالي يوضح كيفية معرفة الزمن الذي تم تسجيله:

MCI_STATUS_PARMS parmStatus;

parmStatus.dwCallback = 0;
parmStatus.dwReturn   = 0;
parmStatus.dwItem     = MCI_STATUS_LENGTH;
parmStatus.dwTrack    = 0;

mciSendCommand(0, MCI_STATUS,
    MCI_WAIT | MCI_STATUS_ITEM, (DWORD)&parmStatus);

// Display the number of seconds
// parmStatus.dwReturn / 1000

الحفظ Saving

يتم حفظ ما تم تسجيله عن طريق الأمر MCI_SAVE والذي يأخذ مدخلات من نوع MCI_SAVE_PARMS والتي لها التعريف التالي:

typedef struct {
    DWORD_PTR  dwCallback;
    LPCTSTR    lpfilename;
} MCI_SAVE_PARMS;

dwCallback تم ذكره. lpFilename هو اسم (عنوان) الملف.

ويجب تحديد الخيار MCI_SAVE_FILE ويفضل استخدام MCI_WAIT أيضا.

الكود التالي يقوم بحفظ ما تم تسجيله:

MCI_SAVE_PARMS parmSave;

parmSave.dwCallback = 0;
parmSave.lpfilename = TEXT("recording.wav");
// save to the current directory

mciSendCommand(wDeviceID, MCI_SAVE,
    MCI_WAIT | MCI_SAVE_FILE, (DWORD)&parmSave);

الإنهاء Closing

بعد أن انتهى عملك مع MCI ومع كارت الصوت يجب أن تقوم بإنهاء ما بدأته وتفريغ ما تم استغلاله من الذاكرة ويتم ذلك عن طريق الأمر MCI_CLOSE والذي يأخذ مدخلات من النوع MCI_GENERIC_PARMS والذي تم ذكره.

الكود التالي يوضح كيفية عملية الإنهاء:

MCI_GENERIC_PARMS parmsGen;

parmsGen.dwCallback = 0;

mciSendCommand(wDeviceID, MCI_CLOSE,
    MCI_WAIT, (DWORD)&parmsGen);

التشغيل Playing

في القسم السابق تعلمنا كيفية تسجيل ملفات صوت وحفظها. والآن نتعلم كيفية تشغيل الملفات الصوتية أيضا باستخدام MCI. وكما كان منهجنا في القسم السابق سيكون منهجنا في هذا القسم مع الفرق أنه تم ذكر الكثير من العناصر والدوال ولهذا لن نذكر شرحها مرة أخرى.

يجب عليك اتباع هذه الخطوات كي تستطيع تشغيل ملف صوتي والتحكم فيه في برنامجك:

  1. تحميل Loading الملف وتجهيز Opening كارت الصوت للعمل مع برنامجك (للإخراج Output.)
  2. بدأ التشغيل Playing.
  3. إنهاء Closing التعامل مع كارت الصوت.

التحميل Loading

بالطبع كي يمكنك تشغيل ملف صوتي يجب عليك تحديد الملف المراد تشغيله وأيضا تجهيز كارت الصوت لتشغيل هذا الملف وهذا يتم عن طريق الأمر MCI_LOAD والذي يأخذ مدخلات من النوع MCI_OPEN_PARMS والتي تم ذكرها.

يتشابه هذا الأمر كثيرا مع أمر MCI_OPEN السابق في عملية التسجيل من حيث المدخلات والخيارات، ولكن الفرق الوحيد أو الإضافة الوحيدة أنك في الأمر MCI_OPEN تقوم بإعطاء نص فارغ للعنصر lpstrElementName، أما في هذا الأمر فإنك تقوم بإعطاء عنوان الملف المراد تشغيله لهذا العنصر.

الكود التالي كود C يوضح كيفية تشغيل ملف:

MCI_OPEN_PARMS parmOpen;
WORD wDeviceID;

parmOpen.dwCallback       = 0;
parmOpen.wDeviceID        = 0;	// the default device
parmOpen.lpstrDeviceType  = TEXT("waveaudio");
parmOpen.lpstrElementName = TEXT("recording.wav");
parmOpen.lpstrAlias       = 0;

mciSendCommand(0, // the default device
    MCI_OPEN,
    MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
    (DWORD)&parmOpen);

// Keep the device ID for future calls
wDeviceID = parmOpen.wDeviceID;

بدأ التشغيل Playing

بعد تحميل الملف في الذاكرة وتحديده في MCI، تصبح MCI جاهزة للعمل ويمكنك الآن البدأ في تشغيل الملف. ويتم ذلك عن طريق الأمر MCI_PLAY والذي يأخذ عنصر من نوع MCI_PLAY_PARMS وهي مثل العنصر MCI_RECORD_PARMS تماما الخاص بالأمر MCI_RECORD.

وبالطبع يفضل استخدام الخيار MCI_NOTIFY مع هذا الأمر.

الكود التالي يوضح كيفية تشغيل ملف من أوله إلا آخره أو إلى إنهائه يدويا:

MCI_PLAY_PARMS parmPlay;

// Play the file
// from the start to the end

parmRec.dwCallback = 0;
parmRec.dwFrom	   = 0;
parmRec.dwTo	   = 0;

// notify me when you finish
mciSendCommand(wDeviceID, MCI_PLAY,
    MCI_NOTIFY, (DWORD)&parmRec);

أما هذا الكود فيوضح كيفية تشغيل أول ثلاثة دقائق فقط من الملف:

MCI_PLAY_PARMS parmPlay;

// play only 3 minutes
parmRec.dwCallback = 0;
parmRec.dwFrom	   = 0;
// 3 * 60 * 1000
parmRec.dwTo	   = 180000;

// notify me when you finish
mciSendCommand(wDeviceID, MCI_PLAY,
    MCI_NOTIFY | MCI_TO, (DWORD)&parmRec);

الإيقاف المؤقت Pausing

وهذا أيضا مثل عملية التسجيل يتم عن طريق الأمر MCI_PAUSE والمدخلات MCI_GENERIC_PARMS.

الاستمرار Resuming

وهذا أيضا مثل سابقه مثل عملية التسجيل يتم عن طريق الأمر MCI_RESUME والمدخلات MCI_GENERIC_PARMS.

معرفة المكان الحالي

ما تعلمناه في القسم السابق من كيفية الاستعلام عن الوضع الذي عليه MCI والطول Length والمكان الحاليCurrent Position للعملية نستطيع تطبيقه على عملية التشغيل Play. ويتم ذلك كما نعرف عن طريق الأمر MCI_STATUS والذي يأخذ المدخلات MCI_STATUS_PARMS.

الكود التالي يوضح كيفية معرفة المكان الحالي Current Position لعملية التشغيل:

MCI_STATUS_PARMS parmStatus;

parmStatus.dwCallback = 0;
parmStatus.dwReturn   = 0;
parmStatus.dwItem     = MCI_STATUS_POSITION;
parmStatus.dwTrack    = 0;

mciSendCommand(wDeviceID, MCI_STATUS,
    MCI_WAIT | MCI_STATUS_ITEM, (DWORD)&parmStatus);

// Display the current position
// parmStatus.dwReturn / 1000

الانتقال إلى مكان معين Seeking

عملية الانتقال إلى مكان معين أو Seeking وتتم عن طريق الأمر MCI_SEEK والذي يأخذ مدخلات من النوع MCI_STATUS_PARMS والتي لها التعريف التالي:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwTo;
} MCI_SEEK_PARMS;

يحوي العنصر dwTo قيمة هي عبارة عن المكان المراد الانتقال إليه مضروب عدد الثواني في 1000.

ويأخذ هذا الأمر الخيار MCI_WAIT والخيار MCI_TO مع أحد الخيارين التاليين:

  • MCI_SEEK_TO_END:
    تكون القيمة الموجودة في dwTo محسوبة تبعا لنهاية الملف. فمثلا إن كانت القيمة هي دقيقتين أي 120000 فعند تنفيذ هذا الأمر باستخدام هذا الخيار ينتقل المؤشر إلى دقيقتين قبل نهاية الملف، فمثلا إن كان طول الملف 10 دقائق ينتقل المؤشر إلى الدقيقة الثامنة.
  • MCI_SEEK_TO_START:
    تكون القيمة الموجودة في dwTo محسوبة تبعا لبداية الملف. فمثلا إذا كان طول الملف 10 دقائق فننتقل إلى الدقيقة الثانية إذا استخدمنا القيمة 120000.

الكود التالي يوضح كيفية الانتقال إلى بداية الملف:

MCI_SEEK_PARMS parmSeek;

parmStatus.dwCallback = 0;
parmStatus.dwTo	      = 0;

mciSendCommand(wDeviceID, MCI_SEEK,
    MCI_WAIT | MCI_SEEK_TO_START, (DWORD)&parmSeek);

وهذا يوضح كيفية الانتقال إلى الدقيقة الثالثة:

MCI_SEEK_PARMS parmSeek;

parmStatus.dwCallback = 0;
parmStatus.dwTo	      = 180000;

mciSendCommand(wDeviceID, MCI_SEEK,
    MCI_WAIT | MCI_TO, (DWORD)&parmSeek);

الإنهاء Closing

كما عرفت، يجب عليك دوما إنهاء تشغيل الـ MCI بعد الانتهاء من عملك. ويتم ذلك كما ذكرنا باستخدام الأمر MCI_CLOSE.

الخلاصة

الجدول التالي يوضح الأوامر التي استخدمناها والمدخلات الخاصة بكل أمر والخيارات الخاصة به.

الأمر Input/Output نوع المدخلات أشهر الخيارات
MCI_OPEN In/Out MCI_OPEN_PARMS MCI_WAIT, MCI_OPEN_ELEMENT, and MCI_OPEN_TYPE
MCI_RECORD In MCI_RECORD_PARMS (none) or MCI_NOTIFY
MCI_PLAY Out MCI_PLAY_PARMS MCI_NOTIFY
MCI_PAUSE In/Out MCI_GENERIC_PARMS MCI_WAIT
MCI_RESUME In/Out MCI_GENERIC_PARMS MCI_WAIT
MCI_STOP In/Out MCI_GENERIC_PARMS MCI_WAIT
MCI_SEEK Out MCI_SEEK_PARMS MCI_WAIT and MCI_TO / MCI_SEEK_TO_START / MCI_SEEK_TO_END
MCI_SAVE In MCI_SAVE_PARMS MCI_WAIT and MCI_SAVE_FILE
MCI_STATUS In/Out MCI_STATUS_PARMS MCI_WAIT and MCI_STATUS_ITEM
MCI_CLOSE In/Out MCI_GENERIC_PARMS MCI_WAIT

MCI والدوت نت

ما ذكرناه سابقا كان عن MCI ولغة C. فكما تعرف لا يمكنك التعامل مع دوال الويندوز Windows API مباشرة من خلال بيئة الدوت نت. ولكن يمكنك ذلك عن طريق تعريف الدوال والعناصر التي تريد استخدامها بتعريف خاص ببيئة الدوت نت يسمى Platform Invokation أو PInvoke.

الكود التالي هو عبارة عن تعاريف جميع ما ذكرناه من دوال وعناصر وخيارات وثوابت وغيرها، تعريفا خاصا بالدوت نت أو بالأصح بالـ C#:

internal static class SafeNativeMethods
{
    // Constants

    public const string WaveAudio = "waveaudio";

    public const uint MM_MCINOTIFY = 0x3B9;

    public const uint MCI_NOTIFY_SUCCESSFUL = 0x0001;
    public const uint MCI_NOTIFY_SUPERSEDED = 0x0002;
    public const uint MCI_NOTIFY_ABORTED = 0x0004;
    public const uint MCI_NOTIFY_FAILURE = 0x0008;

    public const uint MCI_OPEN = 0x0803;
    public const uint MCI_CLOSE = 0x0804;
    public const uint MCI_PLAY = 0x0806;
    public const uint MCI_SEEK = 0x0807;
    public const uint MCI_STOP = 0x0808;
    public const uint MCI_PAUSE = 0x0809;
    public const uint MCI_RECORD = 0x080F;
    public const uint MCI_RESUME = 0x0855;
    public const uint MCI_SAVE = 0x0813;
    public const uint MCI_LOAD = 0x0850;
    public const uint MCI_STATUS = 0x0814;

    public const uint MCI_SAVE_FILE = 0x00000100;
    public const uint MCI_OPEN_ELEMENT = 0x00000200;
    public const uint MCI_OPEN_TYPE = 0x00002000;
    public const uint MCI_LOAD_FILE = 0x00000100;
    public const uint MCI_STATUS_POSITION = 0x00000002;
    public const uint MCI_STATUS_LENGTH = 0x00000001;
    public const uint MCI_STATUS_ITEM = 0x00000100;

    public const uint MCI_NOTIFY = 0x00000001;
    public const uint MCI_WAIT = 0x00000002;
    public const uint MCI_FROM = 0x00000004;
    public const uint MCI_TO = 0x00000008;

    // Structures

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_OPEN_PARMS
    {
        public IntPtr dwCallback;
        public uint wDeviceID;
        public IntPtr lpstrDeviceType;
        public IntPtr lpstrElementName;
        public IntPtr lpstrAlias;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_RECORD_PARMS
    {
        public IntPtr dwCallback;
        public uint dwFrom;
        public uint dwTo;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_PLAY_PARMS
    {
        public IntPtr dwCallback;
        public uint dwFrom;
        public uint dwTo;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_GENERIC_PARMS
    {
        public IntPtr dwCallback;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_SEEK_PARMS
    {
        public IntPtr dwCallback;
        public uint dwTo;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_SAVE_PARMS
    {
        public IntPtr dwCallback;
        public IntPtr lpfilename;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_STATUS_PARMS
    {
        public IntPtr dwCallback;
        public uint dwReturn;
        public uint dwItem;
        public uint dwTrack;
    } ;

    // Functions

    [DllImport("winmm.dll", CharSet = CharSet.Ansi,
    BestFitMapping = true, ThrowOnUnmappableChar = true)]
    [return: MarshalAs(UnmanagedType.U4)]
    public static extern uint mciSendCommand(
        uint mciId,
        uint uMsg,
        uint dwParam1,
        IntPtr dwParam2);

    [DllImport("winmm.dll", CharSet = CharSet.Ansi,
    BestFitMapping = true, ThrowOnUnmappableChar = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool mciGetErrorString(
        uint mcierr,
        [MarshalAs(UnmanagedType.LPStr)]
        System.Text.StringBuilder pszText,
        uint cchText);
}

ماذا عن استلام رسالة الويندوز MM_MCINOTIFY عند استخدام الوضع MCI_NOTIFY؟ الكود التالي يوضح هذه الفكرة:

public partial class MainForm : Form
{
    . . .

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == SafeNativeMethods.MM_MCINOTIFY)
        {
        // Handle the message
        }

        // DO NOT REMOVE the following line
        base.WndProc(ref m);
    }
}

ملحق 1: تحديد خصائص الـ Device

واليوم نستكمل في هذا الموضوع عن كيفية تحديد الخصائص مثل قوة القناة الصوتية Channel، حجم الصوت Volume، المعدل Sampling Rate وغيرها للـ Device المستخدم.

مثل جميع الأوامر الأخرى الخاصة بـ MCI، لتحديد خصائص الـ Device يجب عليك استخدام إما أمر نصي String أو رقمي Command، وفي موضوعنا هذا فإننا نستخدم الأخيرة، فلذلك سوف نستخدم الأمر الرقمي الخاص بتحديد خصائص الـ Device ألا وهو MCI_SET.

يستخدم الأمر MCI_SET لتحديد خصائص الـ Device وبياناته. وبما أن كل Device له الخصائص الخاصة به (مثلا الفيديو يختلف عن الصوت) فإن مدخلات الأمر MCI_SET تختلف بدورها.

لن نخرج عن سياق موضوعنا الأصلي والذي يتكلم عن الصوتيات (عمليات الـ Waveform) فقط. ولذلك فإننا سنستخدم المدخل MCI_WAVE_SET_PARMS والذي يستخدم مع الصوتيات Waveform والذي له التكوين التالي:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwTimeFormat;
    DWORD     dwAudio;
    UINT      wInput;
    UINT      wOutput;
    WORD      wFormatTag;
    WORD      wReserved2;
    WORD      nChannels;
    WORD      wReserved3;
    DWORD     nSamplesPerSec;
    DWORD     nAvgBytesPerSec;
    WORD      nBlockAlign;
    WORD      wReserved4;
    WORD      wBitsPerSample;
    WORD      wReserved5;
} MCI_WAVE_SET_PARMS;

هذا التركيب Structure يحوي جميع البيانات التي يمكنك تحديدها للـ Device المطلوب، للبساطة والاختصار لن نفصل في شرح العناصر الخاصة بهذا التركيب ولكن يمكنك فهم فائدة هذه العناصر وكيفية تحديدها من خلال فقط قراءة أسمائها. وإذا احتجت معلومات أكثر يمكنك القراءة في شرح الـ MSDN لـ MCI_WAVE_SET_PARMS Structure.

كما تعرف، إذا كنت تستخدم لغة خاصة بالدوت نت مثل الـ C# فإنك لن يمكنك استخدام هذا التركيب مباشرة في برنامجك لأنه خاص ببرمجة الويندوز Windows API. ولذلك ستحتاج إلى عملية Marshaling أي عملية تحويل هذا التركيب ليصبح قابل للتعامل معه من خلال بيئة الدوت نت. وهذا هو الـ Marshaling Structure لهذا التركيب في C#:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct MCI_WAVE_SET_PARMS
{
    public IntPtr dwCallback;
    public uint     dwTimeFormat;
    public uint     dwAudio;
    public uint      wInput;
    public uint      wOutput;
    public ushort      wFormatTag;
    public ushort      wReserved2;
    public ushort      nChannels;
    public ushort      wReserved3;
    public uint     nSamplesPerSec;
    public uint     nAvgBytesPerSec;
    public ushort      nBlockAlign;
    public ushort      wReserved4;
    public ushort      wBitsPerSample;
    public ushort      wReserved5;
}

الآن أصبحت جاهزا لكتابة الكود الخاص بك.

عرفت كيفية تحديد الخصائص للـ MCI Device. ماذا عن كيفية استرجاع هذه البيانات؟ هل تتذكر في الموضوع الرئيسي كيف كنا نسترجع حالة الـ Device (يعمل، يقوم بالتسجيل، متوقف مؤقتا، متوقف، غير جاهز للعمل، إلخ)؟

بالطبع تتذكرها وتتذكر الأمر الذي استخدمناه هناك وهو MCI_STATUS والذي يستخدم لمعرفة الحالة وجميع البيانات الخاصة بهذا الـ Device (ولكننا قد تكلمنا وقتها عن كيفية استرجاع الحالة فقط.)

باستخدام الخيار MCI_STATUS_ITEM مع الأمر MCI_STATUS وباستخدام خيارات الاستعلام مثل MCI_DGV_STATUS_VOLUME (للاستعلام عن حجم الصوت) يمكنك استرجاع جميع البيانات التي قمت بتحديدها باستخدام الأمر MCI_SET.

المزيد عن الأمر MCI_STATUS اقرأ القسم ’الحالة Status‘ في موضوعنا الأصلي. المزيد؟ اقرأ في MSDN عن MCI_STATUS Command.

الأمثلة

مع الدرس مثالين، الأول SMPREC مسجل صوتي بالـ C، والآخر SampleRec مسجل صوتي بالـ C#.

حمل “SMPREC – C.zip”.
حمل “SampleRec – C#.zip”.

7 رأي حول “إنشاء مسجل صوتي في C و C#

  1. جزاك الله الف خير
    تدري ماذا أريد أن اعمل
    أريد عمل برنامج يقوم بالإستماع الى قارئ القرآن ويراجعة إن أخطأ وذلك بعد تسجيل نسخة ذات قرائة صحيحة من المصحف
    ثم التسميع غيباً ويقوم بمطابقة القرائة مع النسخة المسجلة سابقاً إذا تغيرت أعطا تنبيهاً

    إعجاب

    1. السلام عليكم ورحمة الله وبركاته

      أولا، شكرا كثيرا شكرا على مروركم ومشاركتكم معنا بأفكاركم.

      من وجهة نظري المحدودة، أظن أن تطبيق مثل هذه الفكرة يعتبر صعب جدا إن لم يكن مستحيلا فكيف ستحسب طول المد والغنة والتفخيم والترقيق ونحوها؟

      أما إذا قصدت التسميع، أي متابعة أخطاء الحفظ للقارئ فهذا أيضا يعتبر من الأمور الصعبة، رغم أن هناك تقينات مثل إدراك الأصوات Speech Recognition ونحوها (ولم نجدها في اللغة العربية حتى الآن) حيث أنها غير دقيقة بالمرة وغير ذلك لن يمكننا متابعتها في القرءان، حيث هناك احكام تجويدية تخرج اللفظ عن المعهود (نحو الإدغام والإخفاء ونحوها) وغير ذلك من الصعوبات التي يمكن أن تواجه النظام.

      أخيرا أقتبس بكلمة الشيخ ابن عثيمين -رحمه الله-: “من كان شيخه كتابه، فخطؤه أكثر من صوابه”. ما بالك بمن كان شيخه برنامجه؟
      🙂

      دمتم بأطيب حال.

      إعجاب

  2. جزاك الله الف خير
    تدري ماذا أريد أن اعمل
    أريد عمل برنامج يقوم بالإستماع الى قارئ القرآن ويراجعة إن أخطأ وذلك بعد تسجيل نسخة ذات قرائة صحيحة من المصحف
    ثم التسميع غيباً ويقوم بمطابقة القرائة مع النسخة المسجلة سابقاً إذا تغيرت أعطا تنبيهاً

    إعجاب

    1. السلام عليكم ورحمة الله وبركاته

      أولا، شكرا كثيرا شكرا على مروركم ومشاركتكم معنا بأفكاركم.

      من وجهة نظري المحدودة، أظن أن تطبيق مثل هذه الفكرة يعتبر صعب جدا إن لم يكن مستحيلا فكيف ستحسب طول المد والغنة والتفخيم والترقيق ونحوها؟

      أما إذا قصدت التسميع، أي متابعة أخطاء الحفظ للقارئ فهذا أيضا يعتبر من الأمور الصعبة، رغم أن هناك تقينات مثل إدراك الأصوات Speech Recognition ونحوها (ولم نجدها في اللغة العربية حتى الآن) حيث أنها غير دقيقة بالمرة وغير ذلك لن يمكننا متابعتها في القرءان، حيث هناك احكام تجويدية تخرج اللفظ عن المعهود (نحو الإدغام والإخفاء ونحوها) وغير ذلك من الصعوبات التي يمكن أن تواجه النظام.

      أخيرا أقتبس بكلمة الشيخ ابن عثيمين -رحمه الله-: “من كان شيخه كتابه، فخطؤه أكثر من صوابه”. ما بالك بمن كان شيخه برنامجه؟
      🙂

      دمتم بأطيب حال.

      إعجاب

اترك تعليقا