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

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

همچنان که در فصل پیش اشاره شد برای ذخیره یا بازخوانی یک نمونه صدا به صورت دیجیتال نیازمند آنیم که برخی ویژگیهای خاص صدای دیجیتالی از قبیل نرخ نمونه‌برداری، تعداد بیت هر نمونه و یک‌کاناله یا دوکاناله بودن صدا را مشخص کنیم.

برای این منظور در محیط برنامه‌نویسی مورد نظر ما (ویندوز) از ساختاری به نام WAVEFORMATEX استفاده می‌گردد که به صورت زیر تعریف می‌گردد:

[code lang=”cpp”]
typedef struct {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
WORD wBitsPerSample;
WORD cbSize;
} WAVEFORMATEX;
[/code]

در این ساختار فیلد wFormatTag فرمت فایل را که نشان دهنده‌ی نوع الگوریتمهای به کار گرفته شده برای فشرده‌سازی صدا و… است را مشخص می‌کند. برای استفاده‌ی مورد نظر ما فرمت خاصی که با ثابت WAVE_FORMAT_PCM مشخص می‌گردد و فرمت پی.سی.ام ((PCM)) نامیده می‌شود مناسب است. علاوه بر آن فیلد cbSize برای فرمتهای غیر پی.سی.ام استفاده می‌شود و ما همواره مقدار آن را صفر در نظر خواهیم گرفت.

از آنجا که پردازش این ساختار در برنامه‌نویسی صدا برای پروژه‌ی مورد نظر بارها صورت می‌گیرد و از آنجا که یک شیوه‌ی طراحی شیءگرا (شیوه‌ی ام.اف.سی ((MFC)))برای پیاده‌سازی پروژه در نظر گرفته شده بود و از آنجا که پردازش این ساختار نیاز به برخی محاسبات تکراری (تعیین nBlockAlign و nAvgBytesPerSec) دارد و به چند دلیل دیگر تصمیم گرفته شد که این ساختار و پردازش آن به صورت یک کلاس با نام HSound پیاده سازی گردد که ضمن خودکار نمودن پردازش این ساختار کلاسهایی که به اعمال پخش و ضبط را بر عهده دارند از این کلاس ارث‌بری نموده برنامه نویسی را آسان‌تر و کد به دست آمده را خواناتر نمایند.

تعریف این کلاس به صورت زیر است:

[code lang=”cpp”]
class HSound
{
public:
//constructor and destructor:
HSound();
virtual ~HSound();
//setting wave data:
void SetBitsPerSample(int bps);
void SetSamplesPerSecond(int sps);
void SetNumberOfChannels(int nchan);
//retrieving wave data:
WAVEFORMATEX* GetFormat();
int GetSamplesPerSecond();
int GetBitsPerSample();
int GetNumberOfChannels();
protected:
WAVEFORMATEX m_wfData;
private:
void Update();
};
[/code]

قبل از هر چیز باید به این نکته اشاره شود که این کلاس برای پردازش فرمت پی.سی.ام در نظر گرفته شده، لذا ضمن تعریف مقادیر پیش‌فرض لازم برای این فرمت و استفاده از روشهای خاص این فرمت برای محاسبه‌ی فیلدهای مختلف امکان تغییر فرمت و استفاده از سایر فرمتها را به برنامه‌نویس نمی‌دهد و به منظور پردازش سایر فرمتها ساختار کلاس می‌بایست تغییر کند.

فیلد wBitsPerSample تعداد بیتِ هر نمونه را مشخص میکند که برای فرمت پی.سی.ام فقط می‌تواند یکی از دو مقدار ۸ و ۱۶ را داشته باشد و برای سایر فرمتها مقادیر ممکن بستگی به مشخصات منتشر شده توسط شرکتهای به وجود آورنده و پشتیبانی‌کننده‌ی آنها دارد.

متدی که در پی می‌آیند آن را مقدارگذاری می‌کند (در مورد متد Update و علت فراخوانی آن در ادامه توضیح داده خواهد شد) :

