Wednesday, April 08, 2009

Plantronics Device Manager SDK for .NET


Plantronics upcoming Persono Suite product will introduce a new managed SDK for the Microsoft .NET platform that end user applications can use to communicate with the Plantronics USB Audio devices. This SDK will eventually replace the PersonoCall SDK 2.11.7.



Plantronics Device manager SDK will provide the host to device interface layer for all next generation Plantronics software products. The current implementation of the Plantronics device manager uses Microsoft .NET 2.0 and will be made available for .NET and un-managed COM applications


The Plantronics Device Manager SDK provides a generic application programming interface (API) for software applications while hiding the details of the of the interface implementation of a particular headset hardware model.
The device manager exposes few interfaces such as IDeviceManager for device enumeration and finding a specific device; IDevice for exposing common device behavior; IHostCommand; and IDeviceEvents for handling device specific behavior. IHostCommand abstracts all the commands a host can send to a device and IDeviceEvents abstracts all the events that a device can send to the host. As part of the device enumeration, the device manager will initialize the common device behavior and also the device specific behavior based on the device it enumerates.
The Plantronics Device Manager SDK includes managed assemblies and type libraries needed for developing applications that interfaces with Plantronics headsets. It also includes a merge module to allow for easy inclusion of all the needed redistributable files into any application’s installer.

Plantronics Device SDK Interface Specification


The Plantronics Device SDK is a managed API and is implemented using .NET 2.0 and is available for managed and unmanaged applications. The core interfaces that are part of the device SDK are IDeviceManager, IDeviceManagerEvents, IDevice, IHostCommand, and IDeviceEvents.
DeviceManager concrete class implements IDeviceManager and is the entry point for end user applications. After creating an instance of the DeviceManager application can request for a list of Plantronics devices connected to the USB or request for specific devices by specifying the Plantronics vendor ID (0x47F) or the product ID.

IDeviceManager Interface



public interface IDeviceManagerEvents
{
event DeviceStateEventHandler DeviceStateChanged;
event EventHandler Suspending;
event EventHandler Resuming;
event EventHandler QueryConfigChange;
}


public interface IDeviceManager : IDeviceManagerEvents, IDisposable
{
[ComVisible(false)]
Collection Devices { get;}
[ComVisible(false)]
Collection FindDevices(int vendorId);
[ComVisible(false)]
Collection FindDevices(int vendorId, int productId);

IEnumerator GetDeviceList();
IDevice FindDeviceForPath(String devicePath);
IDevice FindDeviceForProductID(int vendorId, int productId);
}


Property: Collection Devices { get;}
The Devices property returns a collection of supported Plantronics devices that are connected to the USB.

Method: Collection FindDevices (int vendorId)
Method: Collection FindDevices (int vendorId, int productId)
FindDevices() returns a collection of supported Plantronics devices that are connected to the USB

Method: IDevice FindDevice (String devicePath)
FindDevice() returns an Device object that matches the device path of supported Plantronics devices that are connected to the USB

Event: DeviceStateChanged
DeviceStateChanged event is triggered when a supported Plantronics device is removed or added to the USB port.

Event: Suspending
Suspending event is triggered when a the system starts to Standby or Hibernate. If a device is attached and listening for Input reports if the system enters Standby or Hibernate the device SDK automatically detaches from the device.

Event: Resuming
Resuming event is triggered when the system resumed from Standby or Hibernate.

Event: QueryConfigChange

QueryConfig event is triggered when the system is undocked. If a device is attached and listening for Input reports and an undock is requested, the device SDK will automatically detach from the device to allow a successful undock.

IDevice Interface

public interface IDevice : IDisposable
{
String DevicePath { get;}
int VendorId { get;}
int ProductId { get;}
int VersionNumber { get;}
String InternalName { get;}
String ProductName { get;}
String ManufacturerName { get;}
String SerialNumber { get;}
bool IsAttached { get;}
IDeviceAttribute Attribute { get;}

IDeviceEvents DeviceEvents { get; }
IHostCommand HostCommand { get;}

void Attach();
void Detach();
bool RegisterNotification();
bool UnRegisterNotification();
bool IsSupported(ushort usage);

event ReportDataEventHandler DataReceived;
event DeviceStateEventHandler DeviceStateChanged;
}

