用Multi-Media Library制作流式音频播放器

最近在制作IP话务坐席客户端,在这个系统里,需要用声卡去播放从服务器传来的音频数据,因为电话通讯是实时的,所以不可能等到音频数据都传完了再播放(废话),所以这个播放过程应该是近似于流媒体的方式,有多少数据就播放多少数据(还是废话)。

好吧,废话少说,切入正题。

由于上述原因,我只能选择用低级波形API去播放音频数据,即使用Multi-Media Library。这是WINDOWS下最接近底层的音频API,当然,我们还有一个选择DirectSound,不过那个用起来没有Multi-Media Library那么方便,而且我也不需要用到那么高的特性。

在开始之前,我们先来了解一下波形数据的格式和特性,有这么几个概念需要先熟悉:“声道”、“采样率”、“样本位率”。

“声道”的意思很容易理解,我们通常听说的“单声道”、“双声道”、“环绕立体声”就是声道的概念,简单点说,就是有多少个音源(抱歉,我不知道这个解释是不是十分精确,因为我并不是搞音频工程学的)。 “采样率”的意思是每秒钟采集多少个声音样本,越多越清晰,而采集到的数据也就越多;反之,越小越模糊,采集到的数据也就越少。采样率的单位是hz(赫兹),8000hz就代表每秒采集8000个样本,更高的采样可以得到更清晰的声音,但是对采样设备的能力和网络的速度也就要求更高些。“样本位率”的意思是每个样本占多少位的数据量,一般有8位、16位、32位(浮点)这三个选择,位率高则采样数据精确,位率低则采样数据有失真。

有了上述概念,我们就可以计算出波形数据的数据量了,假如说有一段波形音频数据,是双声道,44100hz的采样率,16位的样本位率(一般CD中都是这样的格式),那么,这个音频数据每秒的数据量就是

(2 * 44100 * 16) / 8 = 176400字节

也就是说,计算公式是:

(声道数 * 采样率 * 样本位率) / 8 = 每秒字节数

为什么除8?一个字节占8位嘛

为了描述上面的内容,Multi-Media Library定义了一个struct

typedef struct tWAVEFORMATEX

{

    WORD        wFormatTag;        
/**//* 格式类别 */


    WORD        nChannels;         
/**//* 声道数 */

    DWORD       nSamplesPerSec;    
/**//* 采样率 */

    DWORD       nAvgBytesPerSec;   
/**//* 平均每秒字节数 */

    WORD        nBlockAlign;       
/**//* 块对齐 */

    WORD        wBitsPerSample;    
/**//* 采样位率 */

    WORD        cbSize;            
/**//* 扩展定义,为0即可 */

}
WAVEFORMATEX, *PWAVEFORMATEX, NEAR *NPWAVEFORMATEX, FAR * LPWAVEFORMATEX;

wFormatTag指的是格式类别,其值在MMREG.H头文件中定义,下面是部分格式的摘录:

/**//* WAVE form wFormatTag IDs */

#define WAVE_FORMAT_UNKNOWN           0x0000 /* Microsoft Corporation */

#define WAVE_FORMAT_ADPCM             0x0002 /* Microsoft Corporation */

#define WAVE_FORMAT_IEEE_FLOAT        0x0003 /* Microsoft Corporation */

#define WAVE_FORMAT_VSELP             0x0004 /* Compaq Computer Corp. */

#define WAVE_FORMAT_IBM_CVSD          0x0005 /* IBM Corporation */

#define WAVE_FORMAT_ALAW              0x0006 /* Microsoft Corporation */

#define WAVE_FORMAT_MULAW             0x0007 /* Microsoft Corporation */

#define WAVE_FORMAT_DTS               0x0008 /* Microsoft Corporation */

#define WAVE_FORMAT_DRM               0x0009 /* Microsoft Corporation */

#define WAVE_FORMAT_OKI_ADPCM         0x0010 /* OKI */

#define WAVE_FORMAT_DVI_ADPCM         0x0011 /* Intel Corporation */

#define WAVE_FORMAT_IMA_ADPCM         (WAVE_FORMAT_DVI_ADPCM)

在本文中,我使用WAVE_FORMAT_ALAW格式,因为我使用的程控交换机输出的就是这种格式。

播放音频数据的API有如下几个,并不多,也很简单。

waveOutOpen – 打开波形输出设备

waveOutPrepareHeader – 准备播放缓冲区

waveOutUnprepareHeader – 取消播放缓冲区