[code lang=”cpp”]
void HSound::SetBitsPerSample(int bps)
{
m_wfData.wBitsPerSample = bps;
Update();
}
[/code]

و متد زیر مقدار انتخاب شده را برمی‌گرداند:

[code lang=”cpp”]
int HSound::GetBitsPerSample()
{
return m_wfData.wBitsPerSample;
}
[/code]

فیلد nSamplesPerSec تعداد نمونه‌ها در هر ثانیه (نرخ نمونه‌برداری) را مشخص می‌کند. برای فرمت پی.سی.ام مقادیر معمول ۸کیلوهرتز (۸۰۰۰)، ۱۱.۰۲۵کیلوهرتز (۱۱۰۲۵)، ۲۲.۰۵کیلوهرتز (۲۲۰۵۰) و ۴۴.۱کیلوهرتز (۴۴۱۰۰) می‌باشد و برای سایر فرمتها مقادیر ممکن بستگی به مشخصات منتشر شده توسط شرکتهای به وجود آورنده و پشتیبانی‌کننده‌ی آنها دارد.

متد مقدارگذاری این فیلد:

[code lang=”cpp”]
void HSound::SetSamplesPerSecond(int sps)
{
m_wfData.nSamplesPerSec = sps;
Update();
}
[/code]

و متد دریافت مقدار آن:

[code lang=”cpp”]
int HSound::GetSamplesPerSecond()
{
return m_wfData.nSamplesPerSec;
}
[/code]

فیلد nChannels تعداد کانالهای موج صوتی را مشخص می‌کنند. صداهای تک کانال (مقدار فیلد برابر با ۱) مونو و صداهای دوکاناله (مقدار فیلد برابر با ۲) استریو خواهند بود.

متد مقدار‌گذاری این فیلد:

[code lang=”cpp”]
void HSound::SetNumberOfChannels(int nchan)
{
m_wfData.nChannels = nchan;
Update();
}
[/code]

و متد دریافت مقدار آن:

[code lang=”cpp”]
int HSound::GetNumberOfChannels()
{
return m_wfData.nChannels;
}
[/code]

هر چند تعداد کانالهای صدا در این کلاس قابل تغییراست اما در کلاسهای مشتق شده همواره الگوریتمها برای موج صوتی تک‌کاناله نوشته شده‌اند و استفاده از آنها برای پردازش موج صوتی دو کاناله نیازمند دستکاری کد این کلاسهاست که به لحاظ استفاده‌ای که ما از این کلاسها نموده‌ایم یک کار اضافی و غیرضروری به نظر می‌رسد.

فیلد nBlockAlign کمینه‌ی تعداد واحد داده را برای فرمت انتخاب شده تعیید می‌کند که اگر فرمت انتخاب شده پی.سی.ام باشد برابر با حاصل ضرب تعداد کانالها (nChannels) در تعداد بیتِ هر نمونه (nBitsPerSample) تقسیم بر تعداد بیتهای موجود در هر بایت (۸) خواهد بود و برای سایر فرمتها بستگی به مشخصات منتشر شده توسط شرکتهای به وجود آورنده و پشتیبانی‌کننده‌ی آنها دارد. فیلد nAvgBytesPerSec نیز تعداد متوسط بایتهای موجود در هر ثانیه‌ی صدا را مشخص می‌کند و برای فرمت پی.سی.ام برابر با تعداد نمونه‌های موجود در هر ثانیه (nSamplesPerSec) در کمینه‌ی تعداد واحد داده (nBlockAlign) خواهد بود و برای سایر فرمتها بستگی به مشخصات منتشر شده توسط شرکتهای به وجود آورنده و پشتیبانی‌کننده‌ی آنها دارد.

متد Update که در کد مقدار‌گذاری سایر فیلد‌ها محاسبات توضیح داده شده‌ی بالا را انجام می‌دهد:

[code lang=”cpp”]
void HSound::Update()
{
m_wfData.nBlockAlign = m_wfData.nChannels*(m_wfData.wBitsPerSample/8);
m_wfData.nAvgBytesPerSec = m_wfData.nSamplesPerSec*m_wfData.nBlockAlign;
}
[/code]