Property: String DevicePath { get;}

The DevicePath property returns the HID path of the device. This is the path the device manager uses to read/write to the device.

Property: String InternalName { get;}

The InternalName property returns the internal device name.

Property: String ProductName { get;}

The ProductName property returns the name under which the device is ,marketed.

Property: String ManufacturerName { get;}

The ManufacturerName property returns the name of the device manufacturer, Plantronics Inc.

Property: Int VendorId { get;}

The VendorId property returns the USB Vendor ID for Plantronics Inc, which is 0x47F.

Property: Int ProductId { get;}

The ProductId property returns the USB Product ID assigned to the device by Plantronics Inc..

Property: bool IsAttached { get;}

The IsAttached property returns true if IDevice is currently attached to a device and false if there is no currently attached device.

Method: void Attach()

Attach connects to the device and starts listening for input report from the device

Method: void Detach()
Detach disconnects from the device and stops listening for input report from the device.

Method: void RegisterNotification()

After attaching to a device RegisterNotification can be used to register for Windows USB device attach and removal notifications.

Method: void UnRegisterNotification()

Before detaching from a device call UnRegisterNotification to unregister for Windows USB device attach and removal notifications.

Method: bool IsSupported(ushort Usage)
Given a HID Usage IsSupported() can be used to find if an attached device supports a specific usage or not

Event: DeviceStateChanged
DeviceStateChanged event is triggered when a device is removed or added to the USB port. For device state change events to be triggered applications should have called RegisterNotification on the device object.

Event: DataReceived
After attaching to a device applications can register for this event to process the raw input reports that are coming from the device. This is mainly useful for troubleshooting and to verify if the device is functioning correctly.

Property: IHostCommand HostCommand { get;}

The HostCommand property returns a IHostCommand interface. The IHostCommand provides methods and properties that can be used to send specific requests to the device from the host application.

IHostCommand Interface


public interface IHostCommand
{
void Ring(bool enable);
AudioType AudioState { get; set;}
bool Mute { get; }
bool Register(RegistrationType sign);
String GetVersion(VersionType version);
}


Method: void Ring(bool enable)

Ring method controls the headset ringer.
User applications should use the ringer property to indicate an incoming call. This should be done in addition to playing a ring-tone through the headset audio stream. This is done because wireless headsets may turn off the audio channel to conserve the battery power. If audio is disabled, no ring-tone will be heard until the radio link is established. Use of the Ring method allows a ring-tone to be signaled out-of-band, and thus allows the wearer to hear the ring even if the audio link is not established. Using the Ring method in addition to the audio-channel ring-tone will ensure that the ring is heard regardless of the audio link state.

Property: AudioType AudioState { get; set; }

This property enables or disables the audio stream to the device. For a wireless device this property also controls the RF link. The link is up when audio is enabled by setting the AudioState to AudioType.MonoOn and the link is down when the audio is disabled by setting the AudioState to AudioType.MonoOff. User applications should use this property to conserve battery power on wireless devices. (For example, a telephony application would keep the audio disabled while there are no active calls and then enable the audio when a call becomes active.)
The only supported values currently for AudioState are MonoOn and MonoOff. The other values are added for future devices that will support mono and stereo.

public enum AudioType
{
MonoOn = 1,
MonoOff = 2,
StereoOn = 3,
StereoOff = 4,
MonoOnWait = 5,
StereoOnWait = 6
}

Property: bool Mute { get; }
This property can be used to get the Mute state of the microphone in the headset.

Method: void Register(RegistrationType sign)
Host applications are required to SignIn and SignOut when they start and shutdown to notify the device that they are running and are interested in listening to device events. If the host applications didn’t register some device may not send input reports on a specific HID usage page.

