Implementation status

This is the record of what exists. The README summarises it; when they disagree, this file is right and the README is a bug. Levels are defined in AGENTS.md:

Evidence is a test name, a file:line, or a recorded run. “Target” is the architecture in AGENTS.md this subsystem is being migrated to.

Baseline (2026-09-29, before the native migration)

Machine: Windows 11 Pro 25H2 (build 26200), Intel i7-1165G7, Realtek ALC257 (speakers, headphone out, mic), Intel SST mic array, Bluetooth headset. No ASIO drivers installed, no VST3 plugins installed, no C/C++ toolchain.

Reality matrix

Subsystem Existing implementation Target Level Evidence / notes
Audio I/O (all platforms) One sounddevice/PortAudio stream per device, Python callbacks (engine/host.py:895/916/972) Windows: native backend, no Python on the audio thread. Linux/macOS: keep, labelled not real-time safe HARDWARE VERIFIED (output only) TestRealHardware::test_output_stream_runs_without_xruns on Realtek WASAPI. Callback is Python under the GIL; see Real-time safety below.
WASAPI Legacy: via PortAudio (host.py:607-625). Native: native/windows_audio/wasapi.cpp — event-driven shared (IAudioClient3 low-latency where possible) and exclusive with format negotiation, raw mode, MMCSS, master + satellite clocks with drift correction Native, driving the engine Native: HARDWARE VERIFIED (render, loopback, process loopback, exclusive, multi-clock) tests/hardware/test_wasapi.py; bit-exact render→process loopback; details and limits in docs/WINDOWS_AUDIO.md. AudioEngine runs on it on Windows (M8)
Device enumeration Legacy: PortAudio, key host_api::name. Native: MMDevice enumeration with endpoint IDs, default roles, formats, periods, raw support; IMMNotificationClient events Native Native enumeration HARDWARE VERIFIED; notifications IMPLEMENTED; behaviour on removal while streaming NOT VERIFIED test_endpoints_have_stable_ids_names_and_formats, test_device_notifications_can_be_watched
ASIO Legacy: enum/preference only. Native: native/asio/asio_host.cpp (GPLv3 DLL) — discovery, STA driver thread, negotiation, buffer switch driving the engine, asioMessage Native ASIO host Host HARDWARE VERIFIED against FlexASIO 1.10b (a software ASIO driver), including FlexASIO driving the Audio Array AI-04 USB interface in WASAPI exclusive, and ASIO4ALL 2.22 (third-party, kernel streaming) on the AI-04 with a measured round trip of 15.58 ms at 64 frames; boundary VERIFIED; an interface manufacturer’s own ASIO driver: NOT AVAILABLE (the AI-04 has none — it is driver-free, on Windows’ class driver) tests/native/test_asio.py; tests/hardware/test_asio.py against FlexASIO: query, 80/80 buffer switches at 48 kHz/882, 0 xruns, output captured at 1 kHz (2026-09-29); through FlexASIO to the AI-04 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 hum at −19.5 dBFS (2026-09-30). docs/ASIO.md
VST3 Legacy pedalboard chain removed. Native: native/vst3/vst3_host.cpp on the VST3 SDK 3.8.1 — out-of-process scan, load, processing as an engine insert, parameters, state, editor, SEH fault isolation Native VST3 host VERIFIED with ToneSphere’s deterministic test plugin; HARDWARE VERIFIED with Surge XT 1.3.4; commercial: HARDWARE VERIFIED with Native Instruments Guitar Rig 7.0.1 (no other commercial plugin tested) tests/native/test_vst3.py (bit-exact gain+delay, measured latency = reported, parameters, state, crash isolation on load and on the audio thread); tests/hardware/test_vst3_third_party.py (Surge XT delay echo at 250.06 ms, bypass, state, editor). Reachable from the UI (browser, insert chains, parameters, editor, host bypass — tests/hardware/test_engine_native_host.py::test_the_inserts_dialog_drives_a_real_plugin), and since M18 from the REST API and CLI (tests/native/test_plugins_remote.py). Instruments played by MIDI: HARDWARE VERIFIED with Surge XT and Dexed (tests/hardware/test_instruments.py, to pitch within 1 %, from the engine, REST, the on-screen keyboard and through the AI-04’s cable); MIDI from a hardware port UNVERIFIED (no device). Guitar Rig 7.0.1 (tests/hardware/test_guitar_rig.py, 2026-10-02): scans OK from source and frozen builds, a 220 Hz tone at −20 dBFS comes out finite and audible, its Rack Master Volume at what it displays as −12.0 dB moves the output −12.04 dB, 60 s (11,250 blocks) with no fault and 0 engine allocations, state round-trips, editor opens and closes; a parameter changed while nothing processes the plugin is in its saved state (test_a_change_made_while_nothing_processes_is_in_the_saved_state). docs/VST3.md
Mixer Python/NumPy in the callback (host.py:986-1204, dsp.py): -3 dB constant-power pan, per-block linear gain ramps, per-channel trim/mute/solo/polarity/swap, output limiter Native mixer on atomic control targets VERIFIED (offline) test_mixer_integration.py, TestBusMixing drive the real _mix_into with synthetic signals. Known defects below.
Routing graph Immutable RoutingGraph (graph.py:111), DFS feedback check on the control thread (graph.py:247), per-route rings (host.py:433-491) Compiled execution plan, native, atomic swap VERIFIED (device→device, and device→bus→device on both hosts) TestGraph, TestBusMixing; bus forwarding in the PortAudio host since M17 (below).
Buses In-process summing points Native buses with real forwarding Native: VERIFIED (offline, exact samples); PortAudio host: VERIFIED offline, and on Linux through a real PulseAudio server (M17) Native: test_device_through_a_bus_to_another_device, and a device-less bus→network send sample for sample (test_a_bus_reaches_a_network_send_sample_for_sample_with_no_device). PortAudio host: a bus an output device consumes is rendered by that output’s callback when its ring runs short (every route in, plus a feed for Python writers, summed with route gains); a bus nothing clocks forwards as its producers arrive. test_mixer_integration.py: device→bus→device at the tone’s level, a chain of buses with each route’s gain, a device and a Python producer summed in one bus, an unclocked chain. test_linux_virtual_device.py::TestRealLinuxBusRouting: pacat plays 1 kHz into null sink A, ToneSphere routes PortAudio’s pulse device through a bus back to pulse, parec records null sink B at 0.1768 rms, exactly the level expected, 5 of 5 runs in WSL2 (Ubuntu 24.04, kernel 6.6.87.2, PulseAudio 16.1), and in the Linux CI job. macOS: the same code; no macOS test of the route, since the CI runner’s only audio device is ToneSphere’s own loopback plug-in
Ring buffer NumPy SPSC with GIL-atomic indices (ringbuffer.py); overrun drops the newest frames, underrun zero-fills Native SPSC, acquire/release atomics IMPLEMENTED TestRingBuffer incl. a two-thread stress test. Several threads write into buses (network playout, process capture); since M14 write_bus runs under the host’s lock, so each ring still has one producer at a time.
Metering Native: peak/RMS/clip per node and per channel (first 8), measured on the audio thread; PortAudio host: per channel in the callback. AudioEngine.get_meters reports each side of a device separately Native, published per block VERIFIED; HARDWARE VERIFIED per channel test_each_channel_is_metered_on_its_own; tests/hardware/test_engine_native_host.py::test_bypass_balance_and_per_channel_meters_are_what_is_heard (meter within 0.3 dB of the tone heard per channel); tests/test_ui_views.py (a duplex device’s input and output strips no longer overwrite each other)
Latency reporting Legacy host: PortAudio-reported stream latency + plugin latency (host.py:1414-1446), labelled reported_latency_ms; measured_round_trip_ms stays None there. Native: tonesphere/native/roundtrip.py sends an exponential sweep, tees it in the engine, and times the capture by GCC-PHAT, refusing below a confidence threshold Nominal / reported / plugin / measured round trip, separately Measurement method VERIFIED (synthetic) and HARDWARE VERIFIED on the digital path; through the AI-04 and a cable: HARDWARE VERIFIED — 17.92 ms exclusive at a 3 ms period, 76.9 ms shared, 15.58 ms through ASIO4ALL at 64 frames; acoustic round trip on this laptop: HARDWARE VERIFIED, marginally — 74.60–75.90 ms in 9 of 10 runs, confidence 4.3–5.1 against a threshold of 4.0 tests/unit/test_roundtrip_estimator.py; tests/hardware/test_roundtrip.py: render→loopback 62.35 ms (2993 frames), confidence 26.9, repeatable to the frame — identical to the independent cross-correlation in test_wasapi.py. Speakers→microphone on this laptop (test_the_laptops_own_speaker_to_its_own_microphone, 2026-09-30): the first attempt found no path, because the speakers were muted and the Realtek microphone heard silence. With the Realtek speakers held unmuted at 60 % for the sweeps only (tests/hardware/endpoint_volume.py, IAudioEndpointVolume; the volume and mute read back exactly as they were), the Intel SST array in raw mode timed 74.60–75.90 ms (3581–3643 frames, a spread of 62) in 9 of 10 runs, confidence 4.3–5.1, captured peak about −19 dBFS; the tenth read -- at confidence 3.8. It is a real figure, only just above the threshold: a laptop’s speaker and microphone are small, close and heavily processed in the codec. AI-04 output→AI-04 input through a 6.35 mm cable (2026-09-30): exclusive at its 3 ms period 17.92 ms (860 frames; 15.94 ms in 2 of 10 starts, a start-phase step of 96 frames), shared 76.9 ms, both captured at −8 dBFS peak for −18 sent (test_an_interface_cable_round_trip, ..._in_shared_mode); from an ASIO buffer switch, measure_asio: ASIO4ALL 15.58 / 18.27 / 23.58 / 34.27 ms at 64 / 128 / 256 / 512 frames, identical to the frame over three runs (test_an_asio_round_trip_through_the_interface_cable). In the UI: Diagnostics → Measure (engine stopped; any output to any input, or the output’s own loopback). A loopback result is recorded as the digital path and never reported as the round trip; a result is shown as the round trip only at the rate and block it was taken at (tests/test_ui_views.py, tests/hardware/test_roundtrip.py::test_the_engine_keeps_a_loopback_measurement_apart_from_the_round_trip)
Performance statistics stream.cpu_load from PortAudio; xruns = callbacks with any status flag Callback min/mean/max/percentile from QPC, load = worst ÷ period IMPLEMENTED (PortAudio’s figures only) No callback timing exists.
Process loopback (Windows) ctypes COM ActivateAudioInterfaceAsync (process_capture.py, wasapi_com.py), a Python capture thread writing into a bus Native WASAPI capture into a native SPSC port HARDWARE VERIFIED TestSelfCapture::test_captures_the_tone_this_process_plays on this machine, 2026-09-29.
Devices that come and go engine/device_monitor.py: Windows’ endpoint notifications (IMMNotificationClient, through the native queue and a per-process hub), debounced 300 ms, then AudioEngine.handle_device_change: re-enumerate, and if a device the routing uses left, arrived or has a failed stream, restart the engine with the devices present — routes to a missing device are kept and come back with it, under the same id. The UI redraws and says what changed. The native backend starts and stops as a whole, so a reopen is a short gap on every device Automatic reopen IMPLEMENTED; the decision VERIFIED without hardware; audio stopping and resuming on a real device: see the driver VM (M19) tests/test_device_monitor.py (a burst of notifications is one reconcile; default-device changes reconcile nothing; the monitor stops while the engine holds its lock; only a used device’s change reopens, and a returning device keeps its id and routes)
Whole-system loopback Legacy: none (previously claimed). Native: loopback stream kind; since M15 every WASAPI output is also a routable source, “ (loopback)" (`NodeId('loopback', key)`, ids from 30000), with a silent shared-mode render on the same output as its clock when nothing else in the plan has one; feeding a loopback back into its own output, directly or through buses, is refused Native WASAPI loopback HARDWARE VERIFIED from the routing tests/hardware/test_loopback_source.py: two other processes play 1 kHz at 0.1 and 440 Hz at 0.05 into the AI-04; ToneSphere’s loopback of it, through a bus, reads 0.1000 and 0.0500 — the system mix at the levels played. tests/test_loopback_source.py (feedback refused in every order of building the path), tests/native/test_loopback_plan.py (the clock render). capture_status() reports it implemented where the native engine is loaded, and only there
Audio-session detection PowerShell Add-Type C# block querying the default render endpoint’s sessions (app_capture.py:51-211) Native or ctypes IAudioSessionManager2 IMPLEMENTED Volume and mute fields are hard-coded (app_capture.py:207-208).
Windows virtual device driver/windows_virtual_audio/ — SimpleAudioSample (MS-PL) with one render→capture cable per device instance of ROOT\ToneSphereVirtualAudio; endpoints “Speakers ()" and "Microphone Array ()", 48 kHz 32-bit stereo; two cables on a fresh install, up to eight, managed from the Virtual Cables dialog (`engine/virtual_cables.py`, `ui/cables_view.py`) through Windows' own administrator prompt PortCls/WaveRT loopback-cable driver, OS-visible endpoints HARDWARE VERIFIED in a Hyper-V test VM (install, two isolated cables each carrying audio between applications at the level sent, add/rename/disable/enable/uninstall from the app and its dialog, a cable disappearing under a running engine and reopened on return, ffmpeg recording one, clean uninstall); on a real desktop with a real communications app NOT VERIFIED; custom pin names NOT IMPLEMENTED; production signing NOT AVAILABLE tests/hardware/test_virtual_driver.py, 14 of 14 in the VM (Windows 11 Enterprise LTSC 10.0.26100, test-signing), 2026-10-01: each cable 1 kHz at +0.000 dB between PortAudio processes; a tone into cable 1 read as an exact-zero peak on cable 2; ToneSphere→Test Gain ×0.5→cable→PortAudio exactly half; ffmpeg (DirectShow) +0.000 dB; a cable disabled under the engine reported gone, the engine running on without it, and reopened on return with the tone back at −0.009 dB and no user action; a cable held by another native-engine program refused cleanly (Windows’ audio engine vetoes the removal) instead of being left pending a restart. Built and run by scripts/vm/new_driver_vm.ps1 / run_driver_tests.ps1; logs in driver/windows_virtual_audio/test-results/. docs/VIRTUAL_AUDIO_DRIVER.md
Linux virtual sink pactl null sink + ALSA pulse PCM Kept as is VERIFIED (CI) TestRealLinuxSink in the Linux CI job.
macOS virtual device AudioServerPlugIn in native/coreaudio-plugin/ Kept as is VERIFIED (CI only) build-macos-plugin job round-trips a 1 kHz sine. Day-to-day use UNVERIFIED.
Built-in DSP Python: RBJ biquad EQ with a per-sample Python loop (effects.py:174), soft-knee compressor, delay, limiter without lookahead Native ports of the same formulas VERIFIED (offline); in the UI VERIFIED, and HARDWARE VERIFIED on the AI-04 (M15) test_effects.py, test_dsp.py signal tests. Since M15 the native built-ins sit in one ordered chain with VST3 plugins, on any device side or bus (AudioEngine.add_builtin / list_inserts / set_builtin_value / move_insert; engine/builtins.py describes each parameter’s range, scale and unit), offered in the Inserts dialog (“Add built-in”), saved in presets, and kept across an engine rebuild (values are pushed after every plan swap — before, a block-size change reset them). tests/native/test_builtin_effects_ui.py, driving the dialog’s own widgets: a high-pass set to 1 kHz takes 100 Hz down 40.0 dB and 5 kHz 0.01 dB, and still does after a rebuild; the default compressor reduces a −6.1 dBFS tone by 10.3 dB, its own readout 10.31 dB. tests/hardware/test_builtin_effects_hardware.py: a 100 ms delay on the AI-04’s output heard through the cable 100.000 ms after the dry sweep. The PortAudio host’s Python effects stay reachable only through get_inserts
Network audio UDP transport with an adaptive (or fixed) jitter buffer and a paced send worker; TCP send and receive; Opus over UDP through libopus. Threads touch rings, never the callback Kept; feeds native external-port rings VERIFIED (localhost; a jitter-adding relay for the adaptive buffer) test_network_send_wiring.py (1 kHz sine across two engines over localhost UDP and over TCP, sample for sample; the adaptive buffer through a real 0-30 ms jitter relay), test_jitter_buffer.py (adaptive depth against scripted arrival schedules), test_opus.py (codec delay 312 frames, 40.4 dB SNR, PLC, Opus between two engines). Not tested between two machines
UI PySide6 mixer strips (per-side, per-channel meters; stereo balance; insert chains), patchbay, status bar; device configuration (backend, exclusive, buffer, sample rate); plugin browser (scan status and reason per module, custom folders); insert chain with parameters, bypass and the plugin’s editor; Diagnostics (callback min/mean/p99/max, worst and mean load, xruns, audio-thread allocations, rings, nominal / driver-reported / plugin / measured latency, round-trip measurement, virtual-device presence). No engine or plugin call runs on the GUI thread (M14): actions on an ordered worker, meters and statistics from a poller, the window drawing from snapshots Control plane only IMPLEMENTED; views VERIFIED offscreen; threading VERIFIED (tests/test_threading.py) test_ui.py, test_ui_views.py, test_i18n.py (every string in en.json and hi.json). Built-in effects in the insert chains (M15); a status-bar notice when devices come and go. No network UI; strip channel swap is API/CLI only
Persistence SQLite settings (utils/config.py); YAML presets and an autosaved session keyed on endpoint IDs with names as a fallback, holding routes (with the input channel), strips, buses, engine settings and every insert with its plugin’s own state Stable endpoint IDs, plugin chains and state VERIFIED (session); HARDWARE VERIFIED (the built app reopened with its route and Guitar Rig 7 at its saved setting, measured at the output) test_config_store.py, test_presets.py, test_end_user_flows.py::TestTheSessionComesBack; tests/hardware/test_first_run.py. Saved 2 s after a change and on close; logs and crash reports in the same per-user folder.
Diagnostics (main.py test) Plays a 440 Hz tone to the default output, checks callback count/errors/xruns, prints reported latency and PortAudio CPU load Real signal verification and measured round trip IMPLEMENTED It never captures its own output, and writes the tone ~3× faster than it is consumed, so the tone it plays is choppy by construction.
First run (end user, Windows) No arguments open the window; the engine starts itself; Monitor Input picks the interface and input 1 to both ears and plays on Start; ASIO listed as unavailable with the reason when no driver is installed; logs and crash reports in the per-user folder — HARDWARE VERIFIED (built app, AI-04, guitar on input 1, 2026-10-02) Driven through the window by UI Automation and measured on the output’s loopback from another process: the guitar 3.02 dB below the input in both ears (mono law 3.01 dB), coherence 0.976; with Guitar Rig 7 added from the browser and its volume set from the parameter list to −12.0 dB, 12.20 dB lower; after close and reopen, route and plugin back at −15.05 dB. Repeatable as tests/hardware/test_first_run.py. The crash report is proven by a unit test only (no reachable fault in the built app). docs/FIRST_RUN_VERIFICATION.md
Packaging PyInstaller one-folder, packed per system: an Inno Setup per-user installer and a portable zip (Windows), an AppImage (Linux), an ad-hoc-signed .app in a .dmg (macOS); MSIX manifest and pack script; a version computed and a release published by CI on every green push to main Plus native DLLs; driver has its own installer VERIFIED (CI) .github/scripts/smoke_test_release.py launches each artifact the way a user does — the Windows installer installed silently and the installed app started with no arguments, the AppImage, the app from the mounted .dmg — and checks the window, the backend and (Windows) the native engine; the frozen scanner reads the test plugin. Not code-signed: SmartScreen and Gatekeeper warn. MSIX never signed, installed or submitted.

Native engine (native/, tonesphere/native/)

Capability Level Evidence
Toolchain: EWDK (MSVC 14.50, SDK 10.0.28000) + CMake/Ninja from PyPI; /W4 /WX clean IMPLEMENTED scripts/build_native.py; docs/BUILDING_WINDOWS.md
C ABI (native/include/tonesphere_native.h), ctypes bindings, ABI version check IMPLEMENTED tests/native/* load and drive it
SPSC frame ring (wait-free with one producer and one consumer) VERIFIED tests/native/test_ring.py — wraparound, full, empty, 8-channel framing, two real threads moving 200 000 frames of sine+noise sample-exact
Plan validation, topological ordering, cycle rejection VERIFIED TestPlanValidation; a refused plan leaves the running plan intact
Plan swap by atomic exchange + hazard pointer, no audio-thread blocking VERIFIED test_plans_swap_while_another_thread_processes — 300 swaps under a concurrently running audio thread, every block finite and bounded
Mixing: routes, gain ramps, mute, invert, master, buses with forwarding, fan-out, channel mapping, constant-power pan, continuous stereo balance VERIFIED (offline) TestAudioMovesThroughTheGraph, TestGainMuteInvert, TestChannelMapping — known signals in, exact samples out
Ring ports (Python producer/consumer ↔ audio thread) with overrun/underrun counts and events VERIFIED TestRingPorts
Non-finite guard at sinks VERIFIED test_a_nan_never_reaches_an_output
Meters (peak since reset, block RMS, clip latch) VERIFIED test_meters_report_what_was_processed, test_clipping_latches_until_reset
Callback timing (min/mean/max, histogram p99, load = worst ÷ period) IMPLEMENTED test_statistics_measure_real_callback_time; None until a block runs
Zero heap allocations on the audio thread VERIFIED test_the_audio_thread_allocates_nothing — 500 blocks with rings, buses, ramps, swaps: rt_allocations == 0 (this DLL’s allocations only; plugin modules are not counted)
Channel strip: per-channel trim and polarity, fader, mute — persisted across plan swaps VERIFIED (offline) tests/native/test_dsp.py::TestChannelStrip, incl. test_strip_settings_survive_a_plan_change
Parametric EQ (peaking, shelves, HP, LP — 8 bands) VERIFIED (offline) test_matches_the_python_reference_sample_for_sample: all five filter types equal the proven Python Biquad within 2e-6 on noise; filter memory survives a plan swap bit-exactly; NaN input cannot poison it
Compressor (soft knee, per-sample detector) VERIFIED (offline) TestCompressor: -6 dBFS into -18 dB/4:1 settles at -15 dBFS with -9 dB reported; attack lags the transient
Limiter (sample-accurate, instant attack, no lookahead) and per-sink safety limiter VERIFIED (offline) TestLimiter: a +6 dBFS tone never exceeds the threshold by one sample; below threshold is bit-exact. Fixes the legacy limiter’s unreduced first block
Delay with feedback VERIFIED (offline) TestDelay: echoes exactly at k·d with amplitude mix·feedback^(k-1), nothing between
RoutingGraph → native plan compiler (device in/out split, solo, stable ids) VERIFIED (offline) tests/native/test_plan_compiler.py; the duplex in→out monitoring path is no longer refused as feedback (graph.would_feedback)
Drift resampler across device clocks VERIFIED (offline) + HARDWARE VERIFIED tests/native/test_boundary.py: ±100, ±500 and ±3000 ppm absorbed with bounded fill, no underrun, no discontinuity (a click at ratio < 1 was found and fixed here); on hardware in the three-clock test. Rebuilt 2026-09-30 after the AI-04 exposed two faults: the loop steered towards a fill the packet cadence never averages to, warping every stream’s first seconds (the AI-04’s digital round trip failed 4 times in 10), and its ±0.1 % limit could not follow the AI-04’s 44.1 kHz input, 0.2–0.3 % slow (355 frames lost in 20 s). Now: a calibration phase learns the real setpoint, a dead band keeps same-clock streams at exactly 1, and a proportional-integral loop within ±0.5 % follows real drift with the fill centred — round trip 10 of 10 at identical confidence; 44.1 kHz on the AI-04 0–65 frames lost at start, then none, the ratio following that input’s ±0.15 % wander (test_interface.py, recorded xfail)
Sample-format conversion at the device boundary VERIFIED TestConversion: int16/24/32, float32/64 round trips within half a step; over-range clips instead of wrapping
WASAPI backend HARDWARE VERIFIED docs/WINDOWS_AUDIO.md
ASIO backend (separate GPLv3 DLL, attached through the external-backend C ABI) HARDWARE VERIFIED against FlexASIO and ASIO4ALL, both on the AI-04 interface with its output heard through a cable at the level native WASAPI gives (±0.03 dB); a manufacturer’s ASIO driver NOT AVAILABLE docs/ASIO.md

Engine on the native host (Windows)

tonesphere/engine/native_host.py gives AudioEngine the host interface it always used, backed by the native engine, so the UI, API, CLI and main.py test run on it unchanged. get_performance_stats()['backend'] says which host is in use; the PortAudio host remains for Linux/macOS, and on Windows only if the native DLL is missing (logged as a warning) or asked for (host_backend='portaudio').

Capability Level Evidence
Device enumeration from MMDevice (or ASIO drivers) with stable endpoint-ID keys; old name-based preset keys still resolve IMPLEMENTED native_devices, DeviceInfo.key/name_key, PresetManager._map_devices
Routing compiled to native plans and swapped live; streams restarted only when the set of devices changes VERIFIED the 822-test suite passes on it
Device → bus → device carries audio VERIFIED (offline) tests/native/test_engine.py::test_device_to_bus_to_device_carries_the_tone; the PortAudio host’s defect is fixed too (M17), and its strict xfail is now an ordinary passing test
Bus-only / network-only routing run by the native clock, sample-exact VERIFIED tests/native/test_engine_native_host.py
AudioEngine → native host → VST3 → WASAPI, level exact; fader and plugin survive a restart; preset restores the plugin and its state HARDWARE VERIFIED tests/hardware/test_engine_native_host.py: 0.07071 RMS heard for 0.07071 expected; after restart at half fader, again exact
Per-block load judged against each block’s own period; a device period longer than the engine block is cut into equal blocks VERIFIED test_load_is_judged_per_block_not_against_the_last_block; main.py test on the development machine: exclusive 128 frames, worst callback 0.025 ms = 0.8 % of the period, mean 0.2 %, 0 xruns, 0 allocations. The AI-04’s 144-frame exclusive period was cut 128 + 16, and the remainder’s 0.33 ms budget reported 152 % load for periods that finished in under 3 % of their time; it is cut 2 × 72 now (14.0 % worst, of a 1.5 ms block), in the WASAPI and ASIO loops alike
Satellite cushion trimmed to its target on priming VERIFIED (hardware) digital round trip 61.35–65.35 ms over six starts (within one 480-frame period), fixed within a run
Stereo strip balance and channel swap on the native host VERIFIED; HARDWARE VERIFIED (balance) balance is folded into the channel trims by the engine’s own law; swap is a native control crossfaded over one block (test_swap_exchanges_the_channels_without_a_step, test_the_native_strip_applies_balance_and_swap); at the speaker, balance 0.5 leaves the near side at unity and the far side at cos(π/4) within 2 % (test_bypass_balance_and_per_channel_meters_are_what_is_heard)
Per-process capture on the native host IMPLEMENTED (Python capture thread into a bus feed) tests/test_process_capture.py hardware tests pass on the native host; the native process_loopback stream kind is not yet used by AudioEngine
Device removal while running HARDWARE VERIFIED in the driver VM a cable disabled under a running engine: reported gone, the engine restarted without it, the other cable’s audio carried on; enabled again: reopened with no user action, audio back at −0.009 dB (test_a_cable_disabled_mid_stream_is_reopened_when_it_returns); the decision logic without hardware in tests/test_device_monitor.py

Measurements

Offline engine cost — benchmarks/bench_engine.py, results in benchmarks/results/m3_offline_i7-1165G7.json. Measured 2026-09-29 on the development machine (i7-1165G7, Windows 11 25H2, MSVC 14.50 release build), benchmark thread registered with MMCSS “Pro Audio”, 5 s of audio per configuration. Times are taken inside run_block; they are what ToneSphere’s own processing costs, not a latency and not a device run.

Scenario, 48 kHz Block Period Mean p99 Max Worst load
Guitar chain (3-band EQ, compressor, bus, delay, safety limiter) 32 667 µs 4.1 µs 8.0 µs 152.8 µs 22.9 %
  128 2667 µs 13.9 µs 19.0 µs 148.9 µs 5.6 %
  256 5333 µs 23.9 µs 32.0 µs 39.7 µs 0.7 %
16 stereo sources, EQ each, 2 buses, 2 limited outputs 32 667 µs 6.2 µs 11.3 µs 142.9 µs 21.4 %
  128 2667 µs 17.8 µs 26.9 µs 166.1 µs 6.2 %
  256 5333 µs 33.1 µs 45.3 µs 165.2 µs 3.1 %

Zero audio-thread allocations in every configuration (30 of them, 44.1/48/96 kHz × 32–512 frames). The worst single block is 140–230 µs in almost every configuration regardless of workload, while p99 tracks the workload — consistent with interrupt or DPC activity on this laptop pre-empting the thread, not established: it has not been traced. The tightest budget measured (32 frames at 96 kHz, 333 µs) peaked at 49 % of the period.

Real-time safety of the existing path

The existing callback is Python under the GIL, so none of the AGENTS.md real-time rules can hold for it; this is recorded so the native engine is measured against it, not so it can be patched into compliance.

Known defects in the PortAudio host, and where the native host stands

The PortAudio host is the audio path on Linux and macOS (and on Windows only with TONESPHERE_HOST=portaudio). The right-hand column is the same behaviour on the native host, which is the default on Windows.

Defect (PortAudio host) Where Consequence Native host (Windows)
Buses never forward their input host.py (no reader of device→bus rings) device→bus→device carries silence; the rings overflow works — test_device_through_a_bus_to_another_device (exact samples); fixed in the PortAudio host too (M17)
Strip pan stored, never applied fixed in M10 on both hosts: a stereo strip’s pan is its balance — works, HARDWARE VERIFIED
In/out meter keys collide fixed in M10: meters are reported per side — works
Stream restart drops channel controls and plugins core/engine.py:1129-1135 settings silently revert after reconfiguring works — strips, plugins and insert state survive restarts (test_the_engine_plays_through_a_plugin_and_keeps_it_across_a_restart)
Stereo balance steps 3 dB off centre host.py:1186-1190, dsp.py:253-255 level jump when a stereo pan leaves centre works — unity at centre, far side only (TestChannelMapping)
Graph-level solo never populated core/engine.py:1009-1015 reads a routing-matrix field nothing sets only per-device channel solo works same (control plane); the native plan compiles solo correctly when it is set (test_solo_silences_every_other_source)
Duplex in→out refused as feedback core/engine.py:407-437 the guitar path through AudioEngine cannot be made on one device id works — only bus cycles are refused (test_the_engine_refuses_nothing_it_used_to_accept)
Limiter has no lookahead, ramp starts at the old envelope dsp.py:340-360 first over-threshold block passes unreduced; with clip_off=True samples >1.0 can reach the driver the native limiter is instant-attack and sample-accurate, so no sample above its ceiling passes (no lookahead either) — tests/native/test_dsp.py::TestLimiter, test_the_sink_safety_limiter_catches_a_routing_mistake
pan_law_minus_6db is not a -6 dB law dsp.py:27-52 mislabelled option not applicable (one pan law)
Dead code GAIN_RAMP_FRACTION in host.py (_bus_pending removed in M17) — —

Migration progress

Milestone Scope Status
M0 AGENTS.md contract, CLAUDE.md pointer done
M1 This matrix, honesty fixes (reported vs measured latency, loopback claim, plugin and bus claims), dependency audit, shared test signals done
M2 Native foundation: toolchain, C ABI, SPSC, snapshots, offline process_block done — see Native engine below
M3 Native graph, mixer, DSP done — see Native engine and Measurements below
M4 Native WASAPI backend done — see docs/WINDOWS_AUDIO.md; switching AudioEngine onto it moves to M8, so the host interface is designed once, with ASIO and VST3 in view
M5 Native ASIO host done — HARDWARE VERIFIED against FlexASIO (software ASIO driver), and through FlexASIO on the Audio Array AI-04; no manufacturer ASIO driver exists for that interface — see docs/ASIO.md
M6 Measured round-trip latency done — tonesphere/native/roundtrip.py; digital path measured, acoustic path unavailable on this machine
M7 Native VST3 host done — see docs/VST3.md; reachable from the UI (M10), not the API
M8 Engine migration, persistence done — AudioEngine runs on NativeHost on Windows; see Engine on the native host below
M9 Windows virtual audio driver done in a test VM — installs, enumerates, carries audio between applications at the level sent, and uninstalls cleanly (2026-09-30, Hyper-V, test-signing); real-desktop use and production signing not available — docs/VIRTUAL_AUDIO_DRIVER.md
M10 UI done — device configuration, plugin browser, insert chains and parameter editor, diagnostics with round-trip measurement, virtual-device status; meters per side and channel; strip balance made real
M11 Validation, benchmarks, documentation done — soak and restart test (30 min, 180,024 callbacks: 0 xruns, 0 audio-thread allocations; callback mean 65.9 µs, p99 ≤ 128 µs, worst 1.10 ms (11 % of the 10 ms period); bus ring underruns after start 0; private bytes 280.3 → 280.5 MB; 25 of 25 restarts running within 55–63 ms, handle count 352 → 352; benchmarks/soak.py); GPLv3 Corresponding Source packaging and licence texts in the binary; legal documents revised (T&C 1.1, ToS 1.2, Privacy 1.1 — not lawyer-reviewed); docs/ARCHITECTURE.md, REALTIME.md, TESTING.md, ENGINEERING_REPORT.md; README rewritten. Device removal during streaming: NOT VERIFIED
M12 Hardware follow-up, 2026-09-30 done — an Audio Array AI-04 USB interface through native WASAPI (exclusive 48 kHz at a 3 ms period, 0 dropouts; its 44.1 kHz input clock recorded as a known failure) and through FlexASIO; round trip through it -- without a cable; the drift resampler rebuilt; the virtual driver HARDWARE VERIFIED in a Hyper-V VM after four real defects were found there; the Store package made MIT-only (no ASIO); docs published by GitHub Actions
M13 Measured round trip through the AI-04 done — a cable from its output to its input: 17.92 ms exclusive at a 3 ms period, 76.9 ms shared; from an ASIO buffer switch (measure_asio) through ASIO4ALL 2.22, 15.58 ms at 64 frames; ASIO output proven at the level native WASAPI gives
M14 Threaded, thread-safe control plane done — one lock per engine object on every public method (utils/threads.py); the UI’s engine calls on an ordered worker and a poller, never the main thread; REST handlers in the threadpool; the native engine swap made safe for network and capture threads (a race the new storm test found at once without the locks)
M15 Devices that come and go, whole-system loopback, built-in effects in the UI done — a device monitor reopens the engine when a routed device leaves or returns; every output’s loopback is a routable source (the system mix at the levels played, on the AI-04); the built-in EQ, compressor, limiter and delay in the Inserts dialog, in one chain with plugins, on devices and buses, in presets, surviving a rebuild; a delay set there heard through the AI-04’s cable at 100.000 ms
M16 Network: TCP send, adaptive jitter buffer, Opus done — TCP send wired through the send worker to the TCP router (sample for sample over localhost; a full bus waits instead of dropping, so TCP’s flow control slows the sender; a send-only instance’s connections no longer close on opening); the jitter buffer sizes itself to the measured delay spread (0 of 7,490 packets lost under ±15 ms of jitter where a fixed 10 ms buffer lost 32 %; comes down one packet a second when the link calms; through a real 0–30 ms relay 0–0.31 % lost); Opus through ctypes against libopus (built from Xiph’s pinned source on Windows, the system library elsewhere): 40.4 dB SNR on a 1 kHz tone, 161 bytes per 10 ms against 3,840 of PCM, losses concealed by Opus’s own PLC. Found on the way: the sender read one block per late timer wake, delivering a third less audio than it was given
M17 Device → bus → device on the PortAudio host (Linux/macOS) done — buses render: an output that consumes a bus renders it when its ring runs short, summing every route in and the feed Python writes to; a bus nothing clocks forwards as its producers arrive. Offline tests replace the strict xfail; on Linux through a real PulseAudio server, 1 kHz came through pulse → bus → pulse at exactly the expected level, 5 of 5 in WSL2, and in CI. macOS: same code, untested with devices
M18 Plugins beyond the desktop UI and beyond effects done — REST (/plugins, /chains, /instruments, /midi) and CLI (plugins, chain, effect, param, instrument, note); VST3 instruments on a bus of their own, played by MIDI through a native single-producer queue and event list: Surge XT and Dexed to pitch from the engine, REST, an on-screen keyboard and through the AI-04’s cable; MIDI from a hardware port implemented, untested (no device); Neural Amp Modeler not tested (unsigned installer)
M19 Several virtual cables, managed from the UI done in the driver VM — one cable per device instance (the sample’s one-device guard removed, a cable per adapter); two by default, up to eight; add, rename, disable, enable, uninstall and remove the driver from Engine → Virtual Cables…, elevated through Windows’ own prompt; a cable in use is refused cleanly rather than left pending a restart, and ToneSphere lets go of its own streams first; a cable disappearing under the engine is reported and reopened on return. ASIO4ALL tested in the VM on the cables. Found on the way and fixed: a WASAPI stop that never returned when a device’s event never stopped firing (the stop event was waited on second), an unbounded packet drain in the capture threads, and a host that would wait for ever on an ASIO driver stuck in stop() (now abandoned after 5 s, and reported). Open: an intermittent exclusive-mode round trip through a cable in the VM (3 of 10 runs; the audio arrives intact, the correlation fails on it)
M20 Acoustic round trip, documentation done — the laptop’s own speakers to its own microphone array: 74.60–75.90 ms in 9 of 10 runs, just above the confidence threshold, the speaker volume raised for the sweeps only and restored exactly; README, this file, the engineering report and the driver documents brought up to date; ASIO4ALL uninstalled from the development machine again
M21 A download that runs done — no arguments open the window (v0.2.0 printed usage into a console a windowed build does not have); file logging and crash reports in the per-user folder, the icon and presets found wherever the app is started from; a per-user Windows installer (no administrator prompt), a portable zip, an AppImage and a .dmg, each launched by its CI smoke test the way a user launches it; a version computed and a release published on every green push to main, the GPLv3 source archive moved to a gpl-source release linked from the notes. The prompt the owner took for elevation was SmartScreen on an unsigned download, reproduced with a Mark-of-the-Web copy
M22 Hearing your guitar done — Monitor Input became a dialog (input, channel, output; the interface preferred over built-in devices) that creates an unmuted route and starts the engine; native routes take one input channel to both ears (ABI 13); the engine starts itself at launch and when a cable is made; a drop anywhere on a node connects and a missed drop says why; strip settings re-applied after every plan; the MASTER meter reads outputs only; the session saved and restored; ASIO listed as unavailable with its reason; the Virtual Cables dialog says when the driver is absent
M23 Guitar Rig 7 done — the scanner’s child sends its output to files (Guitar Rig’s helper process inherited the pipes and held the scan for 120 s; it now takes 0.7 s), the parent trusts the result file over an exit code the plugin’s own teardown spoils, failed scans retried on Scan; the host sets the factory’s host context, gives every declared bus a buffer and never unloads a scanned module; HARDWARE VERIFIED, see the VST3 row
M24 Verified as a user would use it done — the built app driven through its own window on the AI-04 with a guitar (see First run); found and fixed on the way: a plugin setting changed while the plugin was not processing was missing from its saved state (now flushed by a zero-sample process() before the state is read), the Exclusive button claiming exclusive mode after a shared session was restored, the disabled ASIO entry drawn like a choice, and a first scan of Guitar Rig timing out in the installed build because its DLL detach hung the scanner’s exit after the result was written (the scanner now terminates itself, and the parent ends a child that reported but did not leave). Then installed from the CI-built ToneSphere-0.2.1-Setup.exe through its wizard with no administrator prompt, run non-elevated, and used the same way: −2.88 dB monitoring, −12.25 dB through Guitar Rig at −12.0 dB, −15.03 dB after reopening from the Start menu. And found by CI on macOS: a jitter buffer whose playout, once the sender stalled for longer than the buffer holds, ran on ahead of it for good — every later packet arrived just after its slot and was dropped (Opus over UDP: 200 received, 200 lost). Eight late packets in a row into an empty buffer now move playout back and re-prime it, counted as an underrun; a straggler or ordinary jitter never makes such a run (TestOvertakenPlayout). Discord: NOT TESTED as a capture source — it played no audio during the run and its window exposes no controls to automation; per-application capture itself is HARDWARE VERIFIED (Process loopback), and Discord’s microphone needs the virtual cable, VM-only by the owner’s decision