A real-time Windows audio tool that delays one headphone channel by a fraction of a millisecond, sample-accurate and click-free.
AudioSkew is a small Windows 11 desktop tool that takes whatever your PC is playing and delays one stereo channel by a tiny, adjustable amount (0.1 ms steps; the slider covers 0-20 ms by default and can be widened up to 1000 ms) before it reaches your headphones. It is meant for headphone listening to lectures and videos, where a slight interaural timing offset can make the sound feel more spatial and, for some people, easier to focus on.
B toggles bypass. Presets, custom presets, system-tray menu,
settings saved as JSON.Windows apps --> [sink endpoint] --loopback--> AudioSkew --> [your headphones]
Download: take AudioSkew.exe from the
latest release - a single file, no installer and no
drivers. Windows SmartScreen will warn on the first launch because the
executable is not code-signed: choose More info then Run anyway. To compile
it yourself instead, see Building.
Windows lets a program loopback-capture a playback endpoint: it receives the mixed audio of every application that plays into that endpoint. AudioSkew captures one endpoint (the sink), delays one channel, and plays the result to another endpoint (your headphones).
The sink must be an endpoint you do not listen to. If AudioSkew captured the headphones themselves it would capture its own output again and again (feedback), and you would hear the original audio plus the delayed copy. Good sinks:
| Sink | Notes |
|---|---|
| VB-CABLE (free virtual audio cable, https://vb-audio.com/Cable/) | Recommended on laptops. Install once; "CABLE Input" appears as a playback device. |
| An unused digital/HDMI output (S/PDIF, a monitor without speakers) | Free and common on desktops. |
| Any other virtual playback device you already have (Steam Streaming Speakers, Voicemeeter, ...) | Works the same way. |
While the engine runs, AudioSkew's auto-route option makes the sink the Windows default playback device so every app plays into it automatically, and restores your previous default when you stop or exit. You do not have to touch the Sound control panel after the first setup.
The full reasoning, including a comparison with APO / virtual-device / Equalizer APO approaches, is in docs/ARCHITECTURE.md.
Standard CMake (from a "x64 Native Tools Command Prompt for VS 2022", or any
shell where cl.exe is on the PATH):
mkdir build
cd build
cmake ..
cmake --build . --config Release
ctest -C Release --output-on-failure
If you only have the Build Tools and nothing on the PATH, the helper script sets up the environment and uses the bundled Ninja:
scripts\build.bat Release test
Outputs (in build\ or build\Release\ with the Visual Studio generator):
| File | Purpose |
|---|---|
AudioSkew.exe |
the application (statically linked, single file) |
audioskew_devices.exe |
list playback endpoints and their IDs |
audioskew_tone.exe |
play a test tone / noise into a device |
audioskew_headless.exe |
run the engine without a GUI |
audioskew_measure.exe |
measure the left/right delay actually rendered |
test_*.exe |
unit tests |
CMake options: -DAUDIOSKEW_BUILD_TESTS=OFF, -DAUDIOSKEW_BUILD_TOOLS=OFF.
AudioSkew.exe.Windows now sends all application audio to the sink; AudioSkew plays it, skewed, on your headphones. The status line shows Running, the estimated latency and the underrun counter. The "In" meter must move when something plays; if it stays empty, see Troubleshooting.
The choices are remembered. On later launches AudioSkew starts automatically
(auto_start), and it can start hidden in the tray (start_minimized or
AudioSkew.exe --minimized).
| Control | Effect |
|---|---|
| Delayed channel Left / Right | which ear gets the delayed signal; switches with a 10 ms crossfade |
| Delay slider | 0.1 ms steps; the box next to it shows the value and is editable: type e.g. 35.5 and press Enter (Esc cancels). The sample count at the engine's rate is shown underneath |
| Range | slider span: 0-20 ms (default), 50, 100, 200, 500 or 1000 ms. Typing or loading a value beyond the span widens it automatically; the engine always has the full 1000 ms available, so no restart |
| Preset | Normal 0 ms, Subtle 0.5, Light 1, Medium 2, Strong 5, plus your own (Save... / Delete) |
| Bypass button, Space or B | output equals input exactly; toggles instantly and click-free |
| Start / Stop | opens/closes the devices; changing a device while running restarts the engine |
| Show log | device events, sample rates, periods, underruns, drift corrections |
| Window close / minimise | hides to the system tray; audio keeps running |
| Tray icon: double-click | open the window |
| Tray icon: right-click | Enabled (toggles bypass), Delay presets, Delay Left/Right, Open, Exit |
The delay is defined as D = round(sampleRate * ms / 1000) samples. At
48 kHz: 0.5 ms = 24, 1 ms = 48, 2 ms = 96, 5 ms = 240, 10 ms = 480. The
"samples @ Hz" line always uses the real rate of the running engine (the
sink's mix rate; 44.1, 48 and 96 kHz are all supported).
%APPDATA%\AudioSkew\settings.json, written automatically:
{
"version": 1,
"delay_ms": 2,
"max_delay_ms": 20,
"channel": "right",
"bypass": false,
"output_device": "{0.0.0.00000000}.{...}",
"sink_device": "{0.0.0.00000000}.{...}",
"auto_route": true,
"auto_start": true,
"start_minimized": false,
"crossfade_ms": 10,
"buffer_margin_ms": 10,
"prefer_low_latency": true,
"pending_restore_device": "",
"window_x": 100,
"window_y": 100,
"custom_presets": [ { "name": "Lecture", "delay_ms": 1.7 } ]
}Advanced keys: buffer_margin_ms trades latency for robustness (see below);
crossfade_ms is the parameter-change ramp; prefer_low_latency requests the
driver's minimum period via IAudioClient3. pending_restore_device is
AudioSkew's crash-safety note: if it is non-empty at start-up, the previous
default device is restored before anything else.
audioskew_devices # list endpoints: index, name, ID, mix format
audioskew_devices --watch # ... and print again whenever devices change
audioskew_tone --device 2 --seconds 3 # 440 Hz sine on device index 2
audioskew_tone --device "CABLE" --noise --amp 0.05 # white noise, name substring match
audioskew_headless --sink 2 --out 1 --delay 2 --channel right --seconds 30
# engine without GUI; prints diagnostics every second
# (--bypass, --margin MS, --no-lowlatency, --auto-route)
audioskew_measure --device 1 --seconds 3 --expect-ms 2
# loopback-captures device 1 and cross-correlates L vs R
audioskew_measure is how the delay is verified programmatically: play noise
into the sink, run the engine, measure the output endpoint. A typical run:
Measured inter-channel delay: +96 samples = +2.000 ms (right relative to left)
Correlation at peak: 0.9994, next best elsewhere: 0.0075
Expected +96 samples (+2.0 ms): MATCH
Added latency is roughly 0.5 x sink period + 2 x output period + margin:
| Driver periods (sink / output) | Margin | Added latency |
|---|---|---|
| 10 ms / 10 ms (typical HDMI, many onboard codecs) | 10 ms | ~35 ms |
| 10 ms / 3 ms | 10 ms | ~21 ms |
| 3 ms / 3 ms (drivers that support small periods) | 10 ms | ~17 ms |
| 3 ms / 3 ms | 5 ms | ~12 ms |
The status line shows the live estimate. The margin (buffer_margin_ms)
exists because Windows briefly stalls the loopback whenever any application
starts or stops a stream on the sink; 10 ms covered that in testing, 5 ms
occasionally did not. After an underrun the engine grows the margin
automatically (one gap instead of a burst of glitches).
The sink and the headphones run on different clocks. AudioSkew watches the buffer level and, when it drifts, drops or repeats a few frames with a 1 ms crossfade. This is inaudible on speech and the log shows every correction. Bluetooth headphones add their own latency but the left/right offset is still exact, because only the relative timing matters.
Quality: processing is 32-bit float, the only operation is an integer-sample delay, and bypass returns the input bit-for-bit. Nothing changes volume.
IPolicyConfig, the
same one SoundSwitch and EarTrumpet use). If it fails, set the default
playback device to the sink yourself in Sound settings.AudioSkew.exe --minimized in shell:startup if you want it).| Symptom | Cause / fix |
|---|---|
| "Windows protected your PC" when starting the downloaded exe | The build is not code-signed. Click More info then Run anyway, or right-click AudioSkew.exe > Properties > tick Unblock > OK. |
| "In" meter stays empty, status says (sink is silent) | Applications are not playing into the sink. Check that the sink is the Windows default device (auto-route), or that the app is not pinned to another device in Volume Mixer. Some apps only follow the default device when restarted. |
| No sound at all but the meters move | Wrong output device selected, or headphones muted in Windows. |
| Everything is heard twice / echoes | Sink and output are the same physical path (e.g. sink = laptop speakers with headphones plugged into the same jack). Use a virtual cable. |
| "The sink and the output must be different" | Exactly that. |
| Windows still points at the sink after a crash | Start AudioSkew once; it restores the previous default device on launch. Or pick your headphones in Sound settings. |
| Sample rates differ (log warning) | Windows resamples; fine, but set both endpoints to the same rate in Sound settings > device > Advanced for the lowest latency. |
| Underruns keep increasing | Raise buffer_margin_ms in settings.json, or set prefer_low_latency to false. Check for CPU-hungry drivers (DPC latency). |
| Clicks when a video starts | That is the sink stall described above; the margin grows automatically after the first one. |
src/
main.cpp entry point, single-instance, crash recovery
audio/
ChannelDelay.* integer delay line (framework-independent)
StereoDelayProcessor.* L/R taps + click-free crossfades (framework-independent)
RingBuffer.* capture->render FIFO with crossfaded drift corrections
AudioFormat.* WAVEFORMATEX <-> float stereo conversion
DeviceEnumerator.* MMDevice enumeration + change notifications
WasapiCapture.* loopback capture + silent keep-alive stream
WasapiRenderer.* event-driven output (IAudioClient3 when possible)
DefaultDeviceSwitcher.* IPolicyConfig (auto-route)
AudioEngine.* the real-time thread, priming, drift control, diagnostics
ComHelpers.h RAII for COM/handles, HRESULT text
config/Settings.* JSON settings + presets
core/Json.* minimal JSON parser/serialiser
diag/Log.* thread-safe logger (never used on the audio thread)
ui/ Win32 GUI: MainWindow, TrayIcon, InputDialog
tools/ devices / tone / headless / measure
tests/ unit tests (ctest)
docs/ARCHITECTURE.md design notes and the routing analysis
scripts/build.bat build helper for Build-Tools-only machines
scripts/make_icon.py regenerates res/AudioSkew.ico
The delay DSP (ChannelDelay, StereoDelayProcessor, RingBuffer) has no
Windows dependencies and is fully covered by unit tests, so it can be reused in
an APO or a plug-in later without changes.
MIT - see LICENSE. Use it, fork it, ship it; no warranty.
The delay DSP is deliberately free of Windows dependencies, so if you want to
reuse ChannelDelay / StereoDelayProcessor / RingBuffer in a plug-in or an
APO, take them.