public enum RegistrationType
{
SignIn = 1,
SignOut = 2,
Exclusive = 4
}

Method: void GetVersion(VersionType version)
GetVersion allows the host application to request for the USB version, the base version and the headset version of a particular Plantronics device.

public enum VersionType
{
USBFirmware =1,
BaseFirmware = 2,
RemoteFirmware = 3
}

Property: IDeviceEvents DeviceEvents { get;}

The DeviceEvents property returns an IDeviceEvents interface that can be used to register for device events like TalkPressed, ButtonPressed e.t.c. Some of these events are reported by the device based on user action such as a button press or when device state changes such as RFLink up or down.


IDeviceEvents Interface


public interface IDeviceEvents
{
event DeviceEventHandler TalkPressed;
event DeviceEventHandler ButtonPressed;
event DeviceEventHandler MuteStateChanged;
event DeviceEventHandler AudioStateChanged;
event DeviceEventHandler FlashPressed;
event DeviceEventHandler SmartPressed;
}

Event: TalkPressed

The TalkPressed event is triggered when an attached device’s “Talk” button is pressed.
A client application should subscribe to the TalkPressed event. If the application is not currently in an active call, the application should answer any incoming (“ringing”) call. If the application is already in an active call, the application should terminate the current call. If the application is neither in a call nor receiving an incoming call, the application should create a new call and put it into the “dialing” state (and send the dial tone into the headset).

Event : ButtonPressed

The ButtonPressed event is triggered when any of the buttons on the attached device is pressed.
The SDK provides client application two ways to handle button press events:
Via device-independent pre-translated events such as TalkPressed, FlashPressed and SmartPressed;
Via device-specific “raw” ButtonPressed events.
Different Plantronics headsets may map different set of buttons into TalkPressed, FlashPressed and SmartPressed events. Using these events instead of the “raw” ButtonPressed event is recommended because it makes the implementation device-independent and more likely to work with any future Plantronics products without any code changes.

Event: MuteStateChanged

The MuteStateChanged event is triggered when an attached device’s headset microphone mute is changed.

Event : AudioStateChanged
The AudioStateChanged event is triggered when an attached device’s AudioState changes. AudioState may change its state either in response to the client application modifying that property, or in response to the radio link being brought up or down by the device itself.
A client application should subscribe to the AudioStateChanged event and track the device’s status. If the radio link goes down, the application should probably drop the current call. If the radio link goes up, the application should probably pick up any incoming calls (if any) or create a new call and put it into the “dialing” state (and send the dial tone into the headset).
Because changing the AudioState property also causes the AudioStateChanged event to trigger, a client application should distinguish between the audio link state changes which occurred because of an application request, and the changes which were initiated by the device.

Event: FlashPressed

The FlashPressed event is triggered when one of the attached device’s “Flash” buttons is pressed. The nButton parameter indicates the flash button’s number
(For example, the CS50/60 headset interprets long volume up and volume down presses as flash buttons 0 and 1.)

A client application may use the flash buttons to put the current call on hold while simultaneously picking up a new incoming call or activate an existing call that is already on hold state. Distinguishing between the two different “flash” buttons may be used to cycle through the current call list in two different directions

Event: SmartPressed
The SmartPressed event is triggered when one of the attached device’s “Smart” buttons is pressed.
(For example, the CS50/60 headset interprets a long mute press as a “Smart” button press.)
A client application may associate application-specific logic with the “smart” button. Possible examples are launching another application or dialing a particular pre-configured phone number/extension.

Property: IDeviceAttribute Attribute { get;}

The Attribute property returns true a IDeviceAttribute interface that can be used to retrieve Device HID capabilities and details about Input, Output and feature report capabilities. A client application can use this information to know about the capabilities of the device.
IDeviceAttribute Interface