waveOutWrite – 将数据写入波形输出设备

waveOutReset – 波形输出设备复位(清除正在播放的数据,停止播放)

waveOutPause – 波形输出设备暂停(暂停播放)

waveOutRestart – 波形输出设备恢复(继续播放)

waveOutClose – 关闭波形输出设备

处理顺序大致上就是:

waveOutOpen -> waveOutPrepareHeader -> waveOutWrite -> waveOutUnprepareHeader -> waveOutClose

不过有个问题,你几乎不可能一次性就将所有要播放的数据全部写入,流模式数据的播放就更不可能,因此,必须将要播放的波形数据分批分次的写入设备。不过这又带来另一个问题,如果分批次的写入,在第一个数据播放完后接着写入下一个数据的话,无论你的计算机有快,都会有暂时的停顿,那么听起来,声音就一卡一卡的。

这个问题当然可以解决,否则便不会有此文了,相信所有播放器都是用类似的方式解决的。waveOutWrite函数有个特点,即音频数据写完后函数会立即返回,并不等待声音播放完毕,而且如果此时立即再写入另一个数据,那么当第一个数据播放完后,系统会自动播放第二个数据,中间不会有停顿。所以,我们可以建立一个双缓冲(或者多缓冲也可以),一次性写入两段数据,当第一段缓冲区数据播放完毕时立即用第三段据去填充它,此时第二缓冲区数据正在播放,所以不会停顿,当第二段数据播放完毕后第三段数据已经就绪,所以也不会停顿,此时再用第四段数据去填充第二缓冲区,第三段数据播放完毕后再用第五段数据去填充第一缓冲区……


流程如下:



那么,如何得知某一段数据播放完毕了呢?别急,先来看看waveOutOpen的原形

MMRESULT waveOutOpen(

LPHWAVEOUT     phwo,     

UINT_PTR       uDeviceID,

LPWAVEFORMATEX pwfx,     

DWORD_PTR      dwCallback,

DWORD_PTR      dwCallbackInstance,

DWORD          fdwOpen   

);


这个函数用来打开波形输出设备,如果成功,将返回MMSYSTEM_NOERROR,否则返回错误代码。

phwo是返回的设备句柄,如果函数返回成功,这个参数将会返回打开的设备句柄,后面的操作都需要用到这个设备句柄。

uDeviceID 是要打开的设备ID,因为系统中可以拥有多个波形输出设备,用此参数来指定要打开哪一个设备,如果要打开默认的波形输出设备,指定为WAVE_MAPPER即可。

pwfx 就是前面介绍的WAVEFORMATEX结构体,指定要在这个设备上播放什么格式的波形数据。

dwCallback 指定设备的回调,可以是回调函数的指针,也可以是事件句柄,也可以是窗口的句柄,或者线程ID。

dwCallbackInstance 指定回调时的用户数据,可以指定任意数据,数据将在回调产生时作为参数传入(窗口回调的情况下此数据不可用)

fdwOpen 打开设备用的标志,具体有哪些值可用请参考MSDN,我这里只用CALLBACK_FUNCTION,表示用回调函数的方式执行回调。

再来看看waveOutPrepareHeader函数的原形

MMRESULT waveOutPrepareHeader(

HWAVEOUT hwo,

LPWAVEHDR pwh,

UINT cbwh     

);


这个函数用来指定设备的播放缓冲,在播放波形数据前,必须先使用这个函数来指定播放缓冲。

hwo仍然是设备的句柄

pwh是播放缓冲的结构体指针,下面将详细介绍它

cbwh是上面缓冲结构体的字节数,用sizeof计算即可

pwh是WAVEHDR结构体的指针,WAVEHDR的原形是:

typedef struct {

    LPSTR      lpData;

    DWORD      dwBufferLength;

    DWORD      dwBytesRecorded;

    DWORD_PTR dwUser;

    DWORD      dwFlags;

    DWORD      dwLoops;

   
struct wavehdr_tag *
lpNext;

    DWORD_PTR reserved;

}
WAVEHDR;

lpData

是要播放的数据块的指针

dwBufferLength是要播放的数据块的字节数

dwBytesRecorded是已录音的字节数(仅在录音时用)

dwUser我们可以在此指定任意数据

dwFlags是系统指定的状态值,在调用waveOutPrepareHeader前,必须将它置0

dwLoops是循环播放的次数,这里我用不着,置0即可

lpNext和reserved都是备用字段,置NULL

