WinCE 音频驱动
一、WinCE目前有3种音频模式:MDD/PDD、WaveDev2和UAM。
这些模式的存在是有历史原因的,下面具体说明一下。首先,这3个模式是使用同一套WaveAPI的驱动接口,在系统中以驱动形式存在,都会导出WAV_Open、WAV_Close等接口。通过IoControl带入IOCTL_WAV_MESSAGE,与waveapi的子系统配合使用。对下都是控制硬件,它们不同之处在于内部的设计。
(1)MDD/PDD:
CE的旧驱动模型,MDD/PDD把驱动分成2块,sort-of hardware independent的MDD层和really hardware dependent的PDD层。MDD部分是通用的、公开出来的,生成wavemdd.lib的库;PDD必须是OEM自己编写的。这2部分静态的链接起来,才形成一个完整的驱动。
Waveapi驱动已经把硬件依赖关系给分割开来。MDD和PDD把驱动分成硬件相关的和硬件无关的。为了把驱动分割成硬件无关的,MDD层要设想好硬件的工作流程和功能。下面是MDD和PDD需要设定的内容:
A,只有一个设备
B,设备只有1个流(1个输入流和1个输出流)。Waveapi包含了软件混音器(software mixer),在应用层把多个流合并成一个流。
C,输入输出DMA共享一个中断
由此,此模式把PDD层大大简化了,使得音频设备比较容易的被移植和更改。但如果你的设备是比较特殊的,或者需要添加特别的功能,那就需要自己修改MDD的代码。这可能需要做的比较复杂。
(2)WaveDev2:
2000年开始smartphone开发时候,增添了对音频的许多需求,而MDD/PDD模式不能不做改动。在CE 3.0时候,还没有waveapi的软件混音提供,不可以同时播放多个音乐。那就需要一个新的设计,WaveDev2诞生了。
WaveDev2的所有文件都在一个目录下。移植WaveDev2驱动,只需要把样例的所有文件复制过来,再进行修改就可以了。主要修改hwctxt.h和hwctxt.cpp。CE6发布时候带有WaveDev2的样例(public/common/oak/drivers/wavedev/wavedev2/ensoniq),
WaveDev2驱动有以下新特性:
A,MIDI合成器。手机上需要播放MIDI音乐。
B,输入输出流的采样率转换。驱动可以把多个不同采样率的输出流,合并成一个流。也可以把一个输入流,分成多个应用程序需要的采样率输出流(这个应用比较特殊,很少见)。
C,Gain class接口。每个输出流都有自己的class(音量),当应用创建一个新的流时候,它的class是0。系统可以通过不同的waveOutMessage来控制每个class的音量等级。如电话进来时候,后台播放的音乐要静音。
D,Forcespeaker,主要是电话进来时候,让speaker播放铃声。
E,支持S/PDIF接口,让WMAPro流通过S/PDIF进行压缩。
如果是开发一个电话设备,最好使用WaveDev2的模式,系统需要依赖WaveDev2的扩展功能。
(3)UAM:
CE 4.2开始,需要为新增的DirectSound提供驱动。新的设计会利用WaveDev2的一部分改变,增加了使用硬件来混合2个音频流。UAM和WaveDev2看起来差不多,实际内部设计上还是有许多差别的。
真正使用硬件做混音的需求,实在太少。所以在CE5上,DirectSound才被加入。这个设计并不是很有价值,因此许多OEM还是使用以前的音频驱动设计。
二、阅读CPU手册,查看音频接口:
通过阅读AM35X ARM Microprocessor芯片手册,了解到Multi-Channel Buffered Serial Port,共5个,其中McBSP2( Audio data with audio buffer and SIDETONE feature)用来外接音频从设备,来实现音频功能。
音频从设备选用TI的TLV320AIC23B芯片,接下来,就是见证奇迹的时刻……(如何在WINCE中音频驱动架构中添加TLV320AIC23B的驱动)
三、WinCE的Wavedev2模式实现:
Wavedev2模式是单层的驱动模式,平台相关的都在hwctxt.h和hwctxt.cpp中,此外还加入了midi支持、software mixer支持、S/PDIF接口、gain class接口、forcespeaker接口等等。
(1)音频系统的软件实现
音频设备驱动程序通过对硬件的控制实现音频数据流传输,同时向上层提供标准的音频接口。其中的一个流接口函数WAV_IOControl()提供应用层控制音频设备的接口,通过输入和输出消息实现对硬件的控制,主要包括两个函数HandleWaveMessage()和HandleMixerMessage (),其中HandleWaveMessage()负责音频数据的传输即播放数字化声音文件和录音操作等;而HandleMixerMessage ()负责对输出音频进行混音处理,如音量调节、高低音控制等。
在音频数据采样过程中,假如按音频采样频率为44.1KHz、16位每个声道、左右2个声道来计算,码流为1.411Mbps,所以采用DMA控制器来传输十分必要。(有关音频DMA的具体说明,见文:WINCE 平台的DMA)
(2)混音的处理
如果要WINDOWS CE的声音驱动模型支持混音,则要考虑如下问题:
A,声音设备是否支持硬件混音
B,声音设备需要工作在同一种采样频率下
C,声音设备要能够同时支持录音和放音操作
而声音的驱动要负责完成声音采集的混音和声音放音的混音。其基本原理如下:
A,将声音设备设定在一个频率下,比如:44.1KHZ,16BIT
B,驱动允许打开多个音频流,每个音频流可以允许不同的采样率,比如: (A:8KHZ,8BIT B: 44.1KHZ,16BIT)
C,放音的混音在最后的数据准备阶段(即放入到DMA前),用合成算法将所有的流进行数学运算,得出声音设备采样频率下(44.1KHZ,16BIT)的数据。数据通过DMA发送到CODEC中。
D,录音的混音操作,都是从声音设备采样频率下(44.1KHZ,16BIT)下得到采样的基本数据,然后通过数学运算分配到不同的频率下的音频流上。
E, 要注意的就是合成的时候要注意数据不溢出;分开的时候要注意数据速率的匹配和数据宽度的匹配。
(3)Wavedev2的流程
先不去看具体跟硬件操作相关的东西,而是从流程入手,把整个流程搞清楚了,调试起来就非常的容易了。我们着重看hwctxt.cpp,hwctxt.H,devctxt.cpp,devctxt.H,strmctxt.cpp,strmctxt.H这几个源文件。其中hwctxt是类HardwareContext代码文件,devctxt是DeviceContext代码文件,strmctxt是StreamContext代码文件。这几个类的其他一些功能,还在其他一些文件中实现,如output.Cpp,midistrm.Cpp等。
现在我们来看下StreamContext的类图,StreamContext是管理音频流的对象,包括播放、暂停、停止、设置音量、获取播放位置等。从下面的StreamContext的类图中,我们可以看到它派生了WaveStreamContext和MidiStream。然后WaveStreamContext又派生了Input和Output类型的Stream。不用说也可以知道InputStreamContext是针对于像麦克这种输入设备流的。
StreamContext类图
其中OutputStreamContext派生了六个类,M代表单音道,S代表的是立体音,8/16是8/16比特采样了。 SPDIF(SONY/PHILIPS DIGITAL INTERFACE)是一种最新的音频传输格式,它通过光纤进行数字音频信号传输以取代传统的模拟信号传输方式,因此可以取得更高质量的音质效果。
StreamContext是一个管理音频数据流的对象,像智能手机中可能存在用media player播放音乐,同时又开着FM,突然又来电。从上篇文章中我们知道,要想调用wave驱动的播放功能,每个应用都有一份StreamContext对象,上面提到的状况,就会有三个StreamContext对象被创建。在硬件只要一个的条件下,那么这三个StreamContext是如果协同工作的呢?而DeviceContext正是管理StreamContext对象的。
如下是DeviceContext类图:
DeviceContext类图
DeviceContext派生出InputDeviceContext和OutputDeviceContext,他们分别管理InputStreamContext和OutputStreamContext。在DeviceContext内部维护了一个双向链表来管理StreamContext。
HardwareContext是具体操作硬件相关的类,其内部包含InputDeviceContext和OutputDeviceContext对象,下面这种图,就是三个类的关系图,一看就知道他们的对应关系了。
DeivceContext和StreamContext关系图
对于HardwareContext是具体操作硬件的东西,不具有代码性,只要仔细看看代码就行了。现在我们主要分析下DeviceContext和StreamContext的关系。
DeviceContext的作用是管理StreamContext,可以分为几套函数,见Devctxt.h, Devctxt.cpp
音量增益管理:下面这个函数主要是设置设备的整个音量增益,设置了设备音量增益后,对流音量的增益起了限制做用的。音量函数如下:
DWORD GetGain();
DWORD SetGain(DWORD dwGain);
DWORD GetDefaultStreamGain();
DWORD SetDefaultStreamGain(DWORD dwGain);
DWORD GetSecondaryGainLimit(DWORD GainClass);
DWORD SetSecondaryGainLimit(DWORD GainClass, DWORD Limit);
先来讲下设备音量增益(Device Gain)和流音量增益(Stream Gain)的关系。我们从微软Media Player中,很容易就看到了设备音量和流音量的关系。设备音量时通过音量键来控制系统的音量,从而改变整个输出设备的音量的,但是在Media Player中,还是有一个单独的音量控制按钮,它能调节Media Player的音量(不要问我在哪里,自己找),但是调试它是受限制于系统音量,是如何限制,请看下面讲解。
我们现在看下设置系统音量和设置流音量的整个流程,来了解整个音量控制的过程。用户设置时,会调用waveOutSetVolume:
MMRESULT waveOutSetVolume(
HWAVEOUT hwo,
DWORD dwVolume
);
当HWAVEOUT传入为空时,设置的就是设备音量,当HWAVEOUT是通过调用waveOutOpen返回的句柄是,设置的就是流音量。
好,我们进入到驱动中区看看,waveOutSetVolume会调用到来看wavemain.Cpp中HandleWaveMessage的WODM_SETVOLUME分支,我在代码中去掉了不重要的部分,可以看得更清晰些。
case WODM_SETVOLUME:
{
StreamContext *pStreamContext;
pStreamContext = (StreamContext *) dwUser;
LONG dwGain = dwParam1;
if (pStreamContext)
{
dwRet = pStreamContext->SetGain(dwGain);
}
else
{
DeviceContext *pDeviceContext = g_pHWContext->GetOutputDeviceContext(uDeviceId);
dwRet = pDeviceContext->SetGain(dwGain);
}
}
dwUser 指向的是StreamContext对象(在前文中已经讲过),如果pStreamContext为空,那么就调用DeviceContext的SetGain函数,否则调用StreamContext的SetGain函数。调用StreamContext的Gain只对当前的StreamContext的音量起作用,不影响其他的Stream音量。但是对DeviceContext设置音量增益是对DeviceContext管理的所有StreamContext起了控制作用,但是具体是如何影响的,还是根据代码来分析:
在Devctxt.h中的SetGain函数代码如下:
DWORD SetGain(DWORD dwGain)
{
m_dwGain = dwGain;
RecalcAllGains();
return MMSYSERR_NOERROR;
}
用m_dwGain保存设备音量,然后调用RecalcAllGains来重新计算所有StreamContext的音量增益。
在Devctxt.cpp中的RecalcAllGains的实现如下:
void DeviceContext::RecalcAllGains()
{
PLIST_ENTRY pListEntry;
StreamContext *pStreamContext;
for (pListEntry = m_StreamList.Flink;
pListEntry != &m_StreamList;
pListEntry = pListEntry->Flink)
{
pStreamContext = CONTAINING_RECORD(pListEntry,StreamContext,m_Link);
pStreamContext->GainChange();
}
return;
}
它便利所有的StreamContext,并调用pStreamContext->GainChange()来改变StreamContext对象的音量。接着看StreamContext类中的GainChange的实现:
void GainChange()
{
m_fxpGain = MapGain(m_dwGain);
}
DWORD StreamContext::MapGain(DWORD Gain)
{
DWORD TotalGain = Gain & 0xFFFF;
DWORD SecondaryGain = m_pDeviceContext->GetSecondaryGainLimit(m_SecondaryGainClass) & 0xFFFF;
if (m_SecondaryGainClass < SECONDARYDEVICEGAINCLASSMAX)
{
// Apply device gain
DWORD DeviceGain = m_pDeviceContext->GetGain() & 0xFFFF;
TotalGain *= DeviceGain;
TotalGain += 0xFFFF; // Round up
TotalGain >>= 16; // Shift to lowest 16 bits
}
// Apply secondary gain
TotalGain *= SecondaryGain;
TotalGain += 0xFFFF; // Round up
TotalGain >>= 16; // Shift to lowest 16 bits
// Special case 0 as totally muted
if (TotalGain==0)
{
return 0;
}
// Convert to index into table
DWORD Index = 63 - (TotalGain>>10);
return GainMap[Index];
}
音量在系统中用一个DWORD值来表示,其高低两个字节分别来表示左右声道,一般情况下左声道和右声道的音量大小是一样的,所以只取其低两个字节,DWORD TotalGain = Gain & 0xFFFF;
TotalGain是DeviceGain和m_dwGain的乘积,然后再左移16位得到的。其实就是TotalGain=DeviceGain*m_dwGain/最高音量,如果把DeviceGain/最高音量,用百分比来算的话,就很更容易理解了,那么最后的公式就变成TotalGain=DeviceGain*系统音量百分比。那么这里就解释了系统音量是如何限制流音量的疑问。
我们设置好音量增益后,最终会再哪里体现呢:首先看一下Output.cpp文件,WaveStreamContext::Render之后的数据就是直接发送到外部声音芯片的数据,他根据参数以及标志位选择OutputStreamContextXXX::Render2,XXX表示双声道S单声道M,bit位是8位还是16位。以双声道OutputStreamContextS16::Render2为例,BSP里面的代码如下:
PBYTE OutputStreamContextS16::Render2(PBYTE pBuffer, PBYTE pBufferEnd, PBYTE pBufferLast)
{
LONG CurrT = m_CurrT;
LONG DeltaT = m_DeltaT;
LONG CurrSamp0 = m_CurrSamp[0];
LONG PrevSamp0 = m_PrevSamp[0];
PBYTE pCurrData = m_lpCurrData;
PBYTE pCurrDataEnd = m_lpCurrDataEnd;
LONG fxpGain = m_fxpGain;
LONG OutSamp0;
__try
{
while (pBuffer < pBufferEnd)
{
while (CurrT >= 0x100)
{
if (pCurrData>=pCurrDataEnd)
{
goto Exit;
}
CurrT -= 0x100;
PrevSamp0 = CurrSamp0;
PPCM_SAMPLE pSampleSrc = (PPCM_SAMPLE)pCurrData;
CurrSamp0 = (LONG)pSampleSrc->s16.sample_left;
CurrSamp0 += (LONG)pSampleSrc->s16.sample_right;
CurrSamp0 = CurrSamp0>>1;
pCurrData+=4;
}
OutSamp0 = PrevSamp0 + (((CurrSamp0 - PrevSamp0) * CurrT) >> 8);
// 设置增益
OutSamp0 = (OutSamp0 * fxpGain) >> VOLSHIFT;
CurrT += DeltaT;
if (pBuffer < pBufferLast)
{
OutSamp0 += *(HWSAMPLE *)pBuffer;
}
*(HWSAMPLE *)pBuffer = (HWSAMPLE)OutSamp0;
pBuffer += sizeof(HWSAMPLE);
}
}//end the __try block
__except (EXCEPTION_EXECUTE_HANDLER)
{
RETAILMSG(1, (TEXT("InputStreamContext::Render2!/r/n")));
m_lpCurrData = m_lpCurrDataEnd = NULL;
return NULL;
}
Exit:
m_dwByteCount += (pCurrData - m_lpCurrData);
m_lpCurrData = pCurrData;
m_CurrT = CurrT;
m_PrevSamp[0] = PrevSamp0;
m_CurrSamp[0] = CurrSamp0;
return pBuffer;
}
从上面看到是与采样数据相乘,然后在左移16位。跟上面提到的系统音量影响流音量是一样的。
上面讲了,DeviceContext的音量增益管理,现在来看下它的流管理。
StreamContext流管理:主要来管理StreamContext的创建、删除、渲染、传输等功能。主要函数:
StreamContext *CreateStream(LPWAVEOPENDESC lpWOD);
DWORD OpenStream(LPWAVEOPENDESC lpWOD, DWORD dwFlags, StreamContext **ppStreamContext);
HRESULT Open(DeviceContext *pDeviceContext, LPWAVEOPENDESC lpWOD, DWORD dwFlags);
void NewStream(StreamContext *pStreamContext);
void DeleteStream(StreamContext *pStreamContext);
void StreamReadyToRender(StreamContext *pStreamContext);
PBYTE TransferBuffer(PBYTE pBuffer, PBYTE pBufferEnd, DWORD *pNumStreams, BOOL bMuteFlag);
在DeviceContext中有个m_StreamList的双向链表(LIST_ENTRY), m_StreamList用来指向链表的头。在StreamConext中也存在一个m_Link(LIST_ENTRY)。StreamContext是调用DeviceContext的OpenStream来创建的,然后把StreamContext对象加入到DeviceContext的m_StreamList中。
上层调用waveoutOpen,在wavedev2中会调用WODM_OPEN这个分支。在WODM_OPEN中的代码如下:
case WODM_OPEN:
{
StreamContext *pStreamContext;
pStreamContext = (StreamContext *) dwUser;
dwRet = pDeviceContext->OpenStream((LPWAVEOPENDESC)dwParam1, dwParam2, (StreamContext **)pStreamContext);
break;
}
OpenStream的其流程图如下:
StreamContext 初始化流程
CreateStream是根据WAVEFORMATEX这个结构体,来判断具体要创建StreamContext的哪个派生类,下面是CreateStream的流程图,不可不提,还是流程图清晰。
OutputDeviceContext:: CreateStream流程图
上面讲了上层通过WODM_OPEN创建一个StreamContext的过程,那么音频流被打开之后,接下来就是给StreamContext传入音频数据开始播放音乐。Wavedev2提供了WODM_WRITE来向音频设置写入数据。我们先看下WODM_WRITE分支的代码:
case WODM_WRITE:
{
StreamContext *pStreamContext;
pStreamContext = (StreamContext *) dwUser;
dwRet = pStreamContext->QueueBuffer((LPWAVEHDR)dwParam1);
break;
}
这里调用了StreamContext中的QueueBuffer,QueueBuffer的作用就是把WAVEHDR中的数据加入到StreamContext的队列中,等待播放。下面是QueueBuffer的流程图:
QueueBuffer流程图
在QueueBuffer中调用DeviceContext中的StreamReadyToReander通知可以开始渲染了,流程图中的箭头方向是StreamReadyToReander调用流程,最终调用SetEvent(hOutputIntEvent),来通知线程数据已经准备好,得到通知后,就开始播放了。该线程在HardwareContext中的OutputInterruptThread函数中
OutputInterruptThread流程如下:
四、程序实现:
创建平台相关的WAVEDEV2目录,wavemain.cpp中开始初始化:
1,WAV_Init
===>HardwareContext::CreateHWContext(hwctxt.cpp)
===>HardwareContext::Init
===>通过GetDriverRegValue获得I2C总线接口,打开MCBSP
===>创建TLV320AIC23B芯片类m_CAIC23 = new CAIC23(); (创建aic23.cpp和aic23.h,并创建class CAIC23)
===>CAIC23::InitHardware
===>通过I2C总线,来控制TLV320AIC23B,先reset
===>然后configure(选择DAC、频率、使能Slave、设置input bit length、Activate interface等初始化工作,特别注意mic是否被设置成静音)
===>SetSampleRate设置采样率(对照TLV320AIC23B手册)
===>Register MCBSP callbacks注册回调函数
2,SetMicBoost设置mic输入的boost(声音增益)
3,InputEnable打开/关闭mic/line的输入
4,Volume的设置
5,WAV_IOControl控制函数,使用上层API——wave***系列时,通过该函数来控制。
注意事项:
1,InputEnable确保mic打开,line关闭,ADC(输入)打开
2,调节mic boost、mic muted、INSEL(选择mic)
附录一:相关文件
Devctxt.cpp 器件关联——包含了音频流的创造,删除,打开,关闭,格式等功能
Hwctxt.cpp 硬件关联——包含了基本的硬件功能在各个状态的全局配置
Input.cpp 负责输入音频流
Output.cpp 负责输出音频流
Midinote.cpp 负责输出MIDI
Midistrm.cpp 负责MIDI的开关以及控制
Mixerdrv.cpp 系统软件混音
XXX.cpp XXX芯片的所有配置,以及I2C读写等
Strmctxt.cpp 负责所有音频流的增益,buffer请求等功能以及对Devctxt的控制
Wavemain.cpp 包含了所有的流接口函数(init、IOcontrol等)
若要移植音频驱动到另一个器件,一般更改Hwctxt.cpp、XXX.cpp和相关头文件。
附录二:注册表
1,在注册表中还要建立驱动程序的入口点,这样设备管理器才能识别和管理这个驱动。
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/WaveDev]
"Prefix"="WAV"
"Dll"="XXX_wavedev2.dll"
"PortDriver"="MCP1:"
"I2CBaudrateIndex"=dword:1
"I2CBus"=dword:3
"I2CAddress"=dword:1A
……
2,声音设置都在注册表:HKEY_CURRENT_USER\ControlPanel\Volume下,里面的几个键值都是控制声音的。如下:
Volume:系统的主音量,范围是0x0 ~ 0xFFFFFFFF。
Screen:屏幕敲击声。当数值为0(或65536)无声,1为柔和,65538为洪亮。
Key:键盘敲击声,数值的意义和Screen相同。
Mute:控制其它静音的选项。置0x04位为1时允许事件声音,0x02允许应用程序声音,0x01允许警告声。需要注意的是,如果不允许应用程序声音,则警告声位也将被忽略。
通过waveOutSetVolume()这个API,我们可以很容易的更改系统设备的音量,但这个时候,如果你去查看注册表的Volume的键值是没有变化的,因为它只修改了设备的音量,变化还没有这么快到达注册表。但你可以到“控制面板”中的“音量与声音”打开一下,注册表的值也随之改变。(反之,通过对注册表的单独操作对具体音量是起不到作用的)
系统音量的调节,是通过Creg(对注册表操作)类,调用SetDW改变注册表。