public interface IDeviceAttribute : IDisposable
{
DeviceCap DeviceCap { get;}
InputCaps Input { get;}
OutputCaps Output { get;}
FeatureCaps Feature { get;}

DeviceUsage GetDeviceUsage(ReportType reportType);
}

Property: DeviceCap DeviceCap { get;}
The DeviceCap property returns the top level device capabilities such as the Usage, Usage Page, Input report buffer length, Output report buffer length, Feature report buffer length e.t.c. from the USB device.

Property: InputCaps Input { get;}
The InputCaps property returns the HID Input capabilities from the USB Device.

Property: OutputCaps Output { get;}
The OutputCaps property returns the HID Output capabilities from the USB Device.

Property: FeatureCaps Feature { get;}
The FeatureCaps property returns the HID Feature capabilities from the USB Device.







Code Sample

//Instantiate DeviceManager and get a list of Plantronics devices
IDeviceManager m_deviceMgr = new DeviceManager();
Collection devices = m_deviceMgr.FindDevices(VendorID.Plantronics);
foreach (IDevice device in devices)
{
//Iterate through all the devices
}

//Register for the DeviceManager events

private void RegisterDevMgrNotification()
{

m_deviceMgr.DeviceStateChanged += new DeviceStateEventHandler(m_deviceMgr_DeviceStateChanged);
m_deviceMgr.Resuming += new EventHandler(m_deviceMgr_Resuming);
m_deviceMgr.Suspending += new EventHandler(m_deviceMgr_Suspending);
m_deviceMgr.QueryConfigChange += new EventHandler(m_deviceMgr_QueryConfigChange);

}

//UnRegister for the DeviceManager events
private void UnRegisterDevMgrNotification()
{
m_deviceMgr.DeviceStateChanged -= m_deviceMgr_DeviceStateChanged;
m_deviceMgr.Resuming -= m_deviceMgr_Resuming;
m_deviceMgr.Suspending -= m_deviceMgr_Suspending;
m_deviceMgr.QueryConfigChange -= m_deviceMgr_QueryConfigChange;
}

//On Attach Register for the events

public void Attach()
{
m_device.Attach();

if (m_device.IsAttached)
{
//Register for Device events
IDeviceEvents events = m_device.DeviceEvents;
events.TalkPressed += new DeviceEventHandler(events_TalkPressed);
events.AudioStateChanged += new DeviceEventHandler(events_AudioStateChanged);
events.MuteStateChanged += new DeviceEventHandler(events_MuteStateChanged);
events.ButtonPressed += new DeviceEventHandler(events_ButtonPressed);
}
}

//Event handlers for specific button presses

void events_TalkPressed(object sender, DeviceEventArgs e)
{
//Talk Pressed
}

void events_AudioStateChanged(object sender, DeviceEventArgs e)
{
//Audio state Changed
//e.AudioState will provide the current Audio State
}

void events_MuteStateChanged(object sender, DeviceEventArgs e)
{
//Mute state Changed
//e.Mute will be true or false
}

//Generic ButtonPressed event handler to handle
//all headset button presses

void events_ButtonPressed(object sender, DeviceEventArgs e)
{
switch (e.ButtonPressed)
{
case HeadsetButton.VolumeUp:
//Volume Up Pressed
break;

case HeadsetButton.VolumeDown:
//Volume Down Pressed
break;

case HeadsetButton.VolumeUpHeld:
//Long Volume Up Pressed
break;

case HeadsetButton.VolumeDownHeld:
//Long Volume Down Pressed
break;

case HeadsetButton.MuteHeld:
//Long Mute Pressed
break;

case HeadsetButton.Unknown:
//Unmapped event
break;
}
}


//On Detach Un-register the events

public void Detach()
{
IHostCommand command = m_device.HostCommand;
if (command != null)
{
command.Register(RegistrationType.SignOut);
}

//UnRegister Device events
IDeviceEvents events = m_device.DeviceEvents;
events.TalkPressed -= events_TalkPressed;
events.AudioStateChanged -= events_AudioStateChanged;
events.MuteStateChanged -= events_MuteStateChanged;
events.ButtonPressed -= events_ButtonPressed;

m_device.Detach();
}



