Engineering report: the Windows-native migration

Branch windows-native, 2026-09-29 to 2026-10-01. Development machine: Intel Core i7-1165G7 (4C/8T), Windows 11 Pro 26200, Realtek ALC257 (speakers, headphone output, microphone), Intel SST microphone array; from 2026-09-30 also an Audio Array AI-04 USB interface — first with a guitar and earphones, then with a 6.35 mm cable from its headphone output to its input. The virtual driver was tested in a Hyper-V VM on the same machine; Linux routing in WSL2.

Every figure below was measured on that machine unless it says otherwise, and cites the test or tool that produced it. A figure that was not measured is --.

1. Outcome

ToneSphere on Windows now runs its audio on a native real-time engine — C++20 behind a flat C ABI, loaded by ctypes — with native WASAPI and ASIO backends and a native VST3 host. The Python control plane (AudioEngine) and its three front ends (Qt UI, REST API, CLI) run on it unchanged, through a NativeHost implementing the interface the PortAudio host did. The UI gained a plugin browser, insert chains with parameters and editors, and a diagnostics view that measures the round trip instead of reporting it. A Windows kernel driver publishing virtual cables — two by default, up to eight, each managed from the UI — carries audio between applications at exactly the level sent, in a test VM; it is test-signed, so it cannot be offered to anyone until Microsoft signs it. The second half of the work (M13–M20) closed every other gap the README listed: a measured round trip through an interface and acoustically, a thread-safe control plane, devices that come and go, whole- system loopback as a source, built-in effects in the UI, TCP send, an adaptive jitter buffer, Opus, bus routing on the Linux/macOS host, plugins from the REST API and CLI, and instruments.

Area Level Evidence (section)
Native engine: plans, mixer, strips, DSP, rings, resampler, meters, statistics VERIFIED §3.1
Native WASAPI (shared, exclusive, raw, loopback, process loopback, multi-clock) HARDWARE VERIFIED §3.2
Native ASIO host (separate GPLv3 DLL) HARDWARE VERIFIED against FlexASIO and ASIO4ALL, both on a USB interface (Audio Array AI-04), and ASIO4ALL in the driver VM; a manufacturer’s ASIO driver not available (the AI-04 has none) §3.3
Native VST3 host VERIFIED (test plugin); HARDWARE VERIFIED with Surge XT; commercial plugins not tested §3.4
Measured round trip HARDWARE VERIFIED: digital path; through the AI-04 and a cable (WASAPI and ASIO); acoustically on the laptop, just above the confidence threshold §3.5
AudioEngine on the native host, end to end to the speaker HARDWARE VERIFIED §3.6
UI views VERIFIED offscreen; the controls they drive HARDWARE VERIFIED §3.7
Soak and restart HARDWARE VERIFIED, 30 min + 25 restarts §3.8
Native WASAPI on a USB interface (AI-04: exclusive 48 kHz at 3 ms, shared, monitoring) HARDWARE VERIFIED; its 44.1 kHz input clock a recorded known failure §3.10
Windows virtual audio driver, several cables HARDWARE VERIFIED in a Hyper-V test VM (two isolated cables, audio between applications at the level sent, add/rename/disable/enable/uninstall from the UI, a cable disappearing under the engine and reopened, clean uninstall); real-desktop use NOT VERIFIED; production signing NOT AVAILABLE §3.9
Threads, devices that come and go, whole-system loopback, built-in effects in the UI, network (TCP, adaptive jitter, Opus), PortAudio bus routing, plugins from REST/CLI, instruments VERIFIED; the audio paths HARDWARE VERIFIED (AI-04, WSL2 PulseAudio, the VM) §3.11

The per-item matrix is IMPLEMENTATION_STATUS.md.

2. What was built