در صورتی که نیاز باشد با ساختار اصلی WAVEFORMATEX کار شود متد زیر مقدار عضوی از کلاس را که از این نوع است باز می‌گرداند:

[code lang=”cpp”]
WAVEFORMATEX* HSound::GetFormat()
{
return &m_wfData;
}
[/code]

در متد سازنده‌ی این کلاس به طور پیش‌فرض برای نمونه‌ی صوتی مورد نظر نرخ نمونه‌برداری ۴۴.۱کیلوهرتز با ۱۶ بیت در هر نمونه در نظر گرفته شده و فرض بر آن است که نمونه‌ی صوتی یک کاناله است:

[code lang=”cpp”]
HSound::HSound()
{
m_wfData.wFormatTag = WAVE_FORMAT_PCM;
m_wfData.cbSize = 0;
SetBitsPerSample(16);
SetSamplesPerSecond(44100);
SetNumberOfChannels(1);
}
[/code]

همچنانکه از روی تعریف کلاس قابل فهم است این کلاس در واقع تمامی اعمال را روی عضو داده‌ی محافظت شده‌ی m_wfData اِعمال می‌نماید و با غیر مستقیم نمودن دسترسی به این عضو داده برای برنامه‌ی استفاده کننده ضمن رعایت اصل پنهانسازی اطلاعات به فراخوانی رویه‌ی Update در متدهای تغییر دهنده‌ی اعضای مرتبط با nBlockAlign و nAvgBytesPerSec تغییرات لازم را به آنها اعمال می‌کند.

۲- انجام پردازش صدا به صورت یک رشته‌ی ((thread)) مستقل

می‌توان با استفاده از توابع کار با صدای ویندوز به گونه‌ای برنامه‌نویسی نمود که نیازی به ایجاد رشته‌های مستقل برای پردازنده‌های صدا نباشد، اما وجود دلایلی از قبیل عدم انعطاف‌پذیری این روش و تک‌وظیفه‌ای شدن برنامه در حین انجام عملیات پردازش صدا باعث می‌شود که روش استفاده از رشته‌های مستقل مورد توجه ما قرار گیرد.

از آغاز در نظر داشتیم که رابط برنامه به گونه طراحی شود که کاربر در هنگام کار با برنامه و انجام عملیاتی نظیر ضبط صدا از عملکرد برنامه مطمئن باشد. به این معنی که مثلاً در حین هنگام صدا با استفاده از یک رابط گرافیکی مانند یک نمایشگر اسیلوسکوپی از این که برنامه واقعاً و به درستی در حال ضبط صدای اوست و یا به لحاظ فاصله‌ی نامتناسب با میکروفن یا عدم اتصال درست آن به کارت صوتی یا خرابی آن بیشتر آنچه ضبط می‌شود سکوت و یا نویز است مطلع گردد. یک روش مناسب برای ایجاد چنین رابطی استفاده از پیام فرستاده شده برای پردازش صدا توسط یک رشته برای فعال شدن یک تابع رسم‌کننده‌ی نمودار اسیلوسکوپی است که نیاز به آن دارد که بدون قطع شدن جریان ضبط پردازش دیگری صورت گیرد. به این منظور و با استفاده از کد اولیه‌ای که در منبع شماره‌ی ۲ به آن اشاره شده کلاسی به نام HSoundRunner را از کلاس HSound اعضای داده و متدهای مرتبط با پردازش صوت و از کلاس ام.اف.سی CwinThread اعضای داده و متدهای لازم برای یک رشته را ارث‌بری می‌کند به صورت زیر تعریف نمودیم:

[code lang=”cpp”]
class HSoundRunner:
public CWinThread,
public HSound
{
public:
DECLARE_DYNCREATE(HSoundRunner)
HSoundRunner();
~HSoundRunner();
void SetBufferSize(int nSamples);
int GetBufferSize();
//this metheods should be overriden:
void AddBuffer();
BOOL Start(WAVEFORMATEX* pwfex=NULL);
BOOL Stop();
//for graphical display:
void SetOwner(CWnd* pWnd);
void ClearOwner(COLORREF crBkColor=0x000000);
public:
//{{AFX_VIRTUAL(HSoundRecorder)
public:
virtual BOOL InitInstance();
//}}AFX_VIRTUAL
protected:
DWORD m_dwThreadID;
int m_iBufferSize; // number of samples per each period
int m_nBuffers; //number of buffers remained to be run
int m_nSamples; //number of samples stored
short* m_pSamples; //samples stored
BOOL m_bRunning; //indicated running or not
//if graphical display is intended set this value
CWnd* m_pOwner;
void DrawBuffer(int nSamples, short* pSamples, COLORREF crBkColor=0x000000, COLORREF crLineColor=0x00FF00);
};
[/code]

اعضای داده‌ی این کلاس در روند انجام عملیات توسط کلاسهای مشتق شده از آنها نقش خود را نشان خواهند داد و به عنوان نمونه عضو داده‌ی m_iBufferSize که نشان دهنده‌ی آن است که بعد از ضبط با پخش چند نمونه تابع پزدازنده‌ی پیام در کلاس پنجره‌ی کنترل کننده باید فراخوانی شود در این هیچکدام از متدهای این کلاس نقش عملی پیدا نمی‌کند و فقط مقدارگذاری آن از طریق متد SetBufferSize و دیافت مقدار فعلی آن از طریق GetBufferSize صورت می‌گیرد:

[code lang=”cpp”]
void HSoundRunner::SetBufferSize(int nSamples)
{
m_iBufferSize=nSamples;
}
int HSoundRunner::GetBufferSize()
{
return m_iBufferSize;
}
[/code]

عضو داده‌ی m_dwThreadID مقدار شناسه‌ی رشته‌ی ایجاد شده را که در کلاس سازنده با فراخوانی CreateThead ایجاد می‌شود در بر می‌گیرد که در کلاسهای مشتق شده برای کار با فراخوانیهای ای.پی.آی پردازش صدا کاربرد پیدا می‌کند. مقدار این عضو داده در متد بازنویسی ((overriden)) شده‌ی InitInstance و به صورت زیر تعیین می‌گردد:

[code lang=”cpp”]
BOOL HSoundRunner::InitInstance()
{
m_dwThreadID = ::GetCurrentThreadId();
return TRUE;
}
[/code]

اعضای داده‌ی m_nSamples و m_pSamples به ترتیب تعداد نمونه‌های ضبط شده یا آماده برای پخش و آرایه‌ی حاوی آنها -که به لحاظ آسان‌تر شدن کار با کتابخانه‌ای که برای پردازش سیگنال صحبت استفاده شده و همسان با آن از نوع short در نظر گرفته شده- را نشان می‌دهند. این دو عضو داده به صورت پویا پس از انجام عملیات ضبط مقدارگذاری می‌شوند و برای عملیات پخش باید قبلاً مقدارگذاری شده باشند.

آنچه این کلاس انجام می‌دهد غیر از ایجاد یک رشته برای انجام پردازش صدا فراهم آوردن روشی برای نمایش اسیلوسکوپی صداست که از طریق متد DrawBuffer انجام می‌شود.

این متد در توابع پیامهای مربوط به پردازش صوت خود به خود فراخوانی می‌گردد و در صورتی که مقدار m_pOwner اشاره‌گر به یک پنجره یا کنترل انتخاب شود (توسط متد SetOwner) آرایه‌ی ورودی که معمولاً یک تکه‌ی تازه ضبط با پخش شده از کل صداست متناسب با طول و عرض پنجره‌ی مورد نظر بر روی آن کشیده می‌شود. این کار با ایجاد یک ابزار متن ((Device Context [DC])) و یک بیتمپ متناسب با ابزار متن پنجره‌ی مورد نظر، کشیدن طرح لازم با استفاده از این دو و در نهایت نمایش تصویر ایجاد شده بر روی پنجره‌ی مقصد و مطابق با کد زیر انجام می‌شود:

[code lang=”cpp”]
void HSoundRunner::DrawBuffer(int nSamples, short* pSamples, COLORREF crBkColor, COLORREF crLineColor)
{
if(m_pOwner==NULL)
return;
CRect rc;
m_pOwner->GetClientRect(&rc);
int iWidth=rc.Width();
int iHeight=rc.Height();
CDC* pDC=m_pOwner->GetDC();
CBitmap Bitmap;
Bitmap.CreateCompatibleBitmap(pDC, iWidth, iHeight);
CDC dc;
dc.CreateCompatibleDC(pDC);
dc.SelectObject(&Bitmap);
CBrush Brush(crBkColor);
dc.FillRect(&rc,&Brush);
CPen Pen(PS_SOLID,1,crLineColor);
dc.SelectObject(&Pen);
dc.SetBkColor(crBkColor);
if(GetBitsPerSample()==16)
{
float fx=iWidth/float(nSamples);
float fy=float(iHeight/32767.0);
dc.MoveTo(0, iHeight/2);
int i=0;
for(float f=0; fBitBlt(0, 0, iWidth, iHeight, &dc, 0, 0, SRCCOPY);
}
}
[/code]

متد ClearBuffer یک روش قابل دسترسی توسط برنامه برای پاک کردن پنجره‌ی مورد استفاده به وجود می‌آورد و شامل یک فراخوانی متد محافظت شده‌ی DrawBuffer با یک آرایه‌ی به طول صفر است:

[code lang=”cpp”]
void HSoundRunner::ClearOwner(COLORREF crBkColor)
{
DrawBuffer(0,NULL,crBkColor);
}
[/code]

عضو داده‌ی m_nBuffers تعداد بافرهای اختصاص داده شده و استفاده نشده را نشان می‌دهد که در متد AddBuffer این کلاس و بازنویسی شده‌ی آن برای کلاسهاس مشتق شده مقدار آن به ازای هر بار فراخوانی یک واحد افزوده می‌شود و در پیامهای پردازش صدا که از طرف سیستم عامل فعال می‌شوند و نشانگر استفاده شدن بافر مورد نظر است (در کلاسهای مشتق شده) یک واحد کاهش می‌یابد. در نهایت صفر نبودن این عضو داده نشانگر استفاده‌ی ناکامل از بافرهای اختصاص داده شده (معادل با کامل انجام نشدن فرایند ضبط یا پخش) است که می‌تواند پردازش مناسب برای آن صورت گیرد:

[code lang=”cpp”]
void HSoundRunner::AddBuffer()
{
m_nBuffers++;
}
[/code]

عضو داده‌ی m_bRunning به منظور تشخیص این که برنامه در حال اجرای عملیات پردازش صداست و یا نه به منظور جلوگیری از ایجاد اشکال با فراخوانیهای تکراری در نظر گرفته شده که در هنگام آغاز عملیات مقدار آن برابر با TRUE و در پایان آن برابر با FALSE انتخاب می‌گردد. همچنان که در ادامه توضیح داده خواهد شد کلاسهای مشتق شده متدهایی از لحاظ نام متناسب با عملی که برای آن در نظر گرفته شده‌اند (ضبط یا پخش) برای بازگرداندن این مقدار به برنامه‌نویس کاربر این کلاسها دارند:

[code lang=”cpp”]
BOOL HSoundRunner::Start(WAVEFORMATEX* pwfex)
{
if(m_bRunning)
return FALSE;
if(pwfex != NULL)
m_wfData = *pwfex;
return TRUE;
}
BOOL HSoundRunner::Stop()
{
if(m_bRunning)
{
m_bRunning=FALSE;
Sleep(500);
return TRUE;
}
return FALSE;
}
[/code]

فراخوانی استاندارد Sleep در متد Stop برای مصرف کامل بافر ایجاد شده انجام می‌گردد. در ضمن متد Start روشی برای جایگزینی مقدار پیش‌فرض m_wfData (عضو کلاس Hsound) با مقدار جدید در اختیار می‌گذارد.

در متد سازنده اعضای داده با مقادیر پیش‌فرض مقدارگذاری شده و رشته‌ی مورد نظر با فراخوانی CreateThead ایجاد می‌گردد:

[code lang=”cpp”]
HSoundRunner::HSoundRunner()
{
m_iBufferSize= 2048;
m_nBuffers = 0;
m_bRunning = FALSE;
m_nSamples=0;
m_pSamples=NULL;
m_pOwner=NULL;
CreateThread();
}
[/code]

در متد ویرانگر ((destructor)) نیز در صورتی که شیء از نوع این کلاس در حال انجام عملیات پردازش صوت باشد متوقف خواهد شد:

[code lang=”cpp”]
HSoundRunner::~HSoundRunner()
{
if(m_bRunning)
Stop();
}
[/code]

به لحاظ آن که آرایه‌ی داده‌ها (m_pSamples) در این کلاس ایجاد نمی‌گردد در متد ویرانگر آزاد شدن آن پیشبینی نشده است.

۳- ضبط صدا

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

[code lang=”cpp”]
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()
};
[/code]

برای استفاده از این کلاس کافی است متدهای Start و Stop آن فراخوانی گردند ولی درک کامل نحوه‌ی عملکرد آن نیاز به برخی مقدمات دارد.

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

[code lang=”cpp”]
MMRESULT waveInOpen(LPHWAVEIN phwi, UINT uDeviceID, LPWAVEFORMATEX pwfx, DWORD dwCallback, DWORD dwCallbackInstance, DWORD fdwOpen);
[/code]

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

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

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

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

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

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

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

[code lang=”cpp”]
MMRESULT waveInStart(HWAVEIN hwi);
[/code]

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

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

[code lang=”cpp”]
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; } [/code]

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

[code lang=”cpp”]
MMRESULT waveInPrepareHeader(HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh);
[/code]

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

[code lang=”cpp”]
typedef struct {
LPSTR lpData;
DWORD dwBufferLength;
DWORD dwBytesRecorded;
DWORD dwUser;
DWORD dwFlags;
DWORD dwLoops;
struct wavehdr_tag * lpNext;
DWORD reserved;
} WAVEHDR;
[/code]

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

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

[code lang=”cpp”]
MMRESULT waveInAddBuffer(HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh);
[/code]

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

[code lang=”cpp”]
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();
}
[/code]

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

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

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

[code lang=”cpp”]
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–;
}
[/code]

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

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

[code lang=”cpp”]
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;
}
[/code]

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

[code lang=”cpp”]
short* HSoundRecorder::GetSamples(int& nSamples)
{
nSamples=m_nSamples;
return m_pSamples;
}
[/code]

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

[code lang=”cpp”]
HSoundRecorder::~HSoundRecorder()
{
if(m_pSamples)
delete []m_pSamples;
}
[/code]

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

[code lang=”cpp”]
HSoundRecorder::HSoundRecorder()
{
m_hWaveIn =NULL;
}
[/code]

۴- پخش صدا

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

[code lang=”cpp”]
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;
};
[/code]

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

[code lang=”cpp”]
void HSoundPlayer::SetData(int iSize, short* pData)
{
m_nSamples=iSize;
m_pSamples=pData;
}
[/code]

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

[code lang=”cpp”]
BOOL HSoundPlayer::Start(int iSize, short* pData, WAVEFORMATEX* format)
{
SetData(iSize, pData);
return Start(format);
}
[/code]

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

[code lang=”cpp”]
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; } [/code]

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

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

[code lang=”cpp”]
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();
}
[/code]

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

[code lang=”cpp”]
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;
}
}
[/code]

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

[code lang=”cpp”]
BOOL HSoundPlayer::Stop()
{
if(HSoundRunner::Stop())
return (::waveOutReset(m_hWaveOut)!=0);
return FALSE;
}
[/code]

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

[code lang=”cpp”]
HSoundPlayer::HSoundPlayer()
{
m_hWaveOut = NULL;
}
[/code]

۵- کتابخانه‌ی پردازش صوت

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

۶- منابع فصل

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

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