If you are interested in developing using the Plantronics Device Manager SDK please email me Ramesh.Theivendran@Plantronics.com or our PM Naser.Sheikhzadegan@Plantronics.com.

Friday, November 16, 2007

2.11.6 is coming up

We've found a couple of bugs in 2.11.5, and we're planning on releasing 2.11.6 soon. The changes are as follows.

First, when working with hardware that does not expose a mute control on the microphone line, the SDK would incorrectly grab on to the speaker mute control.

Second, in certain intermittent cases on Windows Vista the SDK would try to attach to the mixer before it was ready. This failure would lead to dereferencing a null pointer further on down the line.

Both of these have been fixed. The only other change is the addition of new Product ID to support the upcoming release of a new product revision.

Thursday, August 23, 2007

SDK 2.11.5 is available

The 2.11.5 version of the SDK is now available. The basic structure and the way everything works is the same. The changes are:
  • Updated to version 8 of the C runtime.
  • Added support for the DA45 hardware.
  • Added support for the Calisto hardware.

Friday, April 13, 2007

IAudioEndpointVolume

As I mentioned in my last post, our attempts to synchronize Windows' mixer with the hardware is one of the known issues with Vista compatibility. I've been working on this the last few days and I now have code that should work. This post is about my understanding of the system and how I got it to work. While I don't think there are many that are facing exactly the challenges we are, I suspect my experiences may shed some light on the system as a whole which you might find useful.

The new audio system in Windows Vista (WASAPI) is made up of a cornucopia of streams, sessions, endpoints, audio processing objects, and so on. Going into detail on all of these is way beyond the scope of this post.

One of the bits you do need to understand is about sessions. Every process that works with audio has one session that is implicitly created by default. The new WASAPI interfaces allow you to manipulate this session and create more. If you are still using the old multimedia system, you obviously don't have access to session manipulation functions and you're stuck with the default one. This isn't really a problem for most purposes. The reason it is a problem for us is that all mixer adjustments through the old multimedia API only apply to this default session object, which is local to your process and do not affect the rest of the system. Muting the microphone just for the PerSonoCall process and leaving the rest of the system alone isn't too useful. The point of the integration is to ensure the hardware mute is in sync with the system-wide mute.

This leads us to the next little bit of WASAPI that needs to be understood, endpoints. An endpoint is something capable of rendering audio (e.g. speaker) or capturing audio (e.g. microphone). An endpoint is the lowest level of access to audio properties before everything disappears into kernel-space drivers and the hardware itself. If you are looking at the mixer control panel in Vista, the master volume slider on the left represents the endpoint's setting, while the subordinate mixers to the right represent the settings for all the currently active sessions.



Changing settings at the endpoint is exactly what we need to do in the Plantronics SDK. To accomplish this, we fist need the absolutely latest platform SDK. Once that is in place we need to include Mmdeviceapi.h and Endpointvolume.h from the SDK's /include directory. The last thing to note is that all the new WASAPI stuff is done through COM interfaces, meaning you can add the code to your project and compile without introducing any Vista dependencies on your user's machine. Just make sure your on Vista before you try to instantiate any WASAPI objects. Also, since this is Vista code, we need to make sure WINVER, _WIN32_WINNT, _WIN32_WINDOWS, _WIN32_IE are defined to the proper value of 0x0600.

The place we actually start code-wise is with IMMDeviceEnumerator, which will get us a list of endpoints in an IMMDeviceCollection. That collection gives us access to a series of IMMDevice entries that represent devices meeting the specifications we pass in to IMMDeviceEnumerator::EnumAudioEndpoints(). In this case we'll be asking for active capture devices.

//Obtain a device enumerator, the root of all this Vista multimedia businesss
ATL::CComPtr deviceEnumeratorPtr;
deviceEnumeratorPtr.CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL);