下面要介绍waveOutWrite函数,原形如下:

MMRESULT waveOutWrite(

HWAVEOUT hwo,

LPWAVEHDR pwh,

UINT cbwh     

);


这个函数用来将播放缓冲中的数据发送到波形输出设备,其参数和waveOutPrepareHeader是一样的,需要注意的是:lpData,它指定的指针位置在调用waveOutPrepareHeader后不可以再变化,但是我们仍然可以改变指针所指位置的数据;dwBufferLength的值可以改变,但是必须比调用waveOutPrepareHeader时指定的值小,也就是说,可以播放比指定的缓冲小的数据。

剩下的几个函数由于都很简单或者和上面的函数类似,我这里就不再浪费口舌了。

如果设备的状态发生变化,如设备已打开、设备播放完毕,设备已关闭,系统就会执行回调,我这里只介绍函数回调的情形,其回调函数的原形如下:

void CALLBACK waveOutProc(

HWAVEOUT hwo,     

UINT uMsg,        

DWORD dwInstance,

DWORD dwParam1,   

DWORD dwParam2    

);

这里有几个参数是很重要的,nMsg告诉你现在发生了什么事情,WOM_OPEN表示设备已打开,WOM_DONE表示设备刚播放完一块缓冲,WOM_CLOSE表示设备已被关闭;dwInstance是你在打开设备时指定的dwCallbackInstance值;Param1仅在nMsg的值为WOM_DONE时有效,指示当前播放完的是哪一块缓冲。

如此一来,我们就可以得知哪一块缓冲播放完毕,并立即就可以准备好后续缓冲块。

在程序中,我使用一个波形缓冲来保存接收到的数据,开启4个播放缓冲。为了让波形缓冲中的数据及时进入播放缓冲,我开启了一个线程,只要波形缓冲中有足够的数据可以播放且播放缓冲没有用完,就往里填充数据,播放完一个播放缓冲后,就立即继续填充它以保证流畅的播放效果,在这里,我使用了事件对象来判断缓冲是否已播放完毕。

下面给出播放类的源码(Borland C++ Builder):
Player.H

1//---------------------------------------------------------------------------
2
3 #ifndef PlayerH
4#define PlayerH

5
6#include <basepch.h>

7#include <mmsystem.h>
8#include <MMREG.H>
9#include <SyncObjs.HPP>
10//---------------------------------------------------------------------------
11
12class TPlayer : TObject
13
{
14private
:
15    TCriticalSection *Lock;//临界区锁

16
17    void* WAVEBUFFER;//波形数据缓冲区
18    int BUFFERLENGTH;//波形数据缓冲字节数
19
20    int WAVEBUFFERCOUNT;//播放缓冲块总数量
21    int BufferUseCount;//播放缓冲块当前使用数量
22    int CurrentBufIndex;//当前播放缓冲块索引
23    PWAVEHDR WaveHdr;//缓冲块指针
24    void* SampleBuffer;//缓冲块波形样本数据指针
25
26    int MINSAMPLESIZE;//最小播放样本字节
27    int MAXSAMPLESIZE;//最大播放样本字节
28
29    HWAVEOUT hWave;//波形播放设备句柄
30    WAVEFORMATEX Format;//波形格式
31    HANDLE hEvent;//事件句柄
32
33    Boolean RUN;//线程运行开关
34    HANDLE hThread;//线程句柄
35
36    static DWORD WINAPI PlayThread(PVOID Param);//播放线程函数
37    DWORD __fastcall PlayerThread();//播放线程函数
38
39    void __fastcall RemoveLeftData(int Length);//从波形数据缓冲区中移走已播放过的数据
40
41public:
42    __fastcall TPlayer(WAVEFORMATEX Format, BYTE BufferCount = 4, int MinSampleSize = -1, int MaxSampleSize = -1
);
43    __fastcall ~
TPlayer();
44    static void CALLBACK OutputCallback(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2);//回调函数

45
46    void __fastcall Initialize(HWAVEOUT WaveHandle);//初始化
47    void __fastcall ClearBuffer();//清除缓冲区
48    void __fastcall Resume();//继续播放
49    void __fastcall Pause();//暂停播放
50    void __fastcall FillData(void* Data, int Length);//填充数据到波形数据缓冲区
51    __property int BufferSize = {read=BUFFERLENGTH};//波形数据缓冲区字节数
52}
;
53#endif

Player.CPP
  1//---------------------------------------------------------------------------
  2
  3
  4
