wince 音频学习

本文详细解析Wave驱动的架构,包括StreamContext、DeviceContext和HardwareContext的管理流程,重点阐述了如何通过设置设备音量增益来限制流音量,以及设备与流之间的协同工作关系。此外,文章还深入探讨了音量增益管理、流管理以及DMA在音频数据传输中的应用。

 解析的Wave 驱动的架构。我们了解一个驱动的时候,先不去看具体跟硬件操作相关的东西,而是从流程入手,把整个流程搞清楚了,调试起来就非常的容易了。我们着重看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是针对于像麦克这种输入设备流的。

wps_clip_image-597[4]

StreamContext类图

      其中OutputStreamContext派生了六个类,M代表单音道,S代表的是立体音,8/16是8/16比特采样了。 SPDIF(SONY/PHILIPS DIGITAL INTERFACE)是一种最新的音频传输格式,它通过光纤进行数字音频信号传输以取代传统的模拟信号传输方式,因此可以取得更高质量的音质效果。

     StreamContext是一个管理音频数据流的对象,像智能手机中可能存在用media player播放音乐,同时又开着FM,突然又来电。从上篇文章中我们知道,要想调用wave驱动的播放功能,每个应用都有一份StreamContext对象,上面提到的状况,就会有三个StreamContext对象被创建。 在硬件只要一个的条件下,那么这三个StreamContext是如果协同工作的呢?而DeviceContext正是管理StreamContext对象的。

如下是DeviceContext类图:

wps_clip_image-1047[4]
DeviceContext类图

DeviceContext派生出InputDeviceContext和OutputDeviceContext,他们分别管理InputStreamContext和OutputStreamContext。在DeviceContext内部维护了一个双向链表来管理StreamContext。

HardwareContext是具体操作硬件相关的类,其内部包含InputDeviceContext和OutputDeviceContext对象,下面这种图,就是三个类的关系图,一看就知道他们的对应关系了。

wps_clip_image-1339[4]

DeivceContext和StreamContext关系图

    对于HardwareContext是具体操作硬件的东西,不具有代码性,只要仔细看看代码就行了。现在我们主要分析下DeviceContext和StreamContext的关系。

DeviceContext的作用是管理StreamContext,可以分为几套函数,见Devctxt.h, Devctxt.cpp

音量增益管理:下面这个函数主要是设置设备的整个音量增益,设置了设备音量增益后,对流音量的增益起了限制做用的。

音量函数如下

 

  1. DWORD GetGain();  
  2. DWORD SetGain(DWORD dwGain);  
  3. DWORD GetDefaultStreamGain();  
  4. DWORD SetDefaultStreamGain(DWORD dwGain);  
  5. DWORD GetSecondaryGainLimit(DWORD GainClass);  
  6. 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分支,我在代码中去掉了不重要的部分,可以看得更清晰些。

  1. case WODM_SETVOLUME:  
  2. {  
  3.     StreamContext *pStreamContext;  
  4.     pStreamContext = (StreamContext *) dwUser;  
  5.   
  6.     LONG dwGain = dwParam1;  
  7.     if (pStreamContext)  
  8.     {  
  9.         dwRet = pStreamContext->SetGain(dwGain);  
  10.     }  
  11.     else  
  12.     {  
  13.         DeviceContext *pDeviceContext = g_pHWContext->GetOutputDeviceContext(uDeviceId);  
  14.         dwRet = pDeviceContext->SetGain(dwGain);  
  15.     }  
  16. }  

dwUser 指向的是StreamContext对象(在前文中已经讲过),如果pStreamContext为空,那么就调用DeviceContext的SetGain函数,否则调用StreamContext的SetGain函数。调用StreamContext的Gain只对当前的StreamContext的音量起作用,不影响其他的Stream音量。但是对DeviceContext设置音量增益是对DeviceContext管理的所有StreamContext起了控制作用,但是具体是如何影响的,还是根据代码来分析:

在Devctxt.h中的SetGain函数代码如下

  1. DWORD SetGain(DWORD dwGain)  
  2.     {  
  3.         m_dwGain = dwGain;  
  4.         RecalcAllGains();  
  5.         return MMSYSERR_NOERROR;  
  6.     }  

用m_dwGain保存设备音量,然后调用RecalcAllGains来重新计算所有StreamContext的音量增益。

在Devctxt.cpp中的RecalcAllGains的实现如下

  1. void DeviceContext::RecalcAllGains()  
  2. {  
  3.     PLIST_ENTRY pListEntry;  
  4.     StreamContext *pStreamContext;  
  5.   
  6.     for (pListEntry = m_StreamList.Flink;  
  7.         pListEntry != &m_StreamList;  
  8.         pListEntry = pListEntry->Flink)  
  9.     {  
  10.         pStreamContext = CONTAINING_RECORD(pListEntry,StreamContext,m_Link);  
  11.         pStreamContext->GainChange();  
  12.     }  
  13.     return;  
  14. }  

它便利所有的StreamContext,并调用pStreamContext->GainChange()来改变StreamContext对象的音量。接着看StreamContext类中的GainChange的实现

  1. void GainChange()  
  2.    {  
  3.        m_fxpGain = MapGain(m_dwGain);  
  4. }  
  5. DWORD StreamContext::MapGain(DWORD Gain)  
  6. {  
  7.     DWORD TotalGain = Gain & 0xFFFF;  
  8.     DWORD SecondaryGain = m_pDeviceContext->GetSecondaryGainLimit(m_SecondaryGainClass) & 0xFFFF;  
  9.     if (m_SecondaryGainClass < SECONDARYDEVICEGAINCLASSMAX)  
  10.     {  
  11.         // Apply device gain   
  12.         DWORD DeviceGain = m_pDeviceContext->GetGain() & 0xFFFF;  
  13.         TotalGain *= DeviceGain;  
  14.         TotalGain += 0xFFFF;  // Round up   
  15.         TotalGain >>= 16;     // Shift to lowest 16 bits  
  16.     }  
  17.   
  18.     // Apply secondary gain   
  19.     TotalGain *= SecondaryGain;  
  20.     TotalGain += 0xFFFF;  // Round up   
  21.     TotalGain >>= 16;     // Shift to lowest 16 bits  
  22.   
  23.     // Special case 0 as totally muted  
  24.     if (TotalGain==0)  
  25.     {  
  26.         return 0;  
  27.     }  
  28.   
  29.     // Convert to index into table  
  30.     DWORD Index = 63 - (TotalGain>>10);  
  31.     return GainMap[Index];  
  32. }  