//use the enumerator to obtain a collection of devices
ATL::CComPtr devCollectionPtr;
deviceEnumeratorPtr->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &devCollectionPtr);

Next we'll work our way through the collection checking one device at a time until we find the one we want. In this case, a CS50/CS60-USB. Getting a specific device is a little bit complicated. If you aren't looking for a specific device, using IMMDeviceEnumerator::GetDefaultAudioEndpoint() is easier. The reason it is complicated is that we'll need to use the property store system, another new thing with Vista. We'll need propvarutil.h from the platform SDK's /include directory as well as propsys.lib from the /lib directory. Note that adding this library file introduces a dependency on the presence of propsys.dll, which won't be around on any system other than Vista. Marking propsys.dll as a delayed load solves this problem, but has some other consequences beyond the scope of this post.

A property store is a bag of key/value pairs. The key has its own specific PROPERTYKEY struct that consists of a GUID identifying the property's family and a DWORD identifying the specific property. The value accessed by the key is a PROPVARIANT which isn't much different from the run-of-the-mill VARIANT you probably already know. This property system was developed for the Windows shell and for working with files. In WASAPI's case it is being used with vaguely file-like objects such as endpoints. You can see a lot of the property keys used by the shell are already defined in propkey.h in the SDK's /lib directory. There's a few defined for the audio system in mmdeviceapi.h. Unfortunately the ones we need aren't there. I've determined that {B3F8FA53-0004-438E-9003-51A46E139BFC}.6 appears to be the correct value through experiment. It works, but don't take it as gospel or anything. Also note that opening the property store obtains a lock on the object, so open it, read it, and close it quickly.

//Find the device in the collection that represents the CS50/CS60-USB by going through the collection one device at a time
UINT devCount = 0;
devCollectionPtr->GetCount(&devCount);
for(UINT i = 0; i < devCount; i++)
{
//Obtain a device pointer
ATL::CComPtr devicePtr;
devCollectionPtr->Item(i, &devicePtr);

//open the device's property store
ATL::CComPtr propertyStorePtr;
devicePtr->OpenPropertyStore(STGM_READ, &propertyStorePtr);

//check the proptery store for the device name
GUID MixerNamePropertyGUID = { 0xB3F8FA53, 0x0004, 0x438E, { 0x90, 0x03, 0x51, 0xA4, 0x6E, 0x13, 0x9B, 0xFC } };
DWORD MixerNamePropertyID = 6;
PROPERTYKEY mixerNamePropKey = {MixerNamePropertyGUID, MixerNamePropertyID}; PROPVARIANT mixerNamePropVar;
WCHAR* mixerNamePropVarStr;
propertyStorePtr->GetValue(mixerNamePropKey, &mixerNamePropVar);
PropVariantToStringAlloc(mixerNamePropVar, &mixerNamePropVarStr);

//if the device's property store says it is the CS50/CS60-USB, toggle its mute setting
if(0 == wcscmp(mixerNamePropVarStr, L"CS50/CS60-USB Headset"))
{
//use the device pointer to get an audio endpoint volume pointer
ATL::CComPtr audioEndpointVolumePtr;
devicePtr->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&audioEndpointVolumePtr);

//get the current mute setting and invert it.
BOOL muteSetting;
audioEndpointVolumePtr->GetMute(&muteSetting);
muteSetting = !muteSetting;
audioEndpointVolumePtr->SetMute(muteSetting, NULL);
}

//free the storage that contains the device name that was allocated by PropVariantToStringAlloc
CoTaskMemFree(mixerNamePropVarStr);
}

Starting from the top there's a few things to note. First we get a IMMDevice with IMMDeviceCollection::Item(). We then get an IPropertyStore with IMMDevice::OpenPropertyStore(). We then build a property key, which we pass into IPropertyStore::GetValue(). We turn this into a string we can work with using ::PropVariantToStringAlloc(). It is important to note that this allocates memory that needs to be cleaned up with CoTaskMemFree(). Once we find a matching name string we get an IAudioEndpointVolume with IMMDevice::Activate(). We finish up with IAudioEndpointVolume::GetMute() and IAudioEndpointVolume::SetMute().

