Virtual Audio Cable (VAC)
20+ years of experience. Connects audio apps together since 1998.
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.
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.
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 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:
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.
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.