Add macOS support (Core Audio backend + headless player), and make the circuit tests assertive - #274
Closed
ledogar wants to merge 2 commits into
Closed
Add macOS support (Core Audio backend + headless player), and make the circuit tests assertive#274ledogar wants to merge 2 commits into
ledogar wants to merge 2 commits into
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>
Author
|
Apologies, PR created in error |
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.
This adds macOS support alongside the existing Windows app rather than changing it.
LiveSPICE.slnand every WPF/VST project are untouched; the macOS-specific projects build from a separateLiveSPICE.Core.sln. The Windows CI job in this PR passes unchanged, so the risk to existing paths should be close to zero.It's two separable commits, and the first is useful on its own even if you don't want the audio work.
1. Portable core build, and tests that actually assert (
c9d8b65)The core is already portable —
Util,Audio,Circuit,ComputerAlgebraarenetstandard2.0with exactly one Windows dependency: thekernel32!MoveFileExP/Invoke inSchematic.Save, which is nowFile.Move(overwrite: true)undernet8.0(the in-source comment anticipated this).Testswas pinned tonet6.0-windowsonly because of theSystem.Drawingplotting sidecar, now#if PLOTTING-guarded and excluded off Windows.The more valuable half is the test change.
testpreviously passed if nothing threw — it never compared againstTests/Stats/*.csv. There's now a--checkmode that compares every variable's mean/min/max/rms against those baselines and exits non-zero on mismatch, wired into CI on both platforms.Making that work required recovering how the baselines were generated, which wasn't recorded:
--sampleRate 44100, all other options default. At any other rate they don't reproduce —Simulation.TimeStepisSolution.TimeStep * oversample=1/SampleRateregardless of oversample, so the sample rate alone sets the grid. I recovered it by fitting the input-node row that's common to all 49 files. Worth recording somewhere permanent; it's now in the CI invocation and a doc comment.With that, on
windows-latestall 49 circuits match the committed baselines with max deviation exactly 0.2. Core Audio backend and headless player (
a29ff31)CoreAudio/implements theAudiocontract with AUHAL via P/Invoke to the system frameworks, mirroring howWaveAudioP/Invokes winmm — no third-party native dependency, nothing checked in. It follows the existing patterns:DriverperAsio/Driver.cs(one device each, per-device try/catch),StreamperAsio/Stream.cs(render callback, not WaveAudio's polling thread).A few decisions worth reviewing:
Audio.Device.Openalready assumes.AudioBuffermaps 1:1 ontoAudio.Util.LEf32ToLEf64/LEf64ToLEf32. Those helpers are unit-stride only; interleaved would need a new de-interleave helper.LEf64ToLEf32doesn't clamp and neither does Core Audio.Driverconstructor no-ops off macOS, so this builds and is inert on Windows.LiveSPICE.CLI/is a headless player:list,tone,render,play,loopback. It doesn't reuseLiveSPICEVst/SimulationProcessor.cs— that code is genuinely portable, but reusing it meant either duplicating it or restructuring a Windows-only csproj I can't build. If you'd prefer it factored into a shared project, that's an easy follow-up and probably the right long-term shape.Its
SimulationHostdoes add two things the VST path lacks, which may be worth porting back: it forces the Linq expression tree to compile on the build thread (Simulation.Runcompiles on first call, so today the VST takes a multi-millisecond stall inside the audio callback after every pot change), and it recovers fromSimulationDivergedthe wayLiveSimulationdoes.Verification
On macOS 26.5 / Apple Silicon:
renderofPassive 1stOrder Lowpass RCmatches the analytic RC transfer function to 0.0002% in magnitude. The residual 0.29° phase lag is the half-sample delay from oversample output averaging (predicted 0.335°).Big Muff Pirenders 30.4% THD with odd harmonics dominant, as expected for symmetric diode clipping, from a 0.008% THD input.That last point is the one I'd highlight: it means
Expression.Compile()codegen, the Newton iteration and the Gauss-Jordan solver agree across OS, CPU architecture and SIMD width.Vector<double>is width-invariant here because the row operations are elementwise, so NEON at width 2 and AVX2 at width 4 give bit-identical results.Known limitations
Pro Co Ratneeds a loose tolerance (1e-1, documented inTest.cs). It's bit-identical run-to-run on each platform, but hard clipping in feedback deterministically amplifies ulp-level libm differences into a ~4e-2 trajectory shift. Windows matches its baseline exactly; macOS doesn't. I didn't want to hide this behind a global tolerance.NotSupportedExceptionpointing at Aggregate Devices. Duplex and output-only work. Mac built-in mic and speakers are separate Core Audio devices, so they need an aggregate.CoreAudioisn't inLiveSPICE.sln. Happy to add it if you want it built on Windows too (it's inert there).Related: #262, #263, #202, #51.
🤖 Generated with Claude Code