That's a lot to take in all at once. The quick summary of the basic flow is IMMDeviceEnumerator to IMMDeviceCollection to IMMDevice to IAudioEndpointVolume, with a brief digression into IPropertyStore to make sure we have the right device.

Wednesday, March 21, 2007

Vista support

We've received a few e-mails of the form "Our team is looking for the drivers for Vista. Do you have them available?"

First, the pedantic point. We don't work with anything that meets the technical definition for a driver. That is, everything in the SDK is user-mode application code. All the drivers involved in supporting our hardware are supplied by Microsoft as part of the basic default HID and USB audio support for the Windows operating system.

As for Vista, there will not be any new code released to support Vista. That's because what is already out there should work fine as it is. Of course if it isn't, let me know by all means.

In the interest of completeness: there are two known issues with Vista.

One, We attempt to keep the Windows mixer microphone mute and the hardware microphone mute settings in sync. This doesn't work with Vista. We'll try to resolve this in a future release, but don't consider this critical. It doesn't cause any problematic behavior, or throw any exceptions.

Two, the audio device portion of the Voyager 510-USB and .Audio 910 dongles have a known problem with Vista that occurs with an old version of firmware. (Base firmware version 1.1.5 on the current status tab of PerSonoCall, or 1p15 printed on the black side of the dongle on the far end from the USB connector). Later versions of these dongles should work correctly. If this applies to you, you will get a message from Windows when you first connect the device telling you the dongle isn't working properly. If this is your situation, contact Plantronics tech support and they should be able to help you get sorted out with a replacement dongle.

Finally, let me repeat myself. Please don't assume there will be a Vista update to the SDK coming, since as far as we can tell it should already be working. If you are trying to use the SDK with Vista and it isn't working please let us know.

Wednesday, December 06, 2006

How to keep up to date with availalbe audio devices.

There are three things that need to be done if you want to keep up to date with what audio devices are available. The first is to register for device change notifications.

DEV_BROADCAST_DEVICEINTERFACE notificationFilter;
::ZeroMemory( ¬ificationFilter, sizeof(notificationFilter) );

notificationFilter.dbcc_size = sizeof(notificationFilter);
notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;

m_hDevNotify = ::RegisterDeviceNotification(
m_hWnd,
¬ificationFilter,
DEVICE_NOTIFY_WINDOW_HANDLE|DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);

When a device is added or removed, such as a USB headset, everyone gets a few WM_DEVICECHANGE messages. However without the call RegisterDeviceNotification, you'll only get the DBT_DEVNODES_CHANGED version of WM_DEVICECHANGE. You will not get important messages like the DBT_DEVICEREMOVECOMPLETE version of WM_DEVICECHANGE. This means that our attempt to keep the audio device list up to date (below) will always be one step behind on device removals.

It is possible to limit notifications to a particular kind of device. You'll note that we're not settings the interface class GUID in our notification filter and we're using the DEVICE_NOTIFY_ALL_INTERFACE_CLASSES. We're doing this because I'm lazy and restricting notifications to a particular device type doesn't serve any useful purpose in this example.

It is important to note that you need to have something to notify. In this case it is a window handle. A service status handle will also work. Just sticking in NULL for the window handle won't work.

Finally, note that we are hanging on to the return value from RegisterDeviceNotification. This is a handle that is used with UnregisterDeviceNotification when we clean up and exit. The return value should also be checked to make sure the call succeeded, but such error checking has been omitted here for clarity and simplicity.

Next, you need to make sure you have a handler set up for WM_DEVICECHANGE messages in your message map. In MFC it'll look something like:

BEGIN_MESSAGE_MAP(CListAudioDevicesDlg, CDialog)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
ON_WM_DEVICECHANGE()
ON_BN_CLICKED(IDC_BUTTON1, &CListAudioDevicesDlg::OnBnClickedButton1)
END_MESSAGE_MAP()