#pragma hdrstop
  5

  6

  7

  8#include "Player.h"

  9
10//---------------------------------------------------------------------------

11
12 #pragma package(smart_init)
13

14//---------------------------------------------------------------------------

15
16__fastcall TPlayer::TPlayer(WAVEFORMATEX Format, BYTE BufferCount, int MinSampleSize, int MaxSampleSize)
17
{
18    Lock = new
TCriticalSection();
19    WAVEBUFFER =
NULL;
20    BUFFERLENGTH = 0
;
21    WAVEBUFFERCOUNT =
BufferCount;
22

23    if (MaxSampleSize == -1)//设置默认最大播放样本

24        MaxSampleSize = Format.nAvgBytesPerSec / 4;
25    if (MinSampleSize == -1)//设置默认最小播放样本

26        MinSampleSize = MaxSampleSize / 2;
27

28    MINSAMPLESIZE =
MinSampleSize;
29    MAXSAMPLESIZE =
MaxSampleSize;
30    hWave =
NULL;
31    hEvent =
NULL;
32    hThread =
NULL;
33    Format =
Format;
34}

35
36__fastcall TPlayer::~
TPlayer()
37
{
38
    free(WAVEBUFFER);
39    WAVEBUFFER =
NULL;
40    BUFFERLENGTH = 0
;
41    RUN = false;//停止线程

42
43    if (hEvent != NULL)
44   
{
45
        CloseHandle(hEvent);
46    }

47
48    if (hWave !=
NULL)
49   
{
50        waveOutReset(hWave);//复位波形播放设备

51        for (int i = 0; i < WAVEBUFFERCOUNT; i++)
52       
{//取消波形播放缓冲
53            free(WaveHdr[i].lpData);
54            waveOutUnprepareHeader(hWave, WaveHdr + i, sizeof
(WaveHdr[i]));
55        }

56        waveOutClose(hWave);//关闭波形播放设备
57    }

58
59    if (hThread !=
NULL)
60
        CloseHandle(hThread);
61
       
62
    delete WaveHdr;
63}

64//---------------------------------------------------------------------------
65
66void __fastcall TPlayer::Initialize(HWAVEOUT WaveHandle)
67
{
68    if (hWave == NULL && WaveHandle !=
NULL)
69   
{
70        hWave =
WaveHandle;
71        hEvent =
CreateEvent(NULL, FALSE, FALSE, NULL);
72

73        WaveHdr = (PWAVEHDR)calloc(WAVEBUFFERCOUNT, sizeof(WAVEHDR));//分配缓冲块

74
75        CurrentBufIndex = 0;
76        BufferUseCount = 0
;
77

78        SampleBuffer = calloc(WAVEBUFFERCOUNT, MAXSAMPLESIZE);//分配波形样本缓冲区大小

79               
80        for (int i = 0; i < WAVEBUFFERCOUNT; i++
)
81       
{//准备波形播放缓冲
82            WaveHdr[i].dwBufferLength = MAXSAMPLESIZE;
83            WaveHdr[i].lpData = (char*)SampleBuffer + (i *
MAXSAMPLESIZE);
84            WaveHdr[i].dwUser =
i;
85            WaveHdr[i].dwFlags = 0
;
86            WaveHdr[i].lpNext =
NULL;
87            WaveHdr[i].reserved = 0
;
88

89            waveOutPrepareHeader(hWave, WaveHdr + i, sizeof
(WaveHdr[i]));
90        }

91
92
        DWORD ThreadID;
93        hThread = CreateThread(NULL, 0, PlayThread, this, 0, &ThreadID);//启动播放线程

94    }

95}

