macOS support: portable core build, Core Audio backend, and headless CLI player - #1
Merged
Conversation
Build/port changes: - Add LiveSPICE.Core.sln: the portable subset (Util, Audio, Circuit, ComputerAlgebra, Tests). LiveSPICE.sln and all WPF projects untouched. - Tests: multi-target net6.0-windows;net8.0, collapsing to net8.0 off Windows. The System.Drawing/WinForms plotting sidecar (Tests/Plotting/) is excluded there and guarded with #if PLOTTING; --plot warns instead of crashing. Fix hardcoded '\' path separators in Stats/Plots output. - Circuit: multi-target netstandard2.0;net8.0 and replace the kernel32!MoveFileEx P/Invoke in Schematic.Save with File.Move(overwrite: true) on net8+. This was the only Windows dependency in the core library. Test-suite changes: - Add a --check mode to the harness that compares each circuit's per-variable mean/min/max/rms against the committed baselines in Tests/Stats/, with deviations normalized by signal scale (max(|min|,|max|)), NaN-safe comparison, InvariantCulture I/O, and a nonzero exit code on mismatch. - The committed baselines were generated at --sampleRate 44100 (all other options default); this configuration was recovered by fitting the shared input-node signature present in every baseline file. At that rate, macOS arm64 reproduces the Windows-generated numbers to <=1e-12 (scale-normalized) for 45 of 49 circuits and <=1e-9 for 48. - Pro Co Rat is checked at a loose 1e-1 tolerance: its hard-clipping feedback deterministically amplifies platform ulp differences into a stable ~4e-2 trajectory shift (bit-identical run-to-run per platform). - CI: add a macos-latest job building LiveSPICE.Core.sln, and run --check --sampleRate 44100 on both the Windows and macOS jobs so a green test run asserts numerical agreement rather than only "no exceptions". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was no working audio backend on macOS: WaveAudio is winmm P/Invoke and Asio walks COM vtables against HKLM\SOFTWARE\ASIO. Both are Windows only by construction, but the Audio abstraction they implement is a small 4-method contract that a new backend can satisfy directly. CoreAudio/ implements that contract with AUHAL via P/Invoke to the system frameworks, mirroring how WaveAudio P/Invokes winmm - no third party native dependency. Notable points: - One AudioUnit bound to one device drives everything: the output bus render callback pulls input from the input bus first, so there is a single clock and no ring buffer. This requires input and output to be the same device, which Audio.Device.Open already assumes. - The stream format is non-interleaved float32, so each AudioBuffer maps 1:1 onto Audio.Util.LEf32ToLEf64 / LEf64ToLEf32. Audio.Util's converters are unit-stride only; interleaved would need a new de-interleave helper. - Buffers are allocated once at open. SampleBuffer pins its array for life, so allocating in the render callback would fragment the heap. - The callback does no allocation, logging or locking, and cannot let a managed exception escape into the IOProc. - Output is clamped before conversion: LEf64ToLEf32 does not clamp and neither does Core Audio. - The Driver constructor no-ops off macOS, so this builds and is harmless on Windows. Input-only devices throw NotSupportedException with a message pointing at Aggregate Devices; duplex and output-only work. LiveSPICE.CLI/ is a headless player with list, tone, render, play and loopback commands. It deliberately does not reuse LiveSPICEVst's SimulationProcessor: that code is portable, but it is built around the VST's control model and using it would mean either duplicating it or restructuring a Windows-only csproj. SimulationHost adds two things the VST path lacks: it forces the Linq expression tree to compile on the build thread (Simulation.Run compiles on first call, which would otherwise be a multi-millisecond stall inside the audio callback), and it recovers from SimulationDiverged the way LiveSimulation does. Verified on macOS 26.5 arm64: - list matches system_profiler exactly. - render of Passive 1stOrder Lowpass RC matches the analytic RC transfer function to 0.0002% in magnitude; the 0.29 degree phase lag is the half-sample delay from oversample output averaging. - Big Muff Pi renders 30.4% THD with odd harmonics dominant, as expected for symmetric diode clipping, from a 0.008% THD input. - BlackHole loopback returns the played tone at full amplitude with rms exactly A/sqrt(2), verifying enumeration, format negotiation, the render callback, both conversions and teardown. - 3 s of live play produces exactly 282 x 512 frames at 48 kHz with no dropouts and a clean stop. - The circuit test suite still passes 49/49 against the baselines. CI runs list and an offline render on the macOS job; the live path needs hardware the runners don't have, which is why render exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds macOS support alongside the Windows app rather than changing it.
LiveSPICE.slnand every WPF/VST project are untouched; the macOS projects build from a separateLiveSPICE.Core.sln. The Windows CI job passes on this branch, so existing paths are unaffected.Two separable commits.
1. Portable core build + assertive tests (
c9d8b65)Util,Audio,Circuit,ComputerAlgebrawere alreadynetstandard2.0with one Windows dependency: thekernel32!MoveFileExP/Invoke inSchematic.Save, nowFile.Move(overwrite: true)undernet8.0.Testswas pinned tonet6.0-windowsonly for theSystem.Drawingplotting sidecar, now#if PLOTTING-guarded.The bigger change is that
testpreviously passed if nothing threw — it never compared againstTests/Stats/*.csv. There's now a--checkmode comparing every variable's mean/min/max/rms against those baselines, exiting non-zero on mismatch, wired into CI on both platforms.That required recovering how the baselines were generated, which wasn't recorded anywhere:
--sampleRate 44100, all other options default. At any other rate they don't reproduce, becauseSimulation.TimeStepisSolution.TimeStep * oversample=1/SampleRateregardless of oversample. Recovered by fitting the input-node row common to all 49 files.Result: on
windows-latestall 49 circuits match with max deviation exactly 0.2. Core Audio backend + headless player (
a29ff31)CoreAudio/implements theAudiocontract with AUHAL via P/Invoke to the system frameworks, mirroring howWaveAudioP/Invokes winmm — no third-party native dependency. Follows existing patterns:DriverperAsio/Driver.cs,StreamperAsio/Stream.cs(render callback, not WaveAudio's polling thread).Audio.Device.Openalready assumes.AudioBuffermaps 1:1 ontoAudio.Util.LEf32ToLEf64/LEf64ToLEf32. Those helpers are unit-stride only.LEf64ToLEf32doesn't clamp and neither does Core Audio.Driverctor no-ops off macOS, so it builds and is inert on Windows.LiveSPICE.CLI/provideslist,tone,render,play,loopback. ItsSimulationHostadds two things the VST path lacks and that are worth porting back: it forces the Linq expression tree to compile on the build thread (Simulation.Runcompiles on first call, so the VST currently stalls inside the audio callback after every pot change), and it recovers fromSimulationDiverged.Verification (macOS 26.5, Apple Silicon)
renderofPassive 1stOrder Lowpass RCmatches the analytic RC transfer function to 0.0002% in magnitude; the 0.29° phase lag is the half-sample delay from oversample output averaging (predicted 0.335°).Big Muff Pi: 30.4% THD, odd harmonics dominant, from a 0.008% THD input.Expression.Compile()codegen, Newton iteration and Gauss-Jordan agree across OS, architecture and SIMD width.Vector<double>is width-invariant here because the row operations are elementwise.Known limitations
Pro Co Ratneeds a loose tolerance (1e-1, documented inTest.cs). Bit-identical run-to-run per platform, but hard clipping in feedback amplifies ulp-level libm differences into a ~4e-2 trajectory shift. Not hidden behind a global tolerance.NotSupportedExceptionpointing at Aggregate Devices. Duplex and output-only work.jopdorp:linux-gui-port) looks like the better starting point; Avalonia runs natively on macOS.CoreAudioisn't inLiveSPICE.sln.Untested: the full guitar chain through a USB interface.
🤖 Generated with Claude Code