In our ATL/COM objects in the SDK it looks more like:

BEGIN_MSG_MAP(ThisClass)
MESSAGE_HANDLER(WM_DEVICECHANGE, OnDeviceChange);
END_MSG_MAP()

Finally, on initial startup, and in response to WM_DEVICECHANGE messages we step through the available device list. That looks something like this:

std::vector<tchar*> deviceNameVector;
int numDevs = ::mixerGetNumDevs();
for(int i = 0; i < numDevs; i++)
{
MIXERCAPS mcaps;
mixerGetDevCaps(i, &mcaps, sizeof(mcaps));
vec.push_back(mcaps.szPname);
}

This will leave you with a vector of the device names the Window's mixer knows about. Of course, what exactly you do here will depend on your application and what you're trying to accomplish.

Friday, October 27, 2006

Windows USB Audio Device Selection

The selection of audio devices in Windows can be a little awkward. I suppose that's to be expected since it hasn't seen much change since when our bearskin clad forbearers carved images of Windows 3.1 into the cave walls. This awkwardness has only been exacerbated by new developments like USB.

Modern Windows systems are capable of handling many simultaneous audio devices. However, the only one that really matters is the default device. The default device is where all the bings and bongs of you system sounds go. It is where media players pipe their output unless told otherwise. And so on. All audio goes to the default device unless the application takes explicit steps to do otherwise.

(Brief aside: There is a default input device and a default output device which are interacted with separately in a parallel fashion. I'm only talking about output for the sake of simplicity. The input audio device selection works separately from but identically to output audio.)

You can change the selection of the default audio device through the control panel. The control panel's name is "Sounds and Audio Devices" on WinXP. The audio tab is where the setting resides. The drop downs under sound playback and sound recording will show all the sound devices currently available on your system, and when changed, set the default audio device for your system.

I know this is sort of boring. The reason I'm bringing it up is because things get more complicated once you start plugging USB audio devices (like our headsets) into the machine.

The first thing you need to know about USB audio is that when it discovers a new device it sets that device to be the default device. That means, when you plug in your new Voyager-510USB Bluetooth dongle, all your system sounds go away. When you unplug the dongle, they all come back. What's going on? They haven't actually gone away, the USB audio driver is just setting Voyager up as the default audio device. If you bring up the radio link you'll hear the system sounds just fine.

The second thing you need to know is that the selection process for the default audio device has a certain amount of memory. If you set your default audio device back to your sound card after the USB driver snatched it away the system will remember and not do it again for that particular device. However the hardware actually needs to be connected for this to work. You can't select a preference for your sound card over your USB device if the USB device isn't connected.

The third thing you need to know is what USB considers a device. Every device needs a unique identifier. If two devices have a different unique identifier, they are considered two different devices for the purposes of audio device selection, and thus present two separate opportunities for the USB audio system to change your default audio device. Some devices provide a serial number as part of the USB enumeration process, which is used as the unique identifier. Others, such as the CS50/60-USB and Voyager510-USB don't. In cases where a serial number isn't provided, the USB driver makes one up based on ancillary information. Part of that ancillary information includes the port that the device is connected to.

If you've been paying attention, you can probably guess where this is going. When you plug your device into a different port, the USB driver calculates a different unique identifier, and the USB audio system says "Ah ha! a new device" and gets another chance to change your default audio device again. This is why you sometimes get "new device discovered" messages when you plug in a USB device, even though you know you've had it for a while and have had it connected before. You are getting "new device discovered" because you just happened to connect it to a USB port you hadn't used with that device before.

The unfortunate bottom line of all of this is that there isn't much we can do. The device drivers and device selection routines are all out of our hands. And, as far as I know, default audio device selection is immune to programatic control. The only solution that we've found so far has been awareness of the issue and doing our best to help users catch on to what is happening and fix it as best they can.