音量在系统中用一个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里面的代码如下:

  1. PBYTE OutputStreamContextS16::Render2(PBYTE pBuffer, PBYTE pBufferEnd, PBYTE pBufferLast)  
  2. {  
  3.     LONG CurrT = m_CurrT;  
  4.     LONG DeltaT = m_DeltaT;  
  5.     LONG CurrSamp0 = m_CurrSamp[0];  
  6.     LONG PrevSamp0 = m_PrevSamp[0];  
  7.     PBYTE pCurrData = m_lpCurrData;  
  8.     PBYTE pCurrDataEnd = m_lpCurrDataEnd;  
  9.     LONG fxpGain = m_fxpGain;  
  10.     LONG OutSamp0;  
  11.   
  12.     __try  
  13.     {  
  14.         while (pBuffer < pBufferEnd)  
  15.         {  
  16.             while (CurrT >= 0x100)  
  17.             {  
  18.                 if (pCurrData>=pCurrDataEnd)  
  19.                 {  
  20.                     goto Exit;  
  21.                 }  
  22.                 CurrT -= 0x100;  
  23.                 PrevSamp0 = CurrSamp0;  
  24.                 PPCM_SAMPLE pSampleSrc = (PPCM_SAMPLE)pCurrData;  
  25.                 CurrSamp0 =  (LONG)pSampleSrc->s16.sample_left;  
  26.                 CurrSamp0 += (LONG)pSampleSrc->s16.sample_right;  
  27.                 CurrSamp0 = CurrSamp0>>1;  
  28.                 pCurrData+=4;  
  29.             }  
  30.             OutSamp0 = PrevSamp0 + (((CurrSamp0 - PrevSamp0) * CurrT) >> 8);  
  31.             // 设置增益  
  32.             OutSamp0 = (OutSamp0 * fxpGain) >> VOLSHIFT;  
  33.             CurrT += DeltaT;  
  34.       
  35.             if (pBuffer < pBufferLast)  
  36.             {  
  37.                 OutSamp0 += *(HWSAMPLE *)pBuffer;  
  38.             }  
  39.             *(HWSAMPLE *)pBuffer = (HWSAMPLE)OutSamp0;  
  40.             pBuffer += sizeof(HWSAMPLE);  
  41.   
  42.         }  
  43.     }//end the __try block  
  44.     __except (EXCEPTION_EXECUTE_HANDLER)  
  45.     {  
  46.         RETAILMSG(1, (TEXT("InputStreamContext::Render2!/r/n")));  
  47.         m_lpCurrData = m_lpCurrDataEnd = NULL;    
  48.         return NULL;  
  49.     }  
  50. Exit:  
  51.     m_dwByteCount += (pCurrData - m_lpCurrData);  
  52.     m_lpCurrData = pCurrData;  
  53.     m_CurrT = CurrT;  
  54.     m_PrevSamp[0] = PrevSamp0;  
  55.     m_CurrSamp[0] = CurrSamp0;  
  56.     return pBuffer;  
  57.     }  

从上面看到是与采样数据相乘,然后在左移16位。跟上面提到的系统音量影响流音量是一样的。

上面讲了,DeviceContext的音量增益管理,现在来看下它的流管理。

StreamContext流管理:主要来管理StreamContext的创建、删除、渲染、传输等功能。

主要有如下几个函数  

  1. StreamContext *CreateStream(LPWAVEOPENDESC lpWOD);  
  2. DWORD OpenStream(LPWAVEOPENDESC lpWOD, DWORD dwFlags, StreamContext **ppStreamContext);  
  3. HRESULT Open(DeviceContext *pDeviceContext, LPWAVEOPENDESC lpWOD, DWORD dwFlags);  
  4. void NewStream(StreamContext *pStreamContext);  
  5. void DeleteStream(StreamContext *pStreamContext);  
  6. void StreamReadyToRender(StreamContext *pStreamContext);  
  7. 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中的代码如下:

  1. case WODM_OPEN:  
  2. {  
  3.     StreamContext *pStreamContext;  
  4.     pStreamContext = (StreamContext *) dwUser;  
  5.     dwRet = pDeviceContext->OpenStream((LPWAVEOPENDESC)dwParam1, dwParam2, (StreamContext **)pStreamContext);  
  6.     break;  
  7. }  

OpenStream的其流程图如下

wps_clip_image-7555[4]

StreamContext 初始化流程

CreateStream是根据WAVEFORMATEX这个结构体,来判断具体要创建StreamContext的哪个派生类,下面是CreateStream的流程图,不可不提,还是流程图清晰。

wps_clip_image-7702[4]

OutputDeviceContext:: CreateStream流程图

上面讲了上层通过WODM_OPEN创建一个StreamContext的过程,那么音频流被打开之后,接下来就是给StreamContext传入音频数据开始播放音乐。Wavedev2提供了WODM_WRITE来向音频设置写入数据。我们先看下WODM_WRITE分支的代码 

  1. case WODM_WRITE:  
  2. {  
  3.     StreamContext *pStreamContext;  
  4.     pStreamContext = (StreamContext *) dwUser;  
  5.     dwRet = pStreamContext->QueueBuffer((LPWAVEHDR)dwParam1);  
  6.     break;  
  7. }  