The milestones on windows-native (pushed; pull request #1):

Milestone Commit What
M0 9abc9da AGENTS.md rewritten as the project contract: real-time rules, native build rules, driver rules, licensing, evidence levels
M1 3eac2dd The reality matrix; “measured latency” that was driver-reported renamed and its figure withdrawn; a whole-system-loopback claim removed; requests dropped, packaging tools moved to a group; shared test signals
M2 9be19ee Toolchain (EWDK, CMake and Ninja from PyPI), the C ABI, wait-free SPSC ring, plan exchange by atomic swap and hazard pointer, allocation counter, offline process
M3 d97cffb Mixer strips, 8-band RBJ EQ, compressor, delay, limiter, drift resampler, sample conversion — ported against the Python reference
M4 b92cb9c Native WASAPI: enumeration with endpoint IDs, notifications, event-driven shared/exclusive, raw mode, loopback, process loopback, MMCSS, master and satellite clocks
M5 da42996, 2275220 Native ASIO host as a separate GPLv3 DLL on the external-backend ABI; verified against FlexASIO
M6 2333b6d Round-trip measurement: exponential sweep teed inside the engine, GCC-PHAT, confidence threshold
M7 5a06e18 Native VST3 host on SDK 3.8.1; subprocess scanner; SEH fault isolation; deterministic test plugins (gain+latency, crash on load, crash on the audio thread); pedalboard (GPLv3, unreachable, never proven) removed
M8 f0b334c AudioEngine on NativeHost; plugins per device side; presets v2 with plugin chains and endpoint IDs
M9 532d041 Kernel-mode loopback-cable driver from Microsoft’s SimpleAudioSample; build and VM-kit scripts; install/uninstall scripts; hardware tests that skip without it
M10 b19bf1d UI: diagnostics, plugin browser, insert chains, parameter editor, sample rate; per-channel and per-side meters; strip balance and native channel swap made real; host bypass
M11 6727a7b Soak test, Corresponding Source packaging, licence texts in the binary, legal documents, architecture/real-time/testing docs, README, this report
M12 1ecdb5c…11586a6 The AI-04 interface on native WASAPI and through FlexASIO; the drift resampler rebuilt (calibration, dead band, proportional-integral); device periods cut into equal engine blocks; the virtual driver verified in a Hyper-V VM, with the VM built and driven by committed scripts, and the driver defects that found; the Store package made MIT-only; docs published by GitHub Actions
M13 5470caf Measured round trip through the AI-04 and a cable, WASAPI and ASIO (measure_asio); ASIO4ALL on the AI-04
M14 42e1eb8 A threaded, thread-safe control plane: one lock per engine object; the UI’s engine calls on a worker and a poller; REST handlers in the threadpool
M15 c353949 A device monitor that reopens what changed; every output’s loopback a routable source; the built-in effects in the Inserts dialog
M16 095207a TCP send, an adaptive jitter buffer, Opus through libopus
M17 eeaac3a Buses forward on the PortAudio host: device → bus → device on Linux and macOS
M18 3313881 Plugins from the REST API and CLI; VST3 instruments played by MIDI
M19 this series Several virtual cables, managed from the UI; devices disappearing, proven on the cables; ASIO4ALL in the VM
M20 this series The acoustic round trip; documentation

3. Evidence

Final run on the development machine, 2026-10-01: uv run pytest -m "not hardware" — 916 passed, 3 skipped (two Linux-only tests; makeappx validation, which needs the Windows SDK on PATH). uv run ruff check . clean. The hardware tests were run milestone by milestone on this machine (the virtual-driver tests skip here by rule: the driver is installed only in the VM) and in the VM (§3.9). The suite before the migration: 660 passed.

3.1 Native engine (offline, CI)

148 tests in tests/native/, all passing, run in CI on Windows after the SDK fetch and build. Highlights: ring integrity under a two-thread stress test with sample-exact checking; 300 plan swaps under a concurrently running audio thread with every block finite and bounded; EQ equal to the Python Biquad within 2e-6 for all five filter types; a NaN at a source never reaching an output; rt_allocations == 0 over 500 blocks with rings, buses, ramps and swaps.

Offline cost (benchmarks/results/m3_offline_i7-1165G7.json, MMCSS “Pro Audio”): a guitar chain — EQ, compressor, bus, delay, limiter — at 48 kHz / 256 frames averages about 24 µs a block, p99 32 µs (0.45 % / 0.6 % of the 5.33 ms period).

3.2 WASAPI (tests/hardware/test_wasapi.py)

3.3 ASIO (tests/hardware/test_asio.py)

Against FlexASIO 1.10b (installer SHA-256 FE496BCC…031209; unsigned): 80 of 80 buffer switches at 48 kHz / 882 frames, mean 35 µs, 0 xruns, a 1 kHz output captured back at 1 kHz. Through FlexASIO to the Audio Array AI-04 in WASAPI exclusive at 144 frames: 499 buffer switches in 1.5 s, none missed, max 18.2 µs, 0 xruns, input 1 carrying the guitar’s 50.10 Hz mains hum at −19.5 dBFS; in FlexASIO’s default shared mode, the output came back through process loopback at exactly the level sent. No manufacturer’s ASIO driver has been tested: the AI-04’s maker publishes none.

Against ASIO4ALL 2.22 (WDM-KS) on the AI-04 with its output cabled to its input: the output heard at +0.00 dB from native WASAPI exclusive over the same cable; a measured round trip of 15.58 / 18.27 / 23.58 / 34.27 ms at 64 / 128 / 256 / 512 frames, identical to the frame over three runs. In the driver VM on ToneSphere’s cables: 48 kHz from the buffer switch after a 0.4 s start, and the output through a cable at +0.00 dB; its round trip there -- (confidence 2.1 in one pass; 29.94 ms at 512 frames, confidence 79, in the next). Chasing a hang there found one of ToneSphere’s own: stopping a WASAPI stream whose device event never stopped firing waited for ever, because the stop event was the second handle waited on; fixed (docs/ASIO.md). ASIO4ALL was uninstalled from this machine afterwards.

3.4 VST3 (tests/native/test_vst3.py, tests/hardware/test_vst3_third_party.py)

3.5 Round trip (tests/hardware/test_roundtrip.py)

The digital path (output → its own loopback) measures 61.35–65.35 ms over six starts at 48 kHz / 480 shared, repeatable within one device period and fixed within a run (confidence 26.9 at 62.35 ms; an independent cross-correlation in test_wasapi.py agrees to the frame). The engine records a loopback measurement as the digital path and never reports it as the round trip.

Path Measured round trip
AI-04 output → 6.35 mm cable → input, WASAPI exclusive, 3 ms period 17.92 ms (860 frames; 15.94 ms in 2 of 10 starts, a USB packet group earlier)
the same, WASAPI shared, 480 frames 76.9 ms
the same, ASIO4ALL, 64 / 128 / 256 / 512 frames 15.58 / 18.27 / 23.58 / 34.27 ms
the laptop’s speakers → its microphone array (acoustic, raw capture) 74.60–75.90 ms in 9 of 10 runs, confidence 4.3–5.1 against a threshold of 4.0; the tenth -- at 3.8

The cable path captured the −18 dBFS sweep at about −8 dBFS: no clipping. For the acoustic figure the Realtek speakers, found muted, were held unmuted at 60 % for the sweeps only and put back exactly (tests/hardware/endpoint_volume.py); it is a real figure, only just above the threshold.

3.6 End to end (tests/hardware/test_engine_native_host.py)

AudioEngine → NativeHost → VST3 test plugin at ×0.5 → WASAPI → Windows, heard back by process loopback in a separate engine: RMS 0.07071 against 0.07071 expected. Survives a restart with its fader and plugin; a preset restores the chain with the plugin’s state. Balance 0.5 leaves the near side at unity and the far side at cos(π/4) within 2 %; a bypassed plugin leaves the signal untouched and stops counting its 64 samples of latency; per-channel meters agree with the heard tone within 0.3 dB.

3.7 UI

tests/test_ui_views.py, tests/test_ui.py, tests/test_i18n.py (offscreen): a duplex device’s input and output strips are metered separately; a side with no reading greys out rather than showing silence; the balance knob appears only on stereo strips; nothing unmeasured is shown as a number; a round trip is shown only for the configuration it was measured at; the plugin browser lists crashed and wrong-architecture modules with their reasons and will not insert them or an instrument; every string is in both catalogues. On hardware, the inserts dialog’s slider reaches the plugin and what it then shows is the plugin’s own read-back.

3.8 Soak and restarts (benchmarks/soak.py)

The UI’s own path — AudioEngine, native host, WASAPI shared at 48 kHz / 480 frames on the Realtek headphone output — with a bus fed pink noise in real time by a Python thread and Surge XT Effects then the test plugin (at gain 0, so nothing is audible) on the output, for 30 minutes, then 25 stop/start cycles (benchmarks/results/soak_30min_i7-1165G7.json):

   
Callbacks 180,024, engine running at every sample
Xruns 0
Audio-thread heap allocations 0
Callback mean / p99 / worst 65.9 µs / ≤ 128 µs / 1.10 ms (11 % of the 10 ms period)
Bus ring underruns after start 0
Output meter silent throughout; bus meter carried the noise throughout
Private bytes, first → last sample 280.3 → 280.5 MB
Restarts 25 of 25 running; 55–63 ms each; 0 xruns after each; handles 352 → 352

Not covered: plugins other than these two. Device removal is covered since M19, on the virtual cables in the VM (§3.9).

3.9 Virtual audio driver

Built with the EWDK, test-signed with the WDK test certificate, InfVerif /w clean, and tested in a Hyper-V VM (Windows 11 Enterprise LTSC Evaluation 10.0.26100, test-signing on in the VM disk’s own boot store, Secure Boot off), built by scripts/vm/new_driver_vm.ps1 and driven by scripts/vm/run_driver_tests.ps1. Each cable is its own device instance, with its own buffer. tests/hardware/test_virtual_driver.py, 14 of 14 (2026-10-01):

   
Fresh install “ToneSphere Cable 1” and “ToneSphere Cable 2”, ROOT\MEDIA\0000 and 0001, four endpoints at 48 kHz stereo
PortAudio process → each cable → PortAudio process 1000.00 Hz, +0.000 dB on both
Isolation a tone into cable 1: cable 2’s peak exactly 0
PortAudio → cable → ToneSphere; ToneSphere → Test Gain ×0.5 → cable → PortAudio −0.009 dB; exactly half
ffmpeg (DirectShow) recording what ToneSphere plays into a cable 1000.00 Hz, +0.000 dB
Idle; a new capture after the player stopped exact silence; nothing replayed
Add, rename, uninstall a cable through the app “Chat” added, carrying audio, renamed “Game” (its endpoints with it), uninstalled; cable 1 unaffected
The Virtual Cables dialog’s own buttons a real disable and enable of cable 2
A cable disabled under a running engine reported gone, the engine running on without it (cable 1 carrying 440 Hz), reopened on return with no user action, 1 kHz back at −0.009 dB
A cable in use elsewhere disabled under a PortAudio recorder; refused cleanly while another native-engine program held it; changed once it let go
Uninstall every cable, the driver-store package and the certificate trust gone; nothing left

The passes found: the driver had never written into the cable; a new capture replayed the end of an earlier one; the tests could not have run (floats passed to Popen); the app looked for names Windows does not show; the sample’s one-device guard failed every cable after the first (STATUS_DEVICE_BUSY); a name set before the driver was installed was replaced by the INF’s; disabling a cable ToneSphere streamed through left it “pending a restart” — Windows’ audio engine vetoes taking a device from a native-engine stream — and a vetoed persistent disable still recorded one; a cable’s two endpoints settle hundreds of milliseconds apart. Not verified: a real desktop with Discord or OBS, sleep/resume, many clients. Custom pin names are not implemented; the cable’s own name is in both endpoint names.

3.10 A USB interface (tests/hardware/test_interface.py)

The Audio Array AI-04 (USB, Windows’ class driver), a guitar on input 1:

Mode Period Callback max Worst load Dropouts
Exclusive 48 kHz 144 frames, 3.0 ms reported 209 µs 14.0 % of a 1.5 ms block none
Exclusive 44.1 kHz 132 frames 153 µs 10.2 % 0–65 input frames at start (known failure)
Shared 48 kHz 480 frames 108 µs 1.1 % none

The input carried the guitar pickup’s 50 Hz mains hum at −19.9 dBFS; input → gain bus → output gave the output exactly input × 0.01, sample for sample. At 44.1 kHz this device’s input runs 0.2–0.3 % slow against its own output, and wanders ±0.15 %. Nothing listened to the output jack.

3.11 Closing the gaps (M14–M18)

3.12 The product a user downloads (M21–M24)

v0.2.0, installed by the owner on a clean machine, opened no window, showed meters but no sound on Monitor Input, called Guitar Rig “crashed” and added no cables. Every one of those paths had tests; none of the tests drove what a user downloads. So:

4. Real defects found and fixed

Found by signal tests during the migration, each now covered by one:

5. What remains, and what blocks it

Item Blocker Owner
Production-signed driver EV code-signing certificate and a Partner Center hardware account (attestation signing) owner, cost and identity verification
The driver on a real desktop, with Discord or OBS the driver is test-signed: only a test-signed machine or VM owner / after signing
ASIO with a manufacturer’s ASIO driver an interface whose maker ships one (the AI-04 has none) owner
Commercial VST3 plugins beyond Guitar Rig 7.0.1; Neural Amp Modeler licences; NAM’s installer is unsigned owner
Code-signed downloads (no SmartScreen or Gatekeeper warning) a code-signing certificate; an Apple Developer ID owner
Discord as a capture source, tested it played nothing during the run and exposes no controls to automation; per-app capture itself is HARDWARE VERIFIED project
MIDI from a hardware keyboard a MIDI device owner
macOS day to day, and bus routing with real macOS devices a Mac whoever owns one
An intermittent exclusive-mode round trip through a virtual cable in the VM: the capture holds the whole sweep, the correlation sometimes fails on it cause not established; docs/VIRTUAL_AUDIO_DRIVER.md project
Plugin isolation (a plugin can still take the process down) a plugin host process project
Legal review of T&C, ToS, Privacy a lawyer owner
Store submission Partner Center identity; the package is MIT-only by decision owner
The AI-04 at 44.1 kHz its input clock; a larger satellite cushion would trade latency for the start-up loss project
Custom pin names for the driver a KSPROPERTY_PIN_NAME handler in the driver project
Driver build in CI unverified whether the hosted runners’ WDK builds it project

6. Licensing and distribution state

7. How to reproduce

uv sync --all-groups
uv run python scripts/fetch_sdks.py
uv run python scripts/build_native.py
uv run pytest -m "not hardware"
uv run pytest -m hardware                 # FlexASIO and Surge XT for their tests
uv run python benchmarks/bench_engine.py
uv run python benchmarks/soak.py --minutes 30
uv run python main.py test