Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1534892
  • 博文数量: 114
  • 博客积分: 10010
  • 博客等级: 上将
  • 技术积分: 1357
  • 用 户 组: 普通用户
  • 注册时间: 2006-11-19 18:13
文章分类
文章存档

2010年(8)

2009年(9)

2008年(27)

2007年(62)

2006年(8)

我的朋友

分类: WINDOWS

2007-12-08 02:46:44

北京理工大学  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给发上来,感兴趣的朋友可以下载、测试:
文件: PlayWav.zip
大小: 35KB
下载: 下载
阅读(6074) | 评论(6) | 转发(1) |
给主人留下些什么吧!~~

chinaunix网友2009-03-25 11:06:25

针对你说的这句: 顺便提一句,如果是libmad解码mp3文件的话,必须采用上面图示中所提到的方法,开启两个线程;一个专门解码,一个专门播放;解码线程和播放线程共享一个pcm块队列。只有如此,才能流畅的在ppc 上播放mp3音乐,如果是按照传统流程,解码一帧、播放一帧的话,就200mhz的处理器来说根本不行, CPU占用率大于70%,而且声音也是会一跳一跳的。 其实不管处理器速度有多快,串行地解一帧播放一帧都是不行的,因为解一帧肯定要花时间,这个时间就是播放断掉的一段,人耳对声音是很敏感的,这个断掉的时间不管有多短都能听到。所以一个线程解,一个线程播放是必需的,即使处理器速度非常快。还有,两个线程共享的buffer至少是ping pong buffer,可以解线程去写一个buffer,播放线程去读另一个buffer,播放完后交换buffer

chenwayne2008-02-11 09:33:03

flyindark,您好: 您说的方式是采用callback函数的方式,把什么时候调用waveOutWrite交给 win mobile系统来处理了,系统会自动在需要数据的时候调用它;我的方法是通过event唤醒的方式,如果还在播放中,则处于阻塞状态,等待事件到来,一旦收到了读取的事件,就从缓冲区开始下一次写入。从本质上来说, 让系统回调也是由系统启动和同步播放线程,生产者消费者的格局没有改变; 代码上确实可以简化不少,如果只是为win mobile做开发的话,是完全没问题 的,但是从可移植性来说,如果手机系统不提供回调的机制的话,基于线程 同步的播放方式提供了一种移植的可行性,如mobilinux平台只提供pcm写入 操作,不提供回调函数。

flyindark2008-02-07 15:41:18

补充一下,用一个线程的时候是使用的CALLBACK_FUNCTION,在回调里处理waveOutWrite使用哪一个WAVEHDR。

flyindark2008-02-07 15:38:39

对楼主关于2个线程的说法有点疑问。楼主的说法不正确,因为我手上有在一个线程里解码并实现设备输出的代码,当然每解一帧,然后向设备输出会造成停顿。大概思路是设置几个WAVEHDR,解开的PCM使用这几个WAVEHDR循环向设备写.对于每一帧设备都有固定的持续时间,写设备的API是立即返回的。楼主可以试一试,这种方法可以减少一个线程,并降低程序逻辑复杂度。