这里调用了StreamContext中的QueueBuffer,QueueBuffer的作用就是把WAVEHDR中的数据加入到StreamContext的队列中,等待播放。下面是QueueBuffer的流程图

wps_clip_image-8173[4]

QueueBuffer流程图

在QueueBuffer中调用DeviceContext中的StreamReadyToReander通知可以开始渲染了,流程图中的箭头方向是StreamReadyToReander调用流程,最终调用SetEvent(hOutputIntEvent),来通知线程数据已经准备好,得到通知后,就开始播放了。该线程在HardwareContext中的OutputInterruptThread函数中

OutputInterruptThread流程如下

wps_clip_image-8440[5]

 

WinCE平台上的DMA

      CEDDK提供了DMA的相关函数,在CEDDK/DDK_DMA/ddk_dma.c中定义。最有用的就两个函数,HalAllocateCommonBuffer(..)和HalFreeCommonBuffer(..)分别用于为DMA申请和释放内存。

(1)首先介绍一下会用到的DMA适配器结构,在ceddk.h中定义,如下:

typedef struct _DMA_ADAPTER_OBJECT_ 

      USHORT ObjectSize;               //该结构的大小 
      INTERFACE_TYPE InterfaceType;    //接口类型,一般用做DMA时设置为Internal 
      ULONG BusNumber;                 //一般设置为0 
} DMA_ADAPTER_OBJECT, *PDMA_ADAPTER_OBJECT;

 

(2)DMA内存分配函数:

PVOID HalAllocateCommonBuffer(PDMA_ADAPTER_OBJECT DmaAdapter, ULONG Length, PPHYSICAL_ADDRESS LogicalAddress, BOOLEAN CacheEnabled)

    DmaAdapter:        DMA适配器结构指针

    Length:                 要分配的内存的大小

    LogicalAddress:    分配成功后,内存的物理起始地址

    CacheEnabled:     是否使用Cache

实际上该函数通过调用AllocPhysMem函数来分配一段物理地址连续的内存,这段内存默认是64KB字节对齐的,DMA操作的物理内存必须是连续的。该函数调用成功以后,返回值是虚拟地址,可以在驱动中访问其中的内容,函数的第三个参数返回内存的物理地址,可以赋值给DMA控制器来完成DMA操作。

 

(3)DMA内存释放函数:

VOID HalFreeCommonBuffer(PDMA_ADAPTER_OBJECT DmaAdapter, ULONG Length, PHYSICAL_ADDRESS LogicalAddress, PVOID VirtualAddress, BOOLEAN CacheEnabled)

    DmaAdapter:        DMA适配器结构指针

    Length:                 内存的大小

    LogicalAddress:    内存的物理起始地址

    VirtualAddress:     内存的虚拟地址

    CacheEnabled:      是否使用Cache

该函数通过调用FreePhysMem函数来完成内存的释放,所以在使用该函数的时候,只有函数的第四个参数是必须的,也就是内存的虚拟地址,其他的都可以忽略。

 

(4)下面给个使用上面两个函数的例子:

DMA_ADAPTER_OBJECT dmaAdapter; 
//初始化DMA适配器 
dmaAdapter.ObjectSize = sizeof(dmaAdapter); 
dmaAdapter.InterfaceType = Internal; 
dmaAdapter.BusNumber = 0; 
//分配DMA内存 
m_pDMABuf = (PBYTE)HalAllocateCommonBuffer( &dmaAdapter, 256 * 1024, &m_pDMABufPhys, FALSE );
//将物理地址赋值给DMA控制器 
vm_pDMAreg->DST = (int)m_pDMABufPhys.LowPart;

...

//释放DMA内存 
if( m_pDMABuf != NULL ) 

     HalFreeCommonBuffer( NULL, 0, 0, m_pDMABuffer, FALSE); 
     m_pDMABuf = NULL; 
}

      在ddk_dma.c中,还可以看到其他很多DMA相关的操作函数。这些DMA函数是用来操作DMA设备的,通过CreateFile来打开DMA设备,然后调用DeviceIoControl函数来访问DMA设备。DMA设备驱动在/WINCE600/PUBLIC/Common/Oak/Drivers/DMA下面,该DMA驱动以流设备驱动的形式实现。

      一般来说,DMA驱动会配合其他设备驱动来完成数据传输,所以很少会被单独作为一个设备来使用,大多数情况我们开发设备驱动时需要用到DMA的时候,会用到上面两个函数来申请和释放内存。

(5)音频驱动中的DMA

      以S3C2440A为例,它的DMA控制器没有内置的DMA存储区域,所以驱动程序必须在内存内为音频设备分配DMA缓冲区。缓冲区设置是否合理非常关键,缓冲区太小容易造成缓冲区溢出,而要填充大的缓冲区,CPU就要一次处理大量的数据,容易造成延迟。

      所以在本驱动中采用双缓冲区来解决这个问题,也就是当CPU在处理某一个缓冲区音频数据的同时,DMA控制器可以完成另一个缓冲区音频数据的传输,如此交替下去,则可以提高系统的并行能力,提高音频处理的实时性。本驱动所采用的DMA1通道和DMA2通道分别设置了两个缓冲区。采用DMA控制器通道1控制录制的音频数据的传输,采用通道2控制播放的音频数据的传输。
      以放音为例,示意图如下:

                  

新的音频数据在CPU的控制下先写到DMA缓冲区A中,此时DMA控制器正在从DMA缓冲区B中迁移音频数据到IIS总线。当缓冲区B的数据全部传输完成之后,DMA控制器产生INT_DMA2中断,该中断通知CPU开始往缓冲区B中写新的音频数据,与此同时DMA控制器从缓冲区A中迁移数据到IIS总线。这样交替循环,由于CPU和DMA控制器没有同时处理同一块缓冲区,就减少了资源访问的冲突,并且能够最大程度上提高音频处理的实时性。
      放音的协作过程:

A,DMA请求,开始播放音乐时,DMA控制器收到IIS的发送请求后,向CPU提出接管总线要求,以便进行下面的DMA数据传输。

B,DMA音频数据传输,DMA控制器得到总线的控制权后,通知IIS控制器DMA应答,这时开始进入DMA数据传输,DMA控制器从输出缓冲区A中取出CPU填充的音频数据到IIS控制器的发送FIFO,当前的DMA传输结束,也即2048个字节的音频数据已通过IIS总线发送到音频编解码器。

C,DMA中断,DMA传输结束后DMA控制器向CPU发出INT_DMA2中断,表示输出缓冲区A的数据已经被迁移到音频编解码器,这时CPU(这之前CPU往输出缓冲区B写入音频数据)转而向输出缓冲区A写入音频数据,而DMA控制器同时从输出缓冲区B中迁移数据到发送的FIFO。CPU和DMA控制器如此交替访问缓冲区,实现音频数据的快速传输。

      录音时DMA的操作类似。

 Devctxt.cpp
 器件关联——包含了音频流的创造,删除,打开,关闭,格式等功能
 
Hwctxt.cpp
 硬件关联——包含了基本的硬件功能在各个状态的全局配置
 
I2citf.cpp
 I2C传输配置
 
I2S.cpp
 I2S传输配置
 
Input.cpp
 负责输入音频流
 
Output.cpp
 负责输出音频流
 
Midinote.cpp
 负责输出MIDI
 
Midistrm.cpp
 负责MIDI的开关以及控制
 
Mixerdrv.cpp
 系统软件混音
 
RTcodecComm.cpp
 Wm8753的所有功能配置,以及初始化设置
 
Strmctxt.cpp
 负责所有音频流的增益,buffer请求等功能以及对Devctxt的控制
 
Wavemain.cpp
 包含了所有的流接口函数

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值