ASIO

native/asio/ builds tonesphere_asio.dll, a native ASIO host; tonesphere/native/asio.py binds it.

Status

   
ASIO host implementation IMPLEMENTED — driver discovery, loading, initialisation, channel/format/rate/buffer negotiation, createBuffers, a native buffer switch driving the engine, asioMessage handling, stop/dispose/release
Boundary tests without a driver VERIFIED — tests/native/test_asio.py: every Windows ASIO sample type round-trips, aligned types put the sample in the low bits, over-range clips, big-endian types are refused, a missing driver fails with the reason
Against a real ASIO driver: FlexASIO 1.10b (software ASIO driver) HARDWARE VERIFIED on the development machine, 2026-09-29 — see below
Through FlexASIO to a USB audio interface, Audio Array AI-04, WASAPI exclusive HARDWARE VERIFIED 2026-09-30: the interface’s clock drives the buffer switch at 144 frames, and its input delivers a real signal — see below
Against ASIO4ALL 2.22 (a third-party WDM-KS ASIO driver) on the AI-04, with a cable from its output to its input HARDWARE VERIFIED 2026-09-30: round trip measured at 64–512 frames, repeatable to the frame; the output heard through the cable at the level native WASAPI gives — see below
ASIO4ALL in the driver VM, on ToneSphere’s virtual cables HARDWARE VERIFIED in the VM 2026-10-01 — see below; a hang it exposed in ToneSphere’s WASAPI stop is fixed
A driver that hangs in stop() abandoned after 5 s, reported, no further ASIO load in that process: the engine side IMPLEMENTED (tests/native/test_external_backend.py); the ASIO host’s timeout UNVERIFIED — no driver that hangs in stop() was available to prove it on
Against an audio interface manufacturer’s own ASIO driver NOT AVAILABLE — the AI-04 has none: Audio Array sells it as driver-free, a USB Audio Class device on Windows’ in-box driver. Until an interface with its own ASIO driver is tested, ToneSphere’s ASIO support is proven through a software ASIO driver only

A driver name in the registry is not ASIO support. Only a driver initialised by this host, with audio moving through its buffer switch, is — and that has now happened with FlexASIO.

Verified with FlexASIO

FlexASIO 1.10b (MIT-licensed; installer from its GitHub release, SHA-256 FE496BCC08D6C421C6244C8A60AC7B538560BDA138000FD1A54AB8EBCE031209, not Authenticode-signed) was installed on the development machine for this. It is a real ASIO driver that renders through Windows audio APIs from inside the host process. tests/hardware/test_asio.py:

What this does not establish: behaviour with a hardware interface’s driver (different threading, sample types, buffer behaviour, reset requests), and anything acoustic.

Verified through FlexASIO on the AI-04

The Audio Array AI-04 (2 in / 2 out, USB, C-Media VID 0D8C PID 0269, Windows’ class driver; a guitar on input 1, earphones on the output) with FlexASIO configured for WASAPI exclusive on Line (AI-04) and Speakers (AI-04) and 144-frame buffers (%USERPROFILE%\FlexASIO.toml, removed afterwards). tests/hardware/test_asio.py, 2026-09-30:

With FlexASIO left at its defaults (shared mode, 882-frame buffers) on the AI-04, the output test does run: the 1 kHz tone came back through process loopback at exactly the level sent (rms 0.03536 against 0.03536), and the inputs again carried the guitar’s hum. The +9.2 dB measured with the Realtek above was that driver’s enhancement; the AI-04 applies none.

This is the ASIO host driving real interface hardware, through a software ASIO driver. It is not a test of any manufacturer’s ASIO driver.

Verified with ASIO4ALL on the AI-04

ASIO4ALL 2.22 (freeware by Michael Tippach; ASIO4ALL_2_22.exe from asio4all.org, SHA-256 0d4f0c63bf5df077e4c74f18372f72b8e875a9abea499fea78aaa9f56022ac7e, Authenticode-signed by Michael Tippach) installed silently (/S) on the development machine, and switched in its own panel from the Realtek to the AI-04. It talks to the device through kernel streaming (WDM-KS), a path entirely separate from WASAPI and from FlexASIO. A 6.35 mm cable joined the AI-04’s headphone output to its input. 2026-09-30:

ASIO4ALL is a genuine ASIO driver from a third party and its path to the hardware is kernel streaming, but it is still not an interface manufacturer’s driver. It was uninstalled from the development machine afterwards (its own uninstaller, /S); HKLM\SOFTWARE\ASIO lists FlexASIO alone again.