96//---------------------------------------------------------------------------
97
98void CALLBACK TPlayer::OutputCallback(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
99
{
100    TPlayer* Player = (TPlayer*
)dwInstance;
101

102    switch
(uMsg)
103   
{
104        case
WOM_OPEN:
105       
{
106            break
;
107        }

108        case WOM_DONE:
109       
{//一个缓冲块播放完毕
110            SetEvent(Player->hEvent);//设置播放完毕事件信号状态
111
112            Player->Lock->Enter();
113            Player->BufferUseCount--;//缓冲使用块数递减

114            Player->Lock->Leave();
115            break
;
116        }

117        case WOM_CLOSE:
118       
{
119            break
;
120        }

121    }

122}

123//---------------------------------------------------------------------------
124 DWORD WINAPI TPlayer::PlayThread(PVOID Param)
125
{
126    TPlayer* Player = (TPlayer*
)Param;
127    Player->RUN = true
;
128    return Player->
PlayerThread();
129}

130//---------------------------------------------------------------------------
131
132 DWORD __fastcall TPlayer::PlayerThread()
133
{
134    while
(RUN)
135   
{
136        if (BufferUseCount >= WAVEBUFFERCOUNT)//如果播放缓冲全部用完,则等待任一缓冲播放完毕

137        {
138            ResetEvent(hEvent);//等待信号前先将事件信号复位,免得发生误判

139            WaitForSingleObject(hEvent, INFINITE);//等待任一播放缓冲播放完毕
140        }

141
142        if (CurrentBufIndex >=
WAVEBUFFERCOUNT)
143            CurrentBufIndex = 0;//循环缓冲索引

144
145        int SampleSize = BUFFERLENGTH >= MAXSAMPLESIZE ? MAXSAMPLESIZE : MINSAMPLESIZE;
146

147        if (BUFFERLENGTH <
SampleSize)
148       
{//如果缓存中数据不足,则进入下一循环等待缓存
149            Sleep(100);
150            continue
;
151        }

152
153        Lock->
Enter();
154        memcpy(WaveHdr[CurrentBufIndex].lpData, WAVEBUFFER, SampleSize); //复制波形缓冲音频数据到播放缓冲

155        Lock->Leave();
156

157        RemoveLeftData(SampleSize); //将已复制到播放缓冲的数据移走

158
159        WaveHdr[CurrentBufIndex].dwBufferLength = SampleSize;
160

161        Lock->
Enter();
162        BufferUseCount++;//缓冲使用块数递增

163        Lock->Leave();
164

165        waveOutWrite(hWave, WaveHdr + CurrentBufIndex, sizeof(WaveHdr[CurrentBufIndex]));//播放音频

166
167        CurrentBufIndex++;//当前缓冲索引递增
168    }

169    return 0;
170}

171//---------------------------------------------------------------------------
172
173void __fastcall TPlayer::Resume()
174
{
175    if (hWave !=
NULL)
176
        waveOutRestart (hWave);
177}

178//---------------------------------------------------------------------------
179
180void __fastcall TPlayer::Pause()
181
{
182    if (hWave !=
NULL)
183
        waveOutPause(hWave);
184}

185//---------------------------------------------------------------------------
186
187void __fastcall TPlayer::FillData(void* Data, int Length)
188
{
189    if (Length > 0
)
190   
{
191        Lock->
Enter();
192

193        int NewLength = BUFFERLENGTH +
Length;
194        void *NewBuf = calloc(1, NewLength);//分配新缓冲

195
196        if (BUFFERLENGTH != 0)
197            memcpy(NewBuf, WAVEBUFFER, BUFFERLENGTH);//将原数据复制到新区域

198
199        memcpy((char*)NewBuf + BUFFERLENGTH, Data, Length);//将新数据复制到新区域
200
201        free(WAVEBUFFER);//释放原缓冲
202
203        WAVEBUFFER = NewBuf;
204        BUFFERLENGTH =
NewLength;
205

206        Lock->
Leave();
207    }

208}

209//---------------------------------------------------------------------------
210
211void __fastcall TPlayer::RemoveLeftData(int Length)
212
{
213    Lock->
Enter();
214    if (Length > 0 && Length <=
BUFFERLENGTH)
215   
{
216        int NewLength = BUFFERLENGTH -
Length;
217        void *NewBuf = calloc(1, NewLength);//分配新缓冲

218        memcpy(NewBuf, (char*)WAVEBUFFER + Length, NewLength);//复制数据
219
220        free(WAVEBUFFER);//释放原缓冲
221
222        WAVEBUFFER = NewBuf;
223        BUFFERLENGTH =
NewLength;
224    }

225    Lock->Leave();
226}

227//---------------------------------------------------------------------------
228void __fastcall TPlayer::ClearBuffer()
229
{
230    Lock->
Enter();
231    if (BUFFERLENGTH > 0
)
232   
{
233        BUFFERLENGTH = 0
;
234
        free(WAVEBUFFER);
235        WAVEBUFFER =
NULL;
236    }

237    Lock->Leave();
238    if (hWave !=
NULL)
239
        waveOutReset(hWave);
240}


源码已经更新,请到此处下载

嘿嘿,第一次写那么长篇大论的东西,如有遗漏或者错误,各位包涵则个:)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值