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:
- NOT IMPLEMENTED — does not exist.
- UNVERIFIED — code exists; real-world behaviour not demonstrated.
- IMPLEMENTED — code exists and an automated CI-safe test demonstrates it.
- VERIFIED — plus a signal-based integration test end to end.
- HARDWARE VERIFIED — exercised on real hardware/software, result recorded below.
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.
uv run pytest -m "not hardware": 660 passed, 2 skipped (Linux PulseAudio test;makeappxschema check), 7 deselected — before any change on this branch.uv run pytest -m hardware: 7 passed — output stream on the default Realtek device without xruns; process-loopback self-capture of a 1 kHz tone.
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, “ |
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 ( |
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.
- Allocation per block: strings/tuples/sets per connection (
host.py:1000-1037),source * -1.0for invert (host.py:1102), gain-ramp temporaries (host.py:1110-1115),DriftResamplerfancy indexing (dsp.py:441-446), biquadastype/empty_likeand a per-sample Python loop (effects.py:174-187),np.absin limiter/compressor/meters,np.ascontiguousarray(block.T)and pedalboard’s return copies (effects.py:708-720). - Logging in the callback:
logger.erroron a plugin exception (effects.py:717). - Cross-thread mutation without snapshots:
PluginChainandParametricEQread three parallel lists withzip(strict=True)while the control thread mutates them in steps (effects.py:645-663,:258-282) — a torn read raises in the callback. The graph and route table are published separately (host.py:862), so a callback can see a new graph with an old route table (a route skipped, or mixed 3 dB hot for one block). - No thread priority: no MMCSS, no GC control, no switch-interval tuning.
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) |
| fixed in M10 on both hosts: a stereo strip’s pan is its balance | — | works, HARDWARE VERIFIED | |
| 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 |