ASIO4ALL in the driver VM, on ToneSphere’s virtual cables

scripts/vm/run_driver_tests.ps1 installs ASIO4ALL 2.22 silently in the Hyper-V test VM, where the only audio devices are ToneSphere’s own cables, and runs test_asio.py and test_roundtrip.py against “ToneSphere Cable 1” as the interface (2026-10-01):

A hang, found here and fixed. After the cable tests, with ASIO4ALL playing into a cable through kernel streaming while a second engine held a WASAPI shared stream on the same cable, stopping that WASAPI engine never returned and two cores spun. The cause was ToneSphere’s: each WASAPI stream thread waited on {device event, stop event}, and WaitForMultipleObjects reports the lowest-index handle signalled — so a device event that never stops firing (as it does for a shared stream fighting kernel streaming for the same pins) hid the stop request for ever, and stop() waited to join a thread that never saw it. The stop event now comes first. With that, the test finishes in the VM (it skips, correctly: ASIO4ALL does not render through WASAPI). Two more defects were fixed on the way:

An ASIO driver also stays loaded in the process after release (an in-process COM object is not unloaded), and ASIO4ALL’s left-over state disturbed WASAPI round trips measured on the same cable afterwards in the same process. The VM runner therefore runs each test file in a process of its own.

Licence

The Steinberg ASIO SDK is dual-licensed: a proprietary licence that needs an agreement signed by Steinberg before publishing, or GPL version 3 (its LICENSE.txt says “Version 3”, without “or later”). ToneSphere uses the GPLv3 option. So:

How it works

Discovery. asio.drivers() reads HKLM\SOFTWARE\ASIO — each subkey’s CLSID and Description — and checks that the CLSID’s InprocServer32 DLL actually exists, so a half-uninstalled driver shows as broken rather than failing obscurely on load.

One thread per driver. ASIO drivers are in-process COM objects, and many assume every control call arrives on the thread that created them, and that it pumps messages. Each loaded driver gets a dedicated STA thread with a hidden top-level window (some drivers parent their control panel to it) and a message loop; every control call is marshalled onto it.

Starting. asio.start(engine, name, input_node=, inputs=, output_node=, outputs=, buffer_frames=):

  1. Loads the driver (CoCreateInstance with the driver’s CLSID as both class and interface ID — ASIO’s convention) and calls init with the hidden window.
  2. Checks every requested channel exists.
  3. Requires the engine’s sample rate: canSampleRate, then setSampleRate if needed. A driver that cannot run at the engine rate is refused, never resampled behind the user’s back.
  4. Validates the buffer size against the driver’s min/max/granularity (including power-of-two granularity), defaulting to the driver’s preferred size.
  5. Refuses any channel whose sample type it cannot convert, naming the type. Supported: Int16/24/32 LSB, Float32/64 LSB, and Int32 LSB with 16/18/20/24-bit alignment. MSB (big-endian) types and DSD are refused.
  6. createBuffers, zeroes both halves of every output buffer, reads the driver’s reported latencies, checks for outputReady support, and starts.

The buffer switch (bufferSwitch / bufferSwitchTimeInfo, on the driver’s thread) converts each input channel to float, runs the engine through ts_engine_run_block in blocks of at most the engine’s block size, converts outputs back, and calls outputReady() where supported. It joins MMCSS on first use. It never locks, allocates, logs or calls Python.

Driver messages. kAsioResetRequest and kAsioBufferSizeChange are flagged, never acted on inside the callback; the stream status then says “the driver requested a reset: restart the stream”, and the control plane restarts it. kAsioResyncRequest and kAsioOverload are counted (overloads also count as engine xruns). kAsioLatenciesChanged is flagged. kAsioSupportsTimeInfo is answered yes.

One driver per process. ASIO’s callbacks carry no context pointer, so a host can run one driver at a time. A second start while one runs is refused.

Latency

The stream status’s reported_latency_ms is what the driver’s getLatencies reports for the negotiated buffer — the driver’s claim, not a measurement. A measured round trip needs a loopback path: tonesphere/native/roundtrip.py measures WASAPI paths; with the AI-04 and no cable from its output to its input it reports -- (confidence 1.0 against a threshold of 4).

Verifying on a machine with a driver

uv run python scripts/fetch_sdks.py asio && uv run python scripts/build_native.py
uv run pytest tests/hardware/test_asio.py -m hardware -s

With FlexASIO (an open-source ASIO driver that renders through WASAPI inside the host process), the output test also captures what the ASIO host played through process loopback and checks it. With a hardware interface’s driver, output content can only be checked with a loopback cable.