User manual

VAC application programming interface (API)

VAC driver supports two API sets: common driver API to interact with driver and cable parameters/states, and filter API to interact with a particular Kernel Streaming filter exposed by the driver. For every Virtual Cable, the driver creates two filters: capture and render. Every filter exposes Wave and Topology pins: wave pin is used for data transfer, topology pins are used for endpoint naming, volume control or some other purposes.

To access each API, user-mode client process should obtain the appropriate device interface path. This path should be passed to CreateFile to access (open) the interface, and then DeviceIoControl should be used to communicate with the driver.

VAC driver API

Driver API can be used to control all VAC driver functions, as VAC Control Panel does (Control Panel uses Driver API for this).

There are several ways to obtain device interface paths; the simplest is the following:

To obtain common VAC driver interface path, pass VAC product GUID (83ed7f0e-2028-4956-b0b4-39c76fdaef1d) to SetupAPI functions. Then append API interface GUID (0d13c528-a6fc-47f1-8fee-c5dd46f1fbb1, defined in vacapi.h) with a separating backslash, to form Driver API interface path, and pass it to CreateFile. Then use file handle to call DeviceIoControl.

vacapi.h (MS VC++ syntax) defines supported driver functions and request data structures. Every driver request must be passed with a header (DriverReq), containing API version number in ApiVer. All driver request structures are derived from DriverReq. Driver version 4.70 supports API version 6, driver versions 4.60-4.66 support API version 5.

After filling the appropriate data structure, call DeviceIoControl with the appropriate function code (IoCtl_Local_Xxx) in dwIoControlCode, request data pointer and size in lpInBuffer/nInBufferSize, and optional return data buffer/size in lpOutBuffer/nOutBufferSize.

You can download the examples for API version 5 and for API version 6.

Driver and cable requests

All requests are divided into driver-related and cable-related requests. Driver requests perform operations concerning the entire driver, while cable requests are focused on a particular cable.

All cable request data structures are derived from CableReq, specifying zero-based cable number/index in CableNum.

VAC filter API

VAC filter API can be used to access some private functions of the KS wave filter. The API should be accessed via system-defined KS interface path. This interface path can be obtained several ways:

  • In MME: use waveInMessage/waveOutMessage with the endpoint ID and DRV_QUERYDEVICEINTERFACE to get the interface path of the wave filter corresponding to the endpoint. This is the simplest way.
  • In WASAPI and MMDevice API: open the property store of the appropriate endpoint, using IMMDevice::OpenPropertyStore, and find the property named "{233164c8-1b2c-4c7d-bc68-b671687a2567},1" of type VT_LPWSTR. The resulting string starts with a number enclosed in curly brackets, following by the wave filter interface path. See this example for details.
  • Global: by enumerating (using SetupAPI functions) all device interfaces in KSCATEGORY_RENDER and KSCATEGORY_CAPTURE. VAC filter interface paths contain VAC product GUID (83ed7f0e-2028-4956-b0b4-39c76fdaef1d) and "wave" in the reference string at the end (after the last backslash).

After obtaining wave filter interface path, open it with CreateFile, and use DeviceIoControl to send a KS property request, specifying IOCTL_KS_PROPERTY in dwIoControlCode, a pointer and size of KSPROPERTY structure in lpInBuffer/nInBufferSize, and optional data pointer/size in lpOutBuffer/nOutBufferSize.

KSPROPERTY structure must contain the property set ID (0d13c528-a6fc-47f1-8fee-c5dd46f1fbb1, defined as KSPROPSETID_VAC or KSPROPSETID_VirtualAudioCable), a particular property ID (KSPROPERTY_VAC_Xxx or KSPROPERTY_VirtualAudioCable_Xxx) and the appropriate get/set flag (KSPROPERTY_TYPE_GET/KSPROPERTY_TYPE_SET).

To check if a particular property is supported, specify KSPROPERTY_TYPE_BASICSUPPORT flag instead of get/set flags. If the request is completed successfully, VAC driver supports the specified property. If ERROR_SET_NOT_FOUND is returned, this is either not VAC driver, or a version of VAC driver  that doesn't support filter property requests (prior to 4.60). If ERROR_NOT_FOUND is returned, this is VAC driver that supports filter property requests but doesn't support the specified request.

Since property request format is standardized, it is safe to send such requests to any KS filter to determine if it belongs to VAC driver.

Filter API requests

  • KSPROPERTY_VAC_GetKsInterfaceDetails: get the details of the KS interface. Returns KsInterfaceDetailsReq structure containing zero-based cable number in CableNum and the subdevice type (render/capture) in SubdeviceType.
  • KSPROPERTY_VAC_GetApiInterface: get driver API interface path into the output bufer. The buffer must be large enough for a typical interface path (100-130 wide chars, 200-260 bytes). The path can be immediately passed to CreateFile.
  • KSPROPERTY_VAC_SetClientClockFactor: set client clock factor/multiplier. ClientClockFactorReq::Numerator must contain the numerator of the ratio where the denominator is ClientClockDenom (currently 1000). Valid values are +/- 10% (currently 900..1100), the precision is 0.1%. The process that issued this request becomes a "cable clock client", and subsequent requests from other processes are  denied until the client process disconnects from the cable clock (by issuing a request with zero numerator, or by termination).

Recommendations for adjusting the cable clock

The client clock control feature is intended to adjust the cable clock rate to match the clock rate of the audio device with which the cable is in a "streaming pair", to avoid clock rate difference issues. But it is impossible to equalize clock rates just once, because different clocks always have small deviations. So the adjustment should be repeated on a regular basis.

The ability of the intermediate buffer of the audio device to compensate for unpredictable delays is maximized when the buffer of the recording device is kept almost empty, and the buffer of the playback device is kept almost full. When two devices having independent clocks become a "streaming pair", the states of their buffers begin to diverge. For example, if the clock rate difference is 0.5%, the difference in buffered data durations/amounts will diverge by 1 ms every 200 ms, or by 5 ms every second. If buffer duration of each device in the pair is 200 ms, then after 20 seconds one buffer will remain almost empty or full, but the other will become only half full or empty, or both buffers will become partially empty and full.

If you adjust the speed once, the divergence will almost stop, but the buffers will remain in the same state, their buffering ability will be worse than in the optimal state. So the best way is to regularly monitor the rate/speed difference, and issue clock adjustment requests as soon as the tendency to divergence is detected. The frequency of clock adjustment requests is best tied to the buffer duration, not size, because the same buffer size has different durations at different data rates.

Calculation of actual stream speeds (sampling rates) and comparing them is not the best way to synchronize the streams. As mentioned above, even if the speeds are close to each other, the buffers may remain out of their optimal states. Thus, a more reliable way is to maintain a minimum deviation between the current buffer state and the optimal one. For example, if the streams start when the recording buffer is empty (100% available for data), and the playback buffer is full (100% filled with data), the difference between the amounts of available/filled parts will indicate in which direction you need to adjust the speed.

For example, if the recording buffer has a tendency to empty, while the playback one remains full, it means that the playback side is faster than the recording one, and vice versa. If the Virtual Cable is on playback side, you need to set cable clock factor below 1 to slow it down; if it is on the recording side, you need to set cable clock factor above 1 to speed it up.

To avoid excessive speed fluctuations, it makes sense to use a kind of adaptive control. Even a simple PID controller can give a good result. Due to the stepwise nature of data movement in streams, it is better to apply at least the simplest filtering algorithm (the simple moving average on 2-4 samples may be enough) to the difference of buffered amounts, before passing it to the controller algorithm. It is also better to operate with relative buffered amounts (for example, in percentages) instead of absolute units, to eliminate dependence on the audio data format.

Important: all cable clock adjustment methods act on the cable, not on a specific stream(s). All streams associated with the cable are affected. For example, if one process transmits audio data from a microphone to a Virtual Cable, another process transmits from the same Virtual Cable to speakers, and either of these processes changes cable clock rate, the other process will encounter a change in the data rate in its own stream. Thus, it is not possible to use the clock control feature to synchronize the entire chain. To synchronize multiple devices, only data resampling can help.

However, it is possible to synchronize one Virtual Cable with another audio device, and synchronize another Virtual Cable with the first one. If needed, the third cable can be synchronized with the second one, and so on.