Skip to content

macOS support: portable core build, Core Audio backend, and headless CLI player - #1

Merged
ledogar merged 2 commits into
masterfrom
macos-port
Jul 29, 2026
Merged

macOS support: portable core build, Core Audio backend, and headless CLI player#1
ledogar merged 2 commits into
masterfrom
macos-port

Conversation

@ledogar

@ledogar ledogar commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Adds macOS support alongside the Windows app rather than changing it. LiveSPICE.sln and every WPF/VST project are untouched; the macOS projects build from a separate LiveSPICE.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, ComputerAlgebra were already netstandard2.0 with one Windows dependency: the kernel32!MoveFileEx P/Invoke in Schematic.Save, now File.Move(overwrite: true) under net8.0. Tests was pinned to net6.0-windows only for the System.Drawing plotting sidecar, now #if PLOTTING-guarded.

The bigger change is that test previously passed if nothing threw — it never compared against Tests/Stats/*.csv. There's now a --check mode 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, because Simulation.TimeStep is Solution.TimeStep * oversample = 1/SampleRate regardless of oversample. Recovered by fitting the input-node row common to all 49 files.

Result: on windows-latest all 49 circuits match with max deviation exactly 0.

2. Core Audio backend + headless player (a29ff31)

CoreAudio/ implements the Audio contract with AUHAL via P/Invoke to the system frameworks, mirroring how WaveAudio P/Invokes winmm — no third-party native dependency. Follows existing patterns: Driver per Asio/Driver.cs, Stream per Asio/Stream.cs (render callback, not WaveAudio's polling thread).

  • One AudioUnit drives everything; the output-bus render callback pulls input from bus 1 first, so there's a single clock and no ring buffer. Requires same-device duplex, which Audio.Device.Open already assumes.
  • Non-interleaved float32, so each AudioBuffer maps 1:1 onto Audio.Util.LEf32ToLEf64/LEf64ToLEf32. Those helpers are unit-stride only.
  • Output clamped before conversion — LEf64ToLEf32 doesn't clamp and neither does Core Audio.
  • Driver ctor no-ops off macOS, so it builds and is inert on Windows.

LiveSPICE.CLI/ provides list, tone, render, play, loopback. Its SimulationHost adds 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.Run compiles on first call, so the VST currently stalls inside the audio callback after every pot change), and it recovers from SimulationDiverged.

Verification (macOS 26.5, Apple Silicon)

  • render of Passive 1stOrder Lowpass RC matches 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.
  • BlackHole loopback returns the played tone at full amplitude, rms exactly A/√2 — covering enumeration, format negotiation, the render callback, both conversions and teardown.
  • Live play: exactly 282 × 512 frames in 3 s at 48 kHz, no dropouts, clean stop. Tone from real speakers confirmed by ear.
  • Circuit suite 49/49 on macOS against the Windows-generated baselines, 45 to ≤1e-12 — meaning 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 Rat needs a loose tolerance (1e-1, documented in Test.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.
  • Input-only devices throw NotSupportedException pointing at Aggregate Devices. Duplex and output-only work.
  • No GUI — headless only. PR Add Linux GUI, audio, and LV2 plugin port dsharlet/LiveSPICE#272 upstream (jopdorp:linux-gui-port) looks like the better starting point; Avalonia runs natively on macOS.
  • CoreAudio isn't in LiveSPICE.sln.

Untested: the full guitar chain through a USB interface.

🤖 Generated with Claude Code

tobleromed and others added 2 commits July 28, 2026 18:54
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>
@ledogar
ledogar merged commit 29f204d into master Jul 29, 2026
4 checks passed
@ledogar
ledogar deleted the macos-port branch July 29, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants