قسمت بعدي

قسمت قبلي

 

فهرست مطالب

صفحة فعاليتها

 

 

 

 

 

 

 

فصل چهارم

 

پردازش صوت : برنامه‌نويسي و پياده‌سازي

 

 

1- ساختار مورد نياز براي نگهداري ويژگيهاي صدا

2- انجام پردازش صدا به صورت يك رشتة مستقل

3- ضبط صدا

4- پخش صدا

5- كتابخانة پردازش صوت

6- منابع فصل

 

 

 

 

 

 

 

 

بازگشت به قسمت اوّل فصل

 

3-    ضبط صدا

 

براي ضبط صدا و انجام پردازشهاي مرتبط با آن كلاسي به نام HSoundRecorder به صورت زير از كلاس HSoundRunner مشتق گرديد:

 

class HSoundRecorder :

       public HSoundRunner

{

       DECLARE_DYNCREATE(HSoundRecorder)

 

public:

       HSoundRecorder();          

       virtual ~HSoundRecorder();

 

protected:

       void AddBuffer();

 

       //Message Map For WM_WIM_DATA:

       afx_msg void OnDataReady(UINT uParm, LONG lWaveHdr);

 

private:

       HWAVEIN m_hWaveIn;

       HShortQueue* m_pQueue;

 

public:           

 

       BOOL Start(WAVEFORMATEX* pwfex=NULL);

       BOOL Stop();

       short* GetSamples(int& nSamples);

       BOOL IsRecording();

 

       DECLARE_MESSAGE_MAP()

};

 

براي استفاده از اين كلاس كافي است متدهاي Start و Stop آن فراخواني گردند ولي درك كامل نحوة عملكرد آن نياز به برخي مقدمات دارد.

براي شروع كار ضبط از فراخواني اي.پي.آي زير با پارامترهاي مناسب براي در اختيار گرفتن يك ابزار ورودي صدا استفاده مي‌كنيم:

 

MMRESULT waveInOpen(

  LPHWAVEIN phwi,           

  UINT uDeviceID,           

  LPWAVEFORMATEX pwfx,      

  DWORD dwCallback,         

  DWORD dwCallbackInstance, 

  DWORD fdwOpen             

);

 

كه در آن phwi اشاره‌گر به بافري است كه يك handle به ابزار باز شده براي ورودي صدا را در اختيار مي‌گذارد. اين پارامتر ابزاري را براي دسترسي به وسيلة ورودي صدا در اختيار مي‌گذارد كه ما در ساير فراخوانيهاي مرتبط به آن نياز داريم لذا در كلاس تعريف شده متغير m_hWaveIn را براي ذخيرة اين پارامتر پس از اين فراخواني و دسترسي به آن در ساير متدها در نظر گرفته‌ايم.

پارامتر uDeviceID شناسة ابزار ورودي را به تابع مي‌دهد. مي‌توان از شناسة WAVE_MAPPER استفاده كرد كه با استفاده از آن برنامه سخت‌افزار پيش‌فرض موجود را كه داراي قابليت پردازش فرمت انتخاب شده كه توسط پارمتر pwfx به تابع داده مي‌شود و ما از عضو دادة m_wfData از كلاس HSound براي انتخاب مقدار آن استفاده مي‌كنيم به طور خودكار انتخاب مي‌كند.

پارامتر dwCallback شناسة پنجره، پردازه يا رشته‌اي را كه پيامهاي چندرسانه‌اي به آن ارسال خواهد شد به تابع مي‌دهد كه ما از شناسة رشته (عضو دادة m_dwThreadID) براي تعيين اين پارامتر استفاده خواهيم نمود. پارامتر بعدي dwCallbackInstance دادة سطح كاربري را كه به ساز و كار فرتخواني callback ارسال مي‌شود تعيين مي‌نمايد و ما از اين پارامتر استفاده نخواهيم نمود.

پارامتر آخر يعني fdwOpen پرچمي براي ابزار ورودي است كه سازوكار تفسير پارامترها را مشخص مي‌كند و چون ما از سازوكار فراخواني رشته‌اي استفاده مي‌كنيم مقدار آن را برابر با CALLBACK_THREAD انتخاب مي‌نماييم.

مقدار بازگشتي در صورت بروز اشكال غيرصفر خواهد بود مسائلي از قبيل اختصاص يافتن وسيلة ورودي به يك پردازة ديگر به صورتي كه سيستم عامل به صورت اشتراكي آن را در اختيار نگذارد، كمبود حافظه و ... ممكن است باعث بروز اشكال شوند كه نوع اشكال با توجه به مقدار بازگشتي مشخص مي‌گردد و ميتواند به كاربر اعلام گردد.

بعد از در اختيار گرفتن يك وسيلة ورودي لازم است كه براي عمليات حافظه اختصاص يابد و اين عمل مي‌تواند با فراخواني AddBuffer به تعداد كافي صورت گيرد.

بعد از تخصيص حافظه توسط فراخواني اي.پي.آي زير عمل ضبط شروع مي‌گردد:

 

MMRESULT waveInStart(

HWAVEIN hwi

);

 

پارامتر وروي همان پارامتر بازگشت با مقدار فراخواني waveInOpen يعني phwi است كه همچنانكه اشاره شد ما آن را در عضو دادة m_hWaveIn نگهداري مي‌كنيم. اين فراخواني نيز مانند قبلي در صورت موفقيت‌آميز بودن مقدار صفر باز مي‌گرداند.

توضيحات بالا مقدمات كافي را براي درك كد متد Start كه در زير مي‌آيد فراهم مي‌آورد:

 

BOOL HSoundRecorder::Start(WAVEFORMATEX* pwfex)

{

       if(!HSoundRunner::Start(pwfex))

                    return FALSE;

       m_pQueue=new HShortQueue;

 

       //Open the wave device:

       if(

     ::waveInOpen(

                  &m_hWaveIn,

                  WAVE_MAPPER,

                  &m_wfData,

                  m_dwThreadID,

                  0L,

                  CALLBACK_THREAD

                 )

        )

                    return FALSE;

 

       //Add several buffers to queue:

       for(int i=0;i<3;i++)

                    AddBuffer();

       if(::waveInStart(m_hWaveIn))

                    return FALSE;

 

       m_bRunning=TRUE;

       return TRUE;

}

 

اما توابع چندرسانه‌اي ويندوز سازوكار خاصي براي اضافه كردن بافر دارند كه مي‌بايست آنها را در نسخة بازنويسي شدة AddBuffer اين كلاس لحاظ كنيم. براي اضافه كردن يك بافر ابتدا بايد آن را توسط فراخواني زير براي استفاده آماده كنيم:

 

MMRESULT waveInPrepareHeader(

  HWAVEIN hwi,   

  LPWAVEHDR pwh, 

  UINT cbwh      

);

 

به جاي پارامتر hwi عضو دادة m_hWaveIn را كه قبلاً در فراخواني waveInOpen مقدارگذاري شده قرار مي‌دهيم. پارامتر دوم يك اشاره‌گر به متغيري با ساختار زير است:

 

typedef struct {

    LPSTR  lpData;

    DWORD  dwBufferLength;

    DWORD  dwBytesRecorded;

    DWORD  dwUser;

    DWORD  dwFlags;

    DWORD  dwLoops;

    struct wavehdr_tag * lpNext;

    DWORD  reserved;

} WAVEHDR;

 

كه لازم است اشاره‌گر lpData به يك حافظه حاوي تعداد مورد نياز عضو اشاره كند. از آنجا كه ما هر بار بافري با اندازة m_iBufferSize در نظر مي‌گيريم، تعداد خانه‌هاي اين آرايه بر حسب بايت برابر با اندازة بافر ضرب در حداقل تعداد بلوك براي فرمت انتخاب شده (فيلد nBlockAlign ساختار WAVEFORMATEX) مي‌باشد و لازم است كه به اين تعداد حافظه اختصاص داده اشاره‌گر lpData را برابر با آدرس آن انتخاب كنيم، در ضمن اندازة بافر اختصاص داده شده را از طريق فيلد dwBufferLength به اطلاع تابع استفاده كننده مي‌رسانيم. پارامتر آخر فراخواني مورد بحث بايد برابر با اندازة پارامتر دوم بر حسب بايت قرار داده شود.

بعد از آماده شدن بافر آن را توسط فراخواني زير به بافرهاي آماده براي اعمال چندرسانه‌اي اضافه مي‌كنيم:

 

MMRESULT waveInAddBuffer(

  HWAVEIN hwi,   

  LPWAVEHDR pwh, 

  UINT cbwh      

);

 

مانند فراخوانيهاي قبل مقدار خروجي دو فراخواني اخير در صورت عدم بروز خطا صفر خواهد بود. كد زير پياده‌سازي كامل متد بازنويسي شدة AddBuffer را براي كلاس HSoundRecorder به نمايش مي‌گذارد:

 

void HSoundRecorder::AddBuffer()

{

       //new a buffer:

       char       *sBuf=

         new char[m_wfData.nBlockAlign*m_iBufferSize];

 

       //new a header:

       LPWAVEHDR        pHdr=new WAVEHDR;

       if(!pHdr) return;

       ZeroMemory(pHdr,sizeof(WAVEHDR));

 

       pHdr->lpData=sBuf;

       pHdr->dwBufferLength=

                   m_wfData.nBlockAlign*m_iBufferSize;

 

       //prepare it:

       ::waveInPrepareHeader(

                      m_hWaveIn,

                      pHdr,

                      sizeof(WAVEHDR)

                     );

       //add it:

       ::waveInAddBuffer(m_hWaveIn, pHdr, sizeof(WAVEHDR));

       HSoundRunner::AddBuffer();

}

 

بعد از آن كه عمل ضبط آغاز شد و پس از پر شدن هر بافر پيغامي به پنجره يا رشته‌اي كه شناسة آن به فراخواني waveInOpen داده شده ارسال مي‌گردد كه با شناسة MM_WIM_DATA مي‌توان به آن مراجعه نمود. در هنگام فعال شدن اين پيغام لازم است بافر استفاده شده براي استفادة مجدد آماده گردد و در ضمن مكان مناسب براي ذخيرة داده‌هاي ضبط شده و همچنين نمايش آنها پس از فعال شدن اين پيغام است.

از آنجا كه طول زمان ضبط صدا مشخص نيست طول بافري كه نهايتاً داده‌ها بايد در آن قرار گيرند قابل پيشبيني نمي‌باشد. از اين رو ما از يك ساختار كه نوع آن را HShortPocket نامگذاري كرده‌ايم براي ذخيرة داده‌هاي ارسال شده استفاده مي‌نماييم و حاصل را در صفي كه در قالب كلاس HShortQueue پياده‌سازي شده درج مي‌نماييم.(اين دو ساختار ربطي به برنامه‌نويسي پردازش صدا ندارند و درك عملكرد آنها نياز به توضيح اضافي ندارد لذا در اينجا توضيح داده نمي‌شوند.) عضو دادة m_pQueue از كلاس HSoundRecorder صفي است كه در سطور قبل در مورد آن بحث شد.

ما به شيوة ام.اف.سي براي پيغام MM_WIM_DATA تابعي به نام OnDataReady مي‌سازيم كه پارامترهاي آن پارامترهاي ارسالي از طرف پيغام هستند كه دومين آنها كه حاوي ساختار بافر استفاده شده است براي ما اهميت دارد. با توجه به توضيحات داده شده درك كد اين تابع ميسر است:

 

void

HSoundRecorder::OnDataReady(UINT uParm, LONG lWaveHdr)

{

       LPWAVEHDR        pHdr=(LPWAVEHDR)lWaveHdr;

       ::waveInUnprepareHeader(

                           m_hWaveIn,

                           pHdr,

                           sizeof(WAVEHDR)

                          );

       if(m_bRunning)

       {

                    //Save Input Data:

                    m_pQueue->InsertItem(

                                 pHdr->dwBytesRecorded/2,

                                 (short*)pHdr->lpData

                                );

 

                    //Draw Buffer:

                    DrawBuffer(

               pHdr->dwBytesRecorded/2,

               (short*)pHdr->lpData

                 );

 

                    //reuse the header:

                    ::waveInPrepareHeader(

                              m_hWaveIn,

                              pHdr,

                              sizeof(WAVEHDR)

                             );

                    ::waveInAddBuffer(

                          m_hWaveIn,

                          pHdr,

                          sizeof(WAVEHDR)

                         );

                    return;

       }

       //we are stopping:

       delete pHdr->lpData;

       delete pHdr;

       m_nBuffers--;

}

 

در صورتي كه متد Stop احضار شده باشد مقدار m_bRunning برابر با FALSE است در اين هنگام نه تنها نيازي به اضافه كردن بافر نداريم بلكه مي‌توانيم بافرهاي اختصاص داده شده را آزاد كنيم. قسمت آخر كد چنين عملي را انجام مي‌دهد.

پس از انجام عمل و احضار متدStop  توسط برنامه لازم است با فراخواني waveInStop عمل ضبط را متوقف كنيم و با فراخواني waveInClose ابزار ضبط آن را براي استفادة ساير برنامه‌ها آزاد كنيم. علاوه بر اين در اين هنگام تمامي بافرها ارسال شده‌اند و مي‌توانيم آن را به صورت يك آراية معمولي ذخيره كنيم و متغير m_pQueue را آزاد نماييم:

 

BOOL HSoundRecorder::Stop()

{

       if(!HSoundRunner::Stop())

                    return FALSE;

 

       ::waveInStop(m_hWaveIn);

       ::waveInClose(m_hWaveIn);

 

       m_pSamples=m_pQueue->ConvertToArray(m_nSamples);

       delete m_pQueue;

       return TRUE;

}

 

پس از انجام اين عمل برنامه مي‌تواند با فراخواني GetSamples به آراية حاوي صداي ضبط شده دسترسي پيدا كند:

 

short* HSoundRecorder::GetSamples(int& nSamples)

{

       nSamples=m_nSamples;

       return m_pSamples;

}

 

اين آرايه در هنگام آزاد شدن متغير از نوع HSoundRecorder در متد ويرانگر آزاد مي‌شود:

 

HSoundRecorder::~HSoundRecorder()

{

       if(m_pSamples)

                    delete []m_pSamples;

}

 

در متد سازندة كلاس پدر مقدا m_pOwner برابر با NULL در نظر گرفته مي‌شود. اين به اين معني است كه در حالت پيش‌فرض عمليات به صورت گرافيكي نشان داده نمي‌شود و براي انجام اين عمل لازم است كه ابتدا مقدار m_pOwner به اشاره‌گر به يك پنجره يا كنترل توسط فراخواني SetOwner مقدارگذاري شود. در متد سازندة اين كلاس به منظور جلوگيري از مقدارگزينيهاي ناخواسته مقدار m_hWaveIn برابر با NULL انتخاب مي‌گردد:

 

HSoundRecorder::HSoundRecorder()

{

       m_hWaveIn        =NULL;

}

 

4-    پخش صدا

 

پردازشهاي مربوط به پخش صدا مشابهت زيادي با پردازشهاي مربوط به ضبط دارد در اينجا نيز بايد ابتدا يك ابزار صدا را براي خروجي باز كرد، تعدادي بافر اضافه نمود، در تابع پيام بافرهاي جديد آماده نمود و سرانجام در متد Stop ابزار خروجي را بست:

 

class HSoundPlayer:

       public HSoundRunner

{

public:

       HSoundPlayer();

       ~HSoundPlayer();

 

       void SetData(int nSamples, short* pSamples);

       BOOL Start(WAVEFORMATEX* format=NULL);

       BOOL Start(

              int iSize,

              short* pData,

              WAVEFORMATEX* pwfex=NULL

             );

       BOOL Stop();

       BOOL IsPlaying();

 

       void AddBuffer();

 

       DECLARE_MESSAGE_MAP()

       afx_msg void OnMM_WOM_DONE(UINT parm1, LONG parm2);

 

private:

 

       HWAVEOUT m_hWaveOut;

       int m_nSamplesPlayed;

};

 

تفاوت تنها در اين مورد است كه در اينجا ما بايد داده‌هاي آماده را به برنامه بدهيم. به دليل آن كه در برنامه‌هاي ما خروجي هميشه پس از ورودي مطرح مي‌گردد و ما پس از پايان ورودي به داده‌هاي آن دسترسي داريم ساده‌ترين راه دادن داده‌ها مقداردهي اشاره‌گر m_pSamples با بافر حاوي داده‌هاي ورودي است كه معمولاُ ما آن را از شيء ضبط كننده مي‌گيريم:

 

void HSoundPlayer::SetData(int iSize, short* pData)

{

       m_nSamples=iSize;

       m_pSamples=pData;

}

 

به منظور افزايش انعطاف‌پذيري مي‌توان اين داده‌ها را در متد Start نيز دريافت نمود:

 

BOOL HSoundPlayer::Start(

                         int iSize,

                    short* pData,

                    WAVEFORMATEX* format

                           )

{

       SetData(iSize, pData);

       return Start(format);

}

 

فراخواني waveOutOpen عملي مشابه waveInOpen را براي خروجي صدا انجام مي‌دهد و پارامترهاي آن مشابه با آن است:

 

BOOL HSoundPlayer::Start(WAVEFORMATEX* format)

{

       if(m_pSamples==NULL)

                    return FALSE;

       if(!HSoundRunner::Start(format))

                    return FALSE;

       else

       {

                    // open wavein device

                    MMRESULT mmReturn = 0;

                    mmReturn = ::waveOutOpen(

                                  &m_hWaveOut,

                                  WAVE_MAPPER,

                                  &m_wfData,

                                  m_dwThreadID,

                                  NULL,

                                  CALLBACK_THREAD

                                 );

                    if(mmReturn)

                                return FALSE;

                    else

                    {

                                m_bRunning = TRUE;

                               

                                m_nSamplesPlayed=0;

                                for(int i=0; i<3; i++)

                                            AddBuffer();

                    }

       }

       return TRUE;      

}

 

در اينجا تفاوتي نيز وجود دارد. در فرايند ضبط اين برنامة كاربر بود كه پيغام Stop را ارسال مي‌كرد حال آن كه در اينجا علاوه بر كاربر، تمام شدن بافر حاوي داده‌ها نيز بايد باعث فعال شدن آن پيغام شود. براي اين منظور از متغير شمارشگري به نام m_pSamplesPlayed استفاده كرده‌ايم كه تعداد نمونه‌هاي پخش شده بر حسب تعداد حافظة short (كه نصف تعداد نمونه در حافظة معادل بر حسب بايت است) را ذخيره مي‌كند.

تفاوت ديگري نيز وجود دارد و آن اين است كه ما بايد توسط فراخواني waveOutWrite داده‌ها را روي بافر ارسالي بنويسيم:

 

void HSoundPlayer::AddBuffer()

{

       MMRESULT mmReturn = 0;

      

       // create the header

       LPWAVEHDR pHdr = new WAVEHDR;

       if(pHdr == NULL) return;

 

       // new a buffer

       pHdr->lpData=

                (char*)(m_pSamples+

                        m_nSamplesPlayed);//buffer;

       pHdr->dwBufferLength = m_iBufferSize;

       pHdr->dwFlags = 0;

      

       // prepare it

       mmReturn=

          ::waveOutPrepareHeader(

                m_hWaveOut,

                pHdr,

                sizeof(WAVEHDR)

                                );

       // write the buffer to output queue

       mmReturn =

          ::waveOutWrite(

                m_hWaveOut,

                pHdr,

                sizeof(WAVEHDR)

                        );

       if(mmReturn) return;

       // increment the number of waiting buffers

       m_nSamplesPlayed+=m_iBufferSize/2;

 

       HSoundRunner::AddBuffer();

}

 

در انجام عمل پخش نيز پيغامي پس از پايان پخش هر بافر با شناسة MM_WON_DONE به پنجره يا رشتة كنترل كننده ارسال مي‌شود كه در آن مي‌توانيم دادة پخش شده را به صورت گرافيكي نشان دهيم، بافر بعدي را بفرستيم و در صورت رسيدن به پايان داده‌ها پيغام Stop را ارسال كنيم:

 

void

HSoundPlayer::OnMM_WOM_DONE(UINT parm1, LONG parm2)

{     

 

       LPWAVEHDR pHdr = (LPWAVEHDR) parm2;

       //Draw Buffer:

       DrawBuffer(

              pHdr->dwBufferLength/2,

              (short*)pHdr->lpData

             );

 

       if(

      ::waveOutUnprepareHeader(

                               m_hWaveOut,

                               pHdr,

                               sizeof(WAVEHDR)

                              )

     )

                    return;

 

       m_nBuffers--;

 

       if(m_bRunning)

       {                      

 

                    if(!(m_nSamplesPlayed+m_iBufferSize/2>=m_nSamples))

                    {

                                AddBuffer();

                                // delete old header

                                delete pHdr;

                                return;                           

                    }

                    else

                    {

                                Stop();

                    }

       }

 

       // we are closing the waveOut handle,

       // all data must be deleted

       // this buffer was allocated in Start()

       delete pHdr;

 

       if(m_nBuffers == 0 && m_bRunning == false)

       {

                    if (::waveOutClose(m_hWaveOut))

                                 return;

       }

}

 

اگر دقت كرده باشيد در هنگام رسيدن به پايان داده‌ها در همين تابع اخير ما ابزار خروجي را مي‌بنديم. اگر زودتر پيغام Stop توسط برنامه ارسال شود نيز به لحاظ FALSE شدن مقدار m_bRunning اين عمل انجام مي‌پذيرد لذا كافي است كه در متد Stop ابزار براي استفادة بعدي به طور كامل آزاد گردد:

 

BOOL HSoundPlayer::Stop()

{

       if(HSoundRunner::Stop())

                    return (::waveOutReset(m_hWaveOut)!=0);

       return FALSE;

}

 

از آنجا كه در اين كلاس حافظه‌اي اختصاص داده نمي‌شود در متد ويرانگري نيز آزاد شدن حافظه انجام نمي‌گيرد. مشابه متد سازندة كلاس HSoundPlayer مقدار m_hWaveOut در ابتدا برابر با NULL در نظر گرفته مي‌شود:

 

HSoundPlayer::HSoundPlayer()

{

       m_hWaveOut = NULL;

}

 

5-    كتابخانة پردازش صوت

 

از آنجا كه كلاسهاي پياده‌سازي شده كه در اين فصل توضيح داده شدند مورد استفادة بيش از يك برنامه قرار گرفته‌اند آنها را به صورت يك كتابخانة ايستا و با نام HSoundLib گردآوري نموديم تا تغيير كد اين كتابخانه راحت‌تر در تمامي برنامه‌ها اعمال گردد و نياز به تغيير كد تك تك آنها نباشد.

 

6-    منابع فصل

 

1)     Microsoft ®, MSDN Library (January 2000 Edition)

2)     Thomas Holme, How to play and record sound, from http://www.codeproject.com

 

7-