惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
T
The Blog of Author Tim Ferriss
博客园 - 叶小钗
N
Netflix TechBlog - Medium
腾讯CDC
C
Check Point Blog
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
S
SegmentFault 最新的问题
F
Fortinet All Blogs
美团技术团队
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
F
Full Disclosure
Recorded Future
Recorded Future
D
DataBreaches.Net
博客园 - 【当耐特】
Martin Fowler
Martin Fowler
J
Java Code Geeks
I
InfoQ
Y
Y Combinator Blog
A
About on SuperTechFans
AI
AI
爱范儿
爱范儿
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Forbes - Security
Forbes - Security
W
WeLiveSecurity
M
MIT News - Artificial intelligence
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
Schneier on Security
Schneier on Security
The GitHub Blog
The GitHub Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
G
GRAHAM CLULEY
Know Your Adversary
Know Your Adversary
Latest news
Latest news
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
Docker
Recent Commits to openclaw:main
Recent Commits to openclaw:main
量子位
V2EX - 技术
V2EX - 技术
Project Zero
Project Zero

博客园 - AndyHai

用C#实现的黑客帝国中的字符雨特效 谁动了我的构造函数? 一个PCM音频转换与混音的示例 关心则乱 让ASPX和ASMX脱离IIS运行的例子(ASP.NET宿主程序) SQL 中如何对纪录进行拆分 带有空值提示的TextBox NAT类型检测方法(转载) 在.NET中探测U盘的插入/拔出 用WebService实现中国移动的Provision反向接口 一个动态加载/卸载DLL的例子 用ASP.NET调用Tuxedo Tuxedo 搞定! 用Multi-Media Library实现的波形音频录制与播放 研究如何用Multi-Media Library播放波形数据 又多一道面试题 面试 RTP协议
用Multi-Media Library制作流式音频播放器
AndyHai · 2007-07-07 · via 博客园 - AndyHai
  

最近在制作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计算即可

pwhWAVEHDR结构体的指针,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即可

lpNextreserved都是备用字段,置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 = 4int MinSampleSize = -1int 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, this0&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//---------------------------------------------------------------------------
124DWORD WINAPI TPlayer::PlayThread(PVOID Param)
125{
126    TPlayer* Player = (TPlayer*)Param;
127    Player->RUN = true;
128    return Player->PlayerThread();
129}

130//---------------------------------------------------------------------------
131
132DWORD __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}

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

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

参考资料:

http://www.codeproject.com/audio/wavefiles.asp

(他的代码有点BUG,而且结构不是很易懂,所以我作了较大改动)