Foreword
I've been working on a wavedev2 porting guide over the last few weeks and decided that it's better to post what I've go so far rather than wait until it's what I would consider finished. Expect future updates/additions as time allows, and feel free to ask for specific information in the comments.
Overview
This whitepaper gives an overview of porting the wavedev2 audio driver to new hardware. For additional background on the history and features of wavedev2, please refer to other articles on http://blogs.msdn.com/medmedia.
Different Versions
Like most software, each release of Windows CE includes new features and bug fixes. For this article I’ll be referring to the version of wavedev2 which shipped with Windows CE 6 (AKA Yamazaki) under public\COMMON\oak\drivers\wavedev\wavedev2\ensoniq. This version is backward compatible with previous versions and the porting process is comparable. I’ve included some notes on the differences between Windows CE 6 and previous implementations at the end.
File Layout
All the files needed to build wavedev2 are in a single directory. For porting purposes, these files can be grouped into the following categories:
1. Files which are device independent, and which you should not need to touch during the porting process beyond just copying them:
audiosys.h: Proprietary wave message definitions used by wavedev2.
devctxt.cpp: Implementation of device context class.
devctxt.h: Definition of device context class.
input.cpp: Implementation of audio input streams.
makefile: Used by build system
midinote.cpp: Implementation of tone generator.
midistrm.cpp: Implementation of MIDI stream and MIDI parser.
midistrm.h: Definition of MIDI note and stream classes.
mixerdrv.cpp: Implementation of Mixer API classes (* may need to change if you want to take advantages of mixer API extensions).
mixerdrv.h: Definition of MIXER API classes.
output.cpp: Implementation of audio output streams.
strmctxt.cpp: Implementation of base audio stream class.
strmctxt.h: Definition of base, input, output stream classes.
wavemain.cpp: Device driver interface.
wavemain.h: Common include header used by all source files.
wavepdd.h: Basic PCM sample definitions.
wfmtmidi.h: MIDI structure definitions.
2. Files which are device dependent but are logically part of the wavedev2 driver infrastructure. These files will need to be copied and modified during the port. These files are:
hwctxt.cpp: Implementation of hardware context class.
hwctxt.h: Definition of hardware context class.
oemsettings.h: HW-specific definitions used by hw-independent code.
sources: Used by build system.
wavedev2_ensoniq.def: Driver exports. Probably just need to rename.
wavedev2_ensoniq.reg: Registry entries used to install driver.
3. Files which are device dependent and are not logically part of the driver infrastructure. You can ignore these files in your port (unless they happen to be appropriate to your hardware). For the Ensoniq sample driver, these are:
AC97.H: AC97 codec-specific definitions.
Es1371.cpp: Ensoniq 1371-specific functions.
Es1371.h: Ensoniq 1371-specific header.
Hw_ac97.cpp: AC97 codec-specific functions.
In the above list, note that the vast majority of files shouldn’t need to be touched, and there is really only one source file and two headers you will need to modify to bring up a driver.
Class Descriptions
Class Overview
The wavedev2 driver largely consists of three main base classes:
HardwareContext
This class represents the actual audio hardware. This is the only class you will typically need to modify to port the driver. It is the only device dependent class, and takes care of hardware initialization, power management, DMA and Codec control, handling audio interrupts, and any other proprietary features the driver may implement. There is one instantiated HardwareContext object in the driver which is pointed to by the g_pHWContext global variable. I’ll go into detail about each HardwareContext’s methods later.
DeviceContext
This class represents a specific audio device. You should not have to modify anything in this class to port the driver. There is a DeviceContext virtual base class, from which are derived an InputDeviceContext and an OutputDeviceContext. A typical wavedev2 audio driver (such as the Ensoniq driver) implements a single input device represented by an InputDeviceContext; and a single output device represented by an OutputDeviceContext. In the Ensoniq sample, these objects are directly embedded within HardwareContext class as member variables.
DeviceContext methods include:
StreamContext
This class represents a specific audio stream. You should not have to modify anything in this class to port the driver. There is a StreamContext virtual base class from which are derived a variety of stream classes for various flavors of PCM audio and MIDI data. Each stream is associated with a specific device context. This association is implemented as a linked list of stream contexts hanging off of each device context. In addition, each stream context includes a pointer back to its associated device context.
The class hierarchy is roughly as follows:
StreamContext
CMidiStream
WaveStreamContext
InputStreamContext
OutputStreamContext
OutputStreamContextM8
OutputStreamContextM16
OutputStreamContextS8
OutputStreamContextS16
The reason for the multitude of output contexts is that the mixing/sample-rate-conversion code on the output side is optimized for each type of PCM data (Stereo/Mono, 8/16-bit samples). This avoids some tests in the inner loop. The same optimization wasn’t done for the input side (input isn’t typically used as often as output, and the code is a little simpler).
StreamContextMethods include:
Porting HardwareContext
In this section, I’ll go over each of the methods in HardwareContext and describe what they do.
HardwareContext::CreateHWContext
This is a static method which is called during driver initialization (from the WAV_Init code in wavemain.cpp). This function should create and initialize the global g_pHWContext with a new HardwareContext object and call g_pHWContext->Init. You probably won’t need to change this function, as most changes will be in the Init method.
HardwareContext::Init
This method is only called by CreateHWContext, and is where initialization of the Hardware is typically implemented. Portions of this function may need to be modified for new hardware. Its role is to initialize any hardware, allocate DMA buffers, and startup the interrupt service thread. In addition, during initialization it needs to call into some of the device independent sections to initialize them; specificially:
- Call SetBaseSampleRate on each device context to tell it what sample rate the hardware is running at. Note that these functions can be called at any time to tell the device context that the hardware sample rate has changed, but for devices with a fixed sample rate setting this up during initialization makes sense.
- Call InitMixerControls to initialize the Mixer API support.
HardwareContext::Deinit
This method is called when the driver is unloaded and the system calls WAV_Deinit. In the current design, wave drivers are never unloaded so this method has limited usefulness.
HardwareContext::UpdateOutputGain
HardwareContext::UpdateInputGain
HardwareContext::SetOutputGain
HardwareContext::SetOutputMute
HardwareContext::GetOutputGain
HardwareContext::GetOutputMute
HardwareContext::GetInputMute
HardwareContext::SetInputMute
HardwareContext::GetInputGain
HardwareContext::SetInputGain
These methods are associated with the master input and output gain controls provided by the default mixer API implementation and the device gain waveOutSetVolume API. In the Ensoniq implementation these defer processing to the DeviceContext SetGain methods, which automatically handle volume control in software. There is no need to modify the existing code unless you want to handle some aspects of volume control in hardware. However, keep in mind that individual stream gain controls are still handled in software, and there is no additional overhead in handling device gain as well. Therefore, there is no performance advantage in modifying this code to use hardware gain controls.
HardwareContext::StartOutputDMA
This method starts the DMA controller for audio output. This includes:
1. Check to see if output dma is already running and ignore the call if it is.
2. Clear the variables that track how much “live” data is in each DMA buffer.
3. “Prime” the output DMA buffer with data.
4. Start the DMA channel if (and only if) data was available to be transferred.
The only line you should need to change is the one that specifically turns on the DMA channel, which in the Ensoniq implementation is:
m_CES1371.StartDMAChannel( ES1371_DAC0 );
HardwareContext::StopOutputDMA
This method stops the audio output DMA controller. The only line you should need to change is:
m_CES1371.StopDMAChannel( ES1371_DAC0 );
HardwareContext::StartInputDMA
HardwareContext::StopInputDMA
These methods are analogous to the methods described above for the output DMA. However, note that the code to start the input DMA doesn’t need to “prime” the buffer or keep track of how much application data is in the buffer, and is therefore somewhat simpler than the output case.
HardwareContext::GetDriverRegValue
HardwareContext::SetDriverRegValue
These methods relate to reading driver-specific registry keys. You should not need to change them.
HardwareContext::InitInterruptThread
This method initializes the audio driver’s IST thread and sets it to a realtime priority. If your driver has a single IST thread shared by both input and output you will not need to modify this code.
HardwareContext::PowerUp
HardwareContext::PowerDown
These methods are called by the system’s power management subsystem. In the Ensoniq driver they are stubbed out.
HardwareContext::TransferInputBuffer
HardwareContext::TransferOutputBuffer
This method is called from the IST to transfer one data into our out of the DMA buffers. The code determines the starting address and size of the DMA buffer and passes the information to the device context, which performs the actual transfer. You will not need to modify this code unless you change the organization or data structures representing the DMA buffers.
HardwareContext::InterruptThread
This method implements the Interrupt Service Thread which is shared by both input and output DMA. It’s operation is basically:
1. Wait for an input or output DMA done interrupt.
2. Determine whether an input or output (or both) DMA interrupt occurred.
3. If an output DMA interrupt occurred:
§ Transfer/mix application data into the DMA buffer that was just completed.
§ If there is no application data remaining in either DMA buffer, halt output DMA
4. If an input DMA interrupt occurred:
§ Transfer data out of the DMA buffer that was just completed into application buffers.
§ If we were unable to transfer any data (due to no application buffer being available), halt input DMA.
5. Go back to step 1.
HardwareContext::SetSpeakerEnable
HardwareContext::RecalcSpeakerEnable
HardwareContext::ForceSpeaker
These methods handle the WODM_FORCESPEAKER message which may be used to request that audio data be routed to an auxiliary speaker on the back of the phone (this speaker is typically larger and more powerful than the earpiece speaker, and is used for ringtones). If you hardware supports this functionality, you will need to add code to the SetSpeakerEnable to switch the speaker on or off.
HardwareContext::PmControlMessage
This method receives messages from the Power Manager IOCTL calls:
IOCTL_POWER_CAPABILITIES
IOCTL_POWER_QUERY
IOCTL_POWER_SET
IOCTL_POWER_GET
You probably will not need to modify this code.
HardwareContext::IsSupportedOutputFormat
This method is called during waveOutOpen to allow the OEM to support additional custom audio formats beyond the standard PCM functionality. Normally this method should just return FALSE. The Ensoniq driver supports directly playing WMAPro compressed audio content over its S/PDIF interface and therefore returns TRUE for this specific case.
DMA Buffer Organization and Data Transfer
In the Ensoniq implementation, which is fairly typical, input and output are each allocated a DMA buffer using HalAllocateCommonBuffer. The size of each buffer is only 4k (the same as a memory page), so it’s unlikely that the allocation will fail (especially since it takes place during boot). Other implementations may choose to preallocate a fixed area of memory for the audio DMA buffers.
During audio transfer, each DMA buffer is logically subdivided into equally-sized DMA pages 0 and 1, and the hardware is programmed to:
a. Transfer nonstop from the DMA buffer to the codec, and automatically reload the DMA address register with the start address of the buffer when it reaches the end.
b. Generate an interrupt to the audio system whenever the DMA address moves either past the midpoint of the buffer (e.g. from page 0 to page 1), or reaches the end and restarts itself (e.g. from page 1 to page 0).
On each DMA interrupt, the HardwareContext code needs to determine which DMA page the DMA controller has just finished copying data into/out of and call the DeviceContext’s TransferBuffer method to copy application data into/out of that buffer.
Buffer Security/Copying
(Still working on this section)
Support for S/PDIF
(Still working on this section)
Differences between Windows CE 5 & Windows CE 6
All current versions of Windows Mobile (including Windows Mobile 6) are based on Windows CE 5 or earlier OS releases. However, the most recent version of wavedev2 is shipped with Windows CE 6, and that’s the version I’m examining for this guide. Therefore, it’s important to touch on differences between how the two OS’es interact with wave drivers.
For the most part, the audio driver architecture between CE5 and CE6 is the same. Audio drivers written for CE5 can generally run with little or no modification on CE6.
Virtual Addressing Differences
When porting the Ensoniq wavedev2 driver from CE5 to CE6, the only change specifically related to moving between operating systems was to surround the call to SetProcPermissions in hwctxt.cpp as follows:
#if (_WINCEOSVER < 600)
SetProcPermissions((DWORD)-1);
#endif
On Windows CE6, the API’s SetProcPermissions and GetProcPermissions are no longer supported due to changes in the virtual memory architecture. They are still exported for backward-compatibility purposes, but they will have no affect (other than printing out a nasty warning message on the debugger). This change bears a little explanation:
On pre-Windows CE6 systems there is a limit of 32 processes, and all processes run in a shared virtual memory space. The system provides cross-process protection to ensure that processes don’t access each other’s memory, and this protection is enforced on a per-thread basis. Device drivers (such as the wave driver) run inside device.exe, which is one of these 32 processes. The Interrupt Service Thread (IST) in the driver is responsible for accessing audio data in various application buffers residing in multiple processes. The audio driver’s IST overrides this protection by calling SetProcPermissions(0xFFFFFFFF); each bit in the parameter represents one of 32 processes in the system, so 0xFFFFFFFF enables access to all of them.
Windows CE 6 adopts a more traditional memory architecture, with the kernel taking the upper 2GB of virtual space and each user process occupying the same lower 2GB region. Switching between user processes involves swapping a new process into the lower 2GB. While this greatly expands the amount of virtual memory available to each user process, it also means that the IST thread (now running in the kernel) may no longer freely access other arbitrary process’ address space.
To solve this problem, the waveapi middleware (which sits above the audio driver on the stack) now takes care of mapping each application data buffer into a kernel (via CeAllocAsynchronousBuffer). The memory mapping/unmapping take place during waveOutPrepareHeader/waveOutUnprepareHeader, so the cost of memory management doesn’t impact performance.