The Wavedev2 Gainclass Implementation

Goals

Back in 2000, while we were defining the requirements for the Windows Mobile Smartphone audio design, one of our goals was to mute most audio applications while a phone call is in progress.

A secondary issue is the fact that most of the time the user keeps their phone in their pocket or backpack, or is holding it out in front of them looking at the screen. In this situation we need to play notifications and incoming rings loud enough to be heard. However, during a phone call the phone is being held tightly to the user's ear, and a sound played loudly enough to be heard in the first situation would be far too loud.

A final goal was to minimize any changes at the application level. We didn't want applications to need to monitor whether we were in a call and adjust their own volume level, or even require any modifications at all to third party applications that wanted some reasonable behavior.

Design

To solve these problems, the wavedev2 audio driver implements something we called gain classes. Each wave stream is associated with a specific gain class, and the driver implements an additional gain control for each of these classes. This additonal gain is separate from the controls exposed by waveOutSetVolume, and is transparent to the application. The effects of the various gain controls are cumulative: the total gain applied to a specific output stream will therefore be the product of the stream gain, the device gain, and the class gain.

A quick digression here: there are two standard ways that an application can control the volume of a wave stream, each of which involves a call to waveOutSetVolume.

  • Calling waveOutSetVolume and passing in a wave device ID is used to set the device gain, and will theoretically affect all streams playing on the device.
  • Calling waveOutSetVolume and passing in a wave handle (the thing you get back from waveOutOpen) is used to set the stream volume, and will only affect that stream.

By the way, a common (and hard to diagnose) application error is trying to call waveOutSetVolume on a wave handle before the handle has been initialized to something other than 0. This won't generate an error: the call will be interpreted as a request to change the device volume of device ID 0 (typically the only device in the system), which will affect all the volume of all the other apps in the system.

Application Usage

Whenever an application opens a wave stream, that stream is automatically associated with class 0. However, applications may move their stream to a different class by calling waveOutMessage with the proprietary MM_WOM_SETSECONDARYGAINCLASS. For example, to open a stream and associate it with class 2 one would do the following:

waveOutOpen(&hWaveOut, ...);

...

// Set gain class to 2

waveOutMessage(hWaveOut, MM_WOM_SETSECONDARYGAINCLASS, 2, 0);

Classes are differentiated from each other in two ways:

· During a call, the amount of attenuation is controlled on a per-class basis. Some classes may be muted; others are attenuated (made a little more quiet on the assumption that the phone is being held up to the user’s ear); and others may have no attenuation at all. The amount of attenuation is controlled by the shell based on a set of registry values.

· Each class may or may not be affected by the “system volume”. This behavior is hard-coded in the audio device driver.

In the currently shipping implementation there are four classes with the following behavior:

Class

Behavior During Call

Affected by system volume

Used by

0

Muted

Yes

Default setting for all sounds

1

Attenuated

Yes

?

2

Attenuated

No

Alarm, Reminder, Notification, Ring, In-call sounds

3

Muted

No

System event sounds

In addition, future implementations supporting VoIP may include two additional classes which have no attenuation during a call:

Class

Behavior During Call

Affected by system volume

Used by

4

No attenuation

Yes

?

5

No attenuation

No

?

Shell Usage

Normally, the gains of all classes are set to 0xffff, meaning there’s no attenuation. At the beginning of a phone call the shell calls MM_WOM_SETSECONDARYGAINLIMIT to attenuate each class by some amount, and calls it again during hangup to reset the attenuations. The code to do this looks something like:

for (iClass=0;iClass<NUMCLASSES;iClass++)

{

waveOutMessage(<device ID>, MM_WOM_SETSECONDARYGAINLIMIT, iClass, <volume from 0-0xffff>);

}

The amount of attenuation which the shell applies during a call is controlled by the following registry keys:

[HKEY_CURRENT_USER\ControlPanel\SoundCategories\Attenuation]

"0"=dword:0

"1"=dword:2

"2"=dword:2

"3"=dword:0

The key name is the class index, and the associated value is the amount of gain to allow during a call. The value ranges from 0 to 5, with 0 meaning totally muted and 5 meaning no attenuation.

Note that existing apps which don’t set their class will default to class 0, will be muted during a call, and will be affected by system volume (which is generally the behavior that is desired).

A note on volume values

Volume levels are typically represented as unsigned 16-bit values, with 0xFFFF being full volume and 0x0000 representing the muted state. For example, waveOutSetVolume encodes the volume parameter as a 32-bit DWORD, with the lower 16 bits holding left channel volume and the upper 16 bits holding the right channel volume. On the other hand, the gain class API only accepts a single 16 bit value which is meant to apply to both channels (we didn't think there would be a need to attenuate left and right by different amounts).

Historically there's been alot of disagreement over how these 16 bits map to actual dB attenuation values, and how the stream and device gains interact. In the wavedev2 sample driver the behavior is as follows:

  • Stream gains map from 0 to -100dB attenuation. The idea here was to provide applications with a large enough range to handle any potential situation and also maintain some compatibility with the desktop's usage in DirectSound, which uses the same 0 to -100dB range.
  • Device gains map from 0 to -35dB attenuation. The idea behind this was that historically the device gain has been implemented by going directly to the codec hardware, which at the time the API was designed typically was limited to something in the -32dB range.
  • Gainclass gain values map from 0 to -100dB.
  • When calculating the aggregate attenuation of the various gain values, the code converts each gain value to a dB attenuation and then adds the attenuations. For example, if the stream gain is 0x8000 (half scale), the device gain is 0x8000 (also half scale), and the gainclass gain is 0xFFFF (no attenuation), the total attenuation would be (.5 * 100) + (.5 * 35) + (0 * 100) = -67.5dB.
  • Any gain value of 0 represents the totally muted state. For example, if the stream gain in the above calculation started of at 0x0000, the stream would be totally muted independent of the other gain values.

The calculation above and the ranges that each gain type maps to are implemented inside the wavedev2 driver, and OEMs may choose to modify the values to meet their specific needs.

Areas for improvement

In retrospect, there are a couple of things I wish we had done differently and which we’ll keep in mind for the future:

· We should have made the number of gain classes and how they’re affected by device volume programmable, rather than hard-coding them into the sample driver. With the current design whenever we need to add a new gain class, or change whether the device volume affects a given class, we need to touch the OEMs device driver. This is typically only a one or two line change, but it still makes life difficult.

· There is currently no way to query the current attenuation level for a class.

· The waveapi component of the core OS implements a “gain class” infrastructure as part of the software mixer component to accomplish a similar goal. However, this was implemented after wavedev2 had shipped and its design is incompatible with the way Smartphone needs it to work. This is the main reason Smartphone/PPC-Phone devices need to use wavedev2 as a starting point for a driver. It would be nice if waveapi’s software mixer implemented the same gain class design so we could pull the code out of wavedev2 and simplify its design.

Responses to questions:

1. Where is MM_WOM_SETSECONDARYGAINCLASS defined?

- It should be in audiosys.h, but that might have moved around a bit. The definitions you're looking for are:

#define MM_WOM_SETSECONDARYGAINCLASS (WM_USER)

#define MM_WOM_SETSECONDARYGAINLIMIT (WM_USER+1)

#define MM_WOM_FORCESPEAKER (WM_USER+2)

Keep in mind that these are very likely to change or go away in future release of the operating system.