北京理工大学 20981 陈罡
继续上面一篇的内容,本篇已经假定你已经可以从mp3文件中顺利的解码出pcm码流了。
然后开始我们下一步的工作——播放pcm码流。
在这之前,我们必须熟悉一下微软的几个用于播放pcm码流的函数,如果只是用用
sndPlay之类的简单函数,又不想耽误时间的朋友就可以不必往下看了。偶用的方法
是比较麻烦的方法,呵呵,但是效果是非常不错的可以修改后用于流媒体中的音频部分
播放。
总的来说,微软定义的可用于播放pcm码流的waveOutXxxx api有如下几个:
MMRESULT waveOutOpen(LPHWAVEOUT phwo, UINT uDeviceID, LPWAVEFORMATEX pwfx,
DWORD dwCallback, DWORD dwInstance, DWORD fdwOpen );
MMRESULT waveOutPrepareHeader(HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh);
MMRESULT waveOutWrite(HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh);
MMRESULT waveOutUnprepareHeader(HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh);
MMRESULT waveOutClose(HWAVEOUT hwo);
当然了,还有很多其它的诸如waveOutSetVolume,waveOutPause,waveOutReset等等
这里就不多说了,可以查一下ppc的帮助文档,里面有非常详细的说明。
下面就把播放pcm码流的基本流程说明一下:
(1)打开设备
为了让win mobile设备可以播放pcm码流,我们必须设置如下属性:
format - 格式当然就是WAVE_FORMAT_PCM
channels - 声道个数,可以是单声道(mono)或者双声道(stereo)
sample_rate - 采样率,目前win mobile支持:8.0 kHz, 11.025 kHz, 22.05 kHz, 44.1 kHz
bits_per_sample - 每个采样点占用的位数,通常情况下是8或者16(现在16的比较多了)
这些属性是必须设置的,都作为WAVEFORMATEX结构体的成员进行设置
wf.wFormatTag = format;
wf.nChannels = channels;
wf.nSamplesPerSec = sample_rate * 1000;
wf.wBitsPerSample = bits_per_sample;
下面这个属性是自动计算出来的,块字节对齐
wf.nBlockAlign = wf.nChannels * wf.wBitsPerSample / 8;
这个是每秒平均字节数,通常是块字节对齐数乘以采样率得到的
wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign;
wf.cbSize = 0;
现在就可以调用waveOutOpen函数了,这里需要传入一个HWAVEOUT类型的指针,这个指针
很重要,以后的所有waveOutXxxx函数的第一个参数都要传入HWAVEOUT类型的变量。
第二个参数就是设备id了,通常情况下添0就可以了,也有一个比较稳妥的方法:
for (UINT id = 0; id < waveOutGetNumDevs(); id++) {
if (waveOutOpen(&hwo, id, &wf, 0, 0, CALLBACK_NULL) == MMSYSERR_NOERROR) {
break;
}
}
经过这样枚举,通常也会得到一个id。win mobile的手机一般这个device id都是0。
对于mobilinux来说,就是两个了/dev/dsp和/dev/dsp16。呵呵,不过这是两个不同的系统
了。跑题了。。。继续。。。
注意,这里的fdwOpen参数填入的是CALLBACK_NULL,是用于检测是否能够打开设备的。
微软搞得这个waveOutXxxx系列函数是面向事件的。可以自动像程序发送消息,程序可以对
这些消息进行响应。仔细查看fdwOpen参数的文档,可以发现waveOutOpen函数支持回调函数、
windows消息、事件、线程等类型的响应。
对于回调函数而言,就是声明一个全局或者静态的函数,来响应打开设备(WOM_OPEN)、
数据播放完毕(WOM_DONE)、以及关闭设备(WOM_CLOSE)的消息即可。
例如:
void CALLBACK MyWaveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance,
DWORD dwParam1, DWORD dwParam2)
{
switch(uMsg) {
case WOM_OPEN: // connection opened
break;
case WOM_DONE: // buffer finished playing
break;
case WOM_CLOSE: // connection closed
break;
}
}
然后可以如下这样调用waveOutOpen函数:
waveOutOpen(&hwo, id, &wf, (DWORD) MyWaveOutProc, 0, CALLBACK_FUNCTION);
对于希望窗口接收到指定消息的朋友,可以如下调用:
waveOutOpen(&hwo, id, &wf, (DWORD) hwnd, 0, CALLBACK_WINDOW);
这里的hwnd就是需要接收WOM_OPEN,WOM_DONE,WOM_CLOSE消息的窗口句柄。
对于希望响应某个事件的朋友来说,定义一个event也是一个不错的选择:
done_event = CreateEvent(NULL, FALSE, FALSE, TEXT("DONE_EVENT")) ;
waveOutOpen(&hwo, WAVE_MAPPER, &wf, (DWORD)(done_event),0, CALLBACK_EVENT) ;
这里的WAVE_MAPPER意思是让系统自动寻找合适的设备ID填入,这是一个不错的选择。
综上所述,如果waveOutOpen返回值是MMSYSERR_NOERROR,就代表音频设备已经成功打开了。
(2)开始播放前的准备工作
这一步就可以把先前libmad库解码出来的pcm码流准备好了,该派上用场了。pcm码流总有
数据的长度、数据的指针之类的需要设置给waveOutXxxx函数,因此引入了WAVEHDR结构体,
它的lpData就是指向pcm数据块的指针,dwBufferLength就是数据块的大小。
whdr.lpData = new char[waveFile.GetLength()];
whdr.dwBufferLength = waveFile.GetLength();
whdr.dwUser = 0;
whdr.dwFlags = 0;
whdr.dwLoops = 0;
whdr.dwBytesRecorded = 0;
whdr.lpNext = 0;
whdr.reserved = 0;
// 这一步就是从wave文件中读取数据了。这一步可以改为从libmad解码器中读取数据。
waveFile.Read(whdr.lpData, whdr.dwBufferLength);
这些准备好以后,就可以调用waveOutPrepareHeader函数,告诉系统我们要开始播放了。
waveOutPrepareHeader(hwo, &whdr, sizeof(WAVEHDR));
注意,这一步是不可少的
(3)开始播放!
waveOutWrite(hwo, &whdr, sizeof(WAVEHDR));
就着么简单,只要能够顺利打开设备,我想走到这一步应该毫不费力。
(4)播放完毕后,需要释放资源
waveOutUnprepareHeader(hwo, &whdr, sizeof(WAVEHDR));
waveOutClose(hwo);
delete [] whdr.lpData;
具体在做的时候可能会有所不同,上面的例子只是播放wave文件的,在播放连续的
pcm码流的时候,最好能够如下图这样设计两个线程:
解码线程用于读取mp3文件的原始数据,然后对数据进行解码,然后将解码后的数据存入一个公共的pcm队列。
另外一个线程专门负责读取这个pcm数据块队列,然后把声音播放出来。如果用设计模式的思路来看的话,也可以把解码线程看作是“生产者”,把播放线程看作是“消费者”。二者相对独立,生产者只负责往队列中写入数据,消费者只负责读取队列中的数据;如果队列满了,则生产者可以等待500毫秒或者1 秒钟,然后继续向队列中写入数据。
为了清晰起见,我把播放线程的关键部分代码贴出来,希望对大家有用:
// 声音设备初始话函数
bool CM5PCMOutThd::Init(int channels, int sample_rate, bool * is_playing)
{
MMRESULT mm_result ;
m_is_playing = is_playing ;
m_wave_format.wFormatTag = WAVE_FORMAT_PCM ;
m_wave_format.nChannels = channels ;
m_wave_format.nSamplesPerSec = sample_rate ;
m_wave_format.wBitsPerSample = 16 ;
m_wave_format.nBlockAlign = m_wave_format.wBitsPerSample * m_wave_format.nChannels / 8 ;
m_wave_format.cbSize = 0 ;
m_wave_format.nAvgBytesPerSec = m_wave_format.nSamplesPerSec * m_wave_format.nBlockAlign ;
// 注意,这里我图省事,采用了event的通知的方法
m_play_event = CreateEvent(NULL, FALSE, FALSE, TEXT("DONE_EVENT")) ;
mm_result = waveOutOpen(&m_wave_out_hdl,WAVE_MAPPER,
&m_wave_format,
(DWORD)(m_play_event),0, CALLBACK_EVENT) ;
return (mm_result == MMSYSERR_NOERROR) ? true : false ;
}
// 从队列中读取数据并播放的核心函数
void CM5PCMOutThd::PcmOutProc()
{
MMRESULT mm_res ;
PCM_BLOCK * pcm_blk ;
waveOutSetVolume(m_wave_out_hdl, 0x0ffffffff) ;
do {
// read pcm block from pcm queue
// 这里是读取队列中的数据了,多线程嘛,必须要做同步处理
EnterCriticalSection(m_cs_ptr) ;
pcm_blk = m_pq_ptr->GetDataBlock() ;
LeaveCriticalSection(m_cs_ptr) ;
// 如果没有取到数据,则继续下一次循环
if(!pcm_blk) {
Sleep(0) ;
continue ;
}
// 取到数据以后,开始准备WAVEHDR结构体
ZeroMemory(&m_wave_header, sizeof(WAVEHDR)) ;
m_wave_header.lpData = (LPSTR)(pcm_blk->data) ;
m_wave_header.dwBufferLength = pcm_blk->length ;
m_wave_header.dwUser = 0;
m_wave_header.dwFlags = 0;
m_wave_header.dwLoops = 0;
m_wave_header.dwBytesRecorded = 0;
m_wave_header.lpNext = 0;
m_wave_header.reserved = 0;
// 这里是准备pcm码流
mm_res = waveOutPrepareHeader(m_wave_out_hdl, &m_wave_header, sizeof(WAVEHDR));
if (mm_res != MMSYSERR_NOERROR) break ;
// 这里是开始播放
mm_res = waveOutWrite(m_wave_out_hdl, &m_wave_header, sizeof(WAVEHDR));
if (mm_res != MMSYSERR_NOERROR) break ;
// wait for audio to finish playing
// 这里是等待播放结束,这就非常类似于阻塞模式的linux声音播放机制了
while (!(m_wave_header.dwFlags & WHDR_DONE)) {
WaitForSingleObject(m_play_event, INFINITE);
}
// Clean up
// 当前pcm块播放完毕后一定不要忘记Unprepare这个WAVEHDR
mm_res = waveOutUnprepareHeader(m_wave_out_hdl, &m_wave_header, sizeof(WAVEHDR));
if (mm_res != MMSYSERR_NOERROR) break ;
// 这里就是循环终止条件判断了,准备进入下一个pcm数据块的读取和播放操作
}while(*m_is_playing) ;
m_is_thread_init = false ;
}
顺便提一句,如果是libmad解码mp3文件的话,必须采用上面图示中所提到的方法,开启两个线程;
一个专门解码,一个专门播放;解码线程和播放线程共享一个pcm块队列。只有如此,才能流畅的在ppc
上播放mp3音乐,如果是按照传统流程,解码一帧、播放一帧的话,就200mhz的处理器来说根本不行,
CPU占用率大于70%,而且声音也是会一跳一跳的。
这一点是偶的血泪教训,各位看官一定要牢记在心啊!
最后,我已经实现上图示的所有功能,这个设计思路是完全没有问题的。代码比较繁琐,不易看懂,
为了简单起见,就把一个的playwav.zip给发上来,感兴趣的朋友可以下载、测试: