Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4a5686a
create linux-compatible simulate.cs
jopdorp Mar 12, 2021
acc97e0
created SimulateSimple
jopdorp Mar 12, 2021
b795a95
implemented simulate simple
jopdorp Mar 16, 2021
534c1f1
managed to get some simulation working
jopdorp Mar 21, 2021
010f063
Merge remote-tracking branch 'upstream/master' into linux
jopdorp May 5, 2026
49591b4
Add headless Linux runner
jopdorp May 5, 2026
90eb8b4
Add Avalonia GUI port checkpoint
jopdorp May 6, 2026
0969481
Add virtual audio configuration flow
jopdorp May 6, 2026
e035476
Add live audio simulation mode
jopdorp May 6, 2026
e5184cc
Open all schematic launch arguments
jopdorp May 6, 2026
1797f2e
Add Avalonia editor interaction tests
jopdorp May 6, 2026
6b4b035
Show named waveform output traces
jopdorp May 6, 2026
9c9998e
Fix Avalonia menu open and schematic layout
jopdorp May 6, 2026
328b12d
Expand Avalonia coverage suite
jopdorp May 6, 2026
b740084
Add Linux plugin core and audio backend
jopdorp May 6, 2026
016da0b
Improve Avalonia plugin editor parity
jopdorp May 6, 2026
cd9f21e
Add node probes to Avalonia scope
jopdorp May 6, 2026
32b82fa
Add expression inputs to Avalonia scope
jopdorp May 6, 2026
6171d46
Fix Avalonia Linux audio driver selection
jopdorp May 7, 2026
5f99ea9
Clarify Avalonia audio configuration labels
jopdorp May 7, 2026
5c3cec9
Prefer JACK names for Linux audio ports
jopdorp May 7, 2026
95b2c5b
Add JACK live mode hardware test
jopdorp May 7, 2026
991f540
Strengthen JACK live mode hardware assertions
jopdorp May 7, 2026
48d8aa4
Wire Linux plugin GUI parity tests
jopdorp May 7, 2026
5aebc6d
Add MXR Phase 90 plugin coverage
jopdorp May 7, 2026
1c9634d
Support default schematic plugin builds
jopdorp May 7, 2026
ebb9ea2
Add native Linux LV2 phaser plugin
jopdorp May 7, 2026
fe96c94
Add LV2 schematic state hook
jopdorp May 7, 2026
d030240
Add generic Linux LV2 plugin shell
jopdorp May 7, 2026
1a6059e
Add GTK LV2 schematic loader UI
jopdorp May 7, 2026
d09240c
Show schematic controls in LV2 UI
jopdorp May 7, 2026
3a9dc7e
Style LV2 UI like Windows VST
jopdorp May 7, 2026
ce36533
Refine LV2 UI layout testing
jopdorp May 7, 2026
706c339
Set LV2 knob zero to top
jopdorp May 7, 2026
d7fef8d
Add LV2 knob scale indicators
jopdorp May 7, 2026
13aea24
Refine LV2 knob tick scale
jopdorp May 7, 2026
7507f43
Enlarge LV2 knob scale labels
jopdorp May 7, 2026
90adb18
Fix Linux editor and LV2 label spacing
jopdorp May 7, 2026
169712f
Center LV2 knob label area
jopdorp May 7, 2026
04df018
Split Linux UI into dedicated solution
jopdorp May 7, 2026
3d03188
Keep Windows VST files unchanged
jopdorp May 7, 2026
4af7ef2
Address Linux GUI PR review comments
jopdorp May 8, 2026
0dd943e
Merge upstream PR #272: Avalonia GUI, plugin core, LV2 plugins
tobleromed Jul 29, 2026
5b07b4c
Wire CoreAudio into the Avalonia GUI; make its test suite runnable
tobleromed Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,9 @@ jobs:
dotnet run -c Release --no-build -- render \
--schematic "../Tests/Examples/Passive 1stOrder Lowpass RC.schx" \
--input /tmp/tone.wav --output /tmp/out.wav
# The Avalonia GUI and its tests run headless (Avalonia.Headless.XUnit), so they work on
# runners with no display or audio hardware.
- name: Build Avalonia GUI
run: dotnet build LiveSPICE.Linux.sln -c Release
- name: Run Avalonia tests
run: dotnet test LiveSPICE.Avalonia.Tests/LiveSPICE.Avalonia.Tests.csproj -c Release --no-build
57 changes: 57 additions & 0 deletions Circuit/AudioSimulationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using ComputerAlgebra;
using System;
using System.Linq;

namespace Circuit
{
public static class AudioSimulationFactory
{
public static TransientSolution Solve(Circuit circuit, double sampleRate, int oversample)
{
if (circuit == null)
throw new ArgumentNullException(nameof(circuit));
if (sampleRate <= 0)
throw new ArgumentOutOfRangeException(nameof(sampleRate));
if (oversample <= 0)
throw new ArgumentOutOfRangeException(nameof(oversample));

return TransientSolution.Solve(circuit.Analyze(), (Real)1 / (sampleRate * oversample));
}

public static Simulation Create(Circuit circuit, double sampleRate, int oversample, int iterations)
{
return Create(circuit, Solve(circuit, sampleRate, oversample), oversample, iterations);
}

public static Simulation Create(Circuit circuit, TransientSolution solution, int oversample, int iterations)
{
if (circuit == null)
throw new ArgumentNullException(nameof(circuit));
if (solution == null)
throw new ArgumentNullException(nameof(solution));
if (oversample <= 0)
throw new ArgumentOutOfRangeException(nameof(oversample));
if (iterations <= 0)
throw new ArgumentOutOfRangeException(nameof(iterations));

Expression inputExpression = circuit.Components.OfType<Input>().Select(i => i.In).SingleOrDefault();
if (inputExpression == null)
throw new NotSupportedException("Circuit has no inputs.");

Expression outputExpression = 0;
foreach (Speaker speaker in circuit.Components.OfType<Speaker>())
outputExpression += speaker.Out;

if (outputExpression.EqualsZero())
throw new NotSupportedException("Circuit has no speaker outputs.");

return new Simulation(solution)
{
Oversample = oversample,
Iterations = iterations,
Input = new[] { inputExpression },
Output = new[] { outputExpression }
};
}
}
}
2 changes: 2 additions & 0 deletions Circuit/Circuit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
<PackageReference Include="MathNet.Numerics" Version="4.12.0" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions Circuit/Components/Resistor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ protected internal override void LayoutSymbol(SymbolLayout Sym)

Draw(Sym, 0, -16, 16, 7);

Sym.DrawText(() => Name, new Coord(6, 0), Alignment.Near, Alignment.Center);
Sym.DrawText(() => resistance.ToString(), new Coord(-6, 0), Alignment.Far, Alignment.Center);
Sym.DrawText(() => Name, new Coord(14, 0), Alignment.Near, Alignment.Center);
Sym.DrawText(() => resistance.ToString(), new Coord(-14, 0), Alignment.Far, Alignment.Center);
}
}
}
189 changes: 189 additions & 0 deletions GUI_PORT_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# LiveSPICE Cross-Platform GUI Port Plan

## Current state

The local `linux` branch already has a working non-Windows simulation path through `LiveSPICE.Headless`. That branch builds a console runner around the shared `Circuit` library and `Circuit/AudioSimulationFactory.cs`, and the README documents Linux smoke tests and WAV processing.

The remaining portability gap is the GUI. The standalone editor is a WPF Windows desktop app in `LiveSPICE/LiveSPICE.csproj`, and the VST UI is also WPF-based in `LiveSPICEVst/LiveSPICEVst.csproj` through `AudioPlugSharpWPF`. Both target `net*-windows`, enable WPF/Windows Forms, and depend on Windows-only UI concepts such as `Microsoft.Win32` dialogs, WPF commands, WPF drawing, AvalonDock, and WinMM/ASIO-backed audio configuration.

## Branch strategy

Create a second branch from the local Linux branch so the headless Linux work stays intact and the GUI port can move independently:

```bash
git switch linux
git pull --ff-only origin linux
git switch -c linux-gui-port
```

Before running those commands locally, review the current dirty worktree. At the time this plan was written, `.gitignore` was modified and `.codacy/`, `PR_DESCRIPTION.md`, and `out/` were untracked. Either commit, stash, or intentionally carry those changes before switching branches.

## Recommended UI approach

Use Avalonia as the default open-source port target for the standalone GUI.

Avalonia is the closest fit because it keeps a XAML/C# mental model, supports Windows, macOS, and Linux, and has direct replacements for many WPF primitives used by LiveSPICE: windows, menus, command bindings, layout panels, data binding, custom controls, pointer input, drawing contexts, file pickers, and theming.

Do not start with Electron unless the goal changes to a web-first shell. The existing editor is deeply tied to C# circuit objects, custom schematic rendering, and direct manipulation of schematic elements, so an Electron port would add an interprocess API and rewrite most UI logic instead of adapting it.

Treat Avalonia XPF as an optional commercial shortcut, not the default plan. It may run more existing WPF XAML with fewer edits, but it adds licensing and still leaves platform-specific APIs, WPF-only dependencies, and audio/plugin details to solve.

## Architecture target

Keep the existing simulation core and serialization shared:

- `Circuit` remains the cross-platform schematic, component, solver, and simulation library.
- `ComputerAlgebra` and `Util` remain shared support libraries.
- `LiveSPICE.Headless` remains the Linux command-line validation target.
- A new Avalonia app, tentatively `LiveSPICE.Avalonia`, hosts the cross-platform editor UI.
- The existing WPF `LiveSPICE` app stays buildable on Windows until the Avalonia app reaches feature parity.

Avoid trying to retarget `LiveSPICE/LiveSPICE.csproj` in place at first. A sibling project makes it possible to port control by control while preserving the Windows app as a reference implementation.

## Porting phases

### Phase 1: Establish the Avalonia shell

Create `LiveSPICE.Avalonia` targeting `net8.0` or newer without a `-windows` target framework. Add it to `LiveSPICE.sln` and reference `Circuit`, `ComputerAlgebra`, and `Util`.

Build the first shell with:

- main window
- menu bar and toolbar
- status bar
- open/save file pickers for `.schx`
- single-document schematic viewer
- basic settings path under the user's platform-appropriate application data directory

Defer docking, MRU polish, audio config, and simulation UI until a schematic can be loaded and rendered.

### Phase 2: Port schematic rendering

Port `SchematicControls` concepts to Avalonia rather than trying to reuse WPF controls directly.

The core work is replacing WPF rendering types in `SchematicControls`:

- `Control.OnRender(DrawingContext)` becomes Avalonia custom control rendering.
- `System.Windows.Point`, `Vector`, `Matrix`, `Rect`, `Size` become Avalonia equivalents.
- WPF `Pen`, `Brushes`, `FormattedText`, geometry, and text APIs become Avalonia drawing APIs.
- Tooltips and hit testing need Avalonia pointer/event equivalents.

Start with read-only rendering of symbols and wires. Use the existing `Circuit.SymbolLayout` data as the model, because it is already UI-framework neutral. Once rendering is correct, add selection highlighting, terminal tooltips, and zoom/pan.

### Phase 3: Port schematic editing

Port `SchematicControl`, `SchematicViewer`, and `SchematicEditor` behavior into Avalonia equivalents.

Primary features:

- grid snapping and coordinate conversion
- selection rectangle and multi-select
- move, wire, symbol, and probe tools
- clipboard copy/paste using XML serialization
- undo/redo through `EditStack`
- save/save-as and external modification detection
- component library insertion

Keep edit operations model-driven. The existing `AddElements`, `RemoveElements`, `PropertyEdit`, and schematic serialization should continue to be the behavioral source of truth.

### Phase 4: Replace WPF-only desktop dependencies

Replace AvalonDock with a simpler first-pass layout: left component list, right property panel, central tabbed documents. Add a richer docking library only after the editor is functional.

Replace `DotNetProjects.Extended.Wpf.Toolkit` property grid usage with either:

- a small LiveSPICE-specific property editor generated from browsable component properties, or
- an Avalonia-compatible property grid package if one proves mature enough.

Prefer the small custom property editor initially. LiveSPICE mostly needs predictable editing of component values, not a general-purpose WPF toolkit clone.

### Phase 5: Audio on Linux and macOS

For the standalone GUI, keep audio driver selection behind the existing `Audio.Driver` abstraction. The current GUI references `Asio` and `WaveAudio`; `WaveAudio` is WinMM-only and `Asio` is Windows-oriented.

Implement new drivers as separate assemblies so `Audio.Driver.Drivers` can discover them like the existing drivers:

- `JackAudio` for Linux JACK, using a maintained .NET binding or a small native interop layer.
- `CoreAudio` for macOS only after the GUI can load, edit, and render schematics.

Initial Linux GUI milestones can run without real-time audio by reusing the headless WAV path. Real-time JACK should be a later milestone because it has native library, callback-thread, buffer-size, and packaging concerns.

### Phase 6: Plugin UI strategy

Handle the VST UI separately from the standalone editor.

AudioPlugSharp itself is intended to support non-Windows audio/plugin targets, but this repository's plugin UI currently inherits from `AudioPluginWPF` and references `AudioPlugSharpWPF`. Public AudioPlugSharp docs emphasize built-in WPF UI support, so cross-platform plugin UI support needs verification before committing to an implementation.

Investigate in this order:

1. Check whether current AudioPlugSharp has a non-WPF UI base class or host-embeddable native view API.
2. Build a minimal no-editor plugin on Linux/macOS using the existing `SimulationProcessor` logic.
3. Build a tiny cross-platform plugin editor proof of concept before porting LiveSPICE's plugin UI.
4. If AudioPlugSharp cannot host Avalonia directly, keep the plugin editor Windows-only for the first GUI branch and ship the standalone Avalonia editor plus headless/audio processing on Linux.

The plugin UI is much smaller than the full desktop app, but it has harder host-embedding constraints. Do not let it block the standalone GUI port.

## Milestones and validation

### Milestone A: Branch and baseline

- Create `linux-gui-port` from `linux`.
- Confirm `dotnet build Circuit/Circuit.csproj` passes.
- Confirm `dotnet build LiveSPICE.Headless/LiveSPICE.Headless.csproj` passes.
- Run the README headless smoke test against `Tests/Circuits/Passive 1stOrder Highpass RC.schx`.

### Milestone B: Avalonia app opens a schematic

- Create `LiveSPICE.Avalonia`.
- Load a `.schx` file.
- Display schematic metadata or a placeholder document tab.
- Save settings without Windows registry or WPF settings dependencies.

### Milestone C: Read-only schematic renderer

- Render wires, symbols, text, terminals, and schematic grid.
- Validate against known examples under `Tests/Examples`.
- Add screenshot comparison fixtures if practical.

### Milestone D: Basic editor parity

- Select, move, add, delete, undo, redo, copy, paste, save, and save-as.
- Component library and property editor are usable.
- Existing `.schx` files round-trip without serialization changes.

### Milestone E: Simulation workflow

- Run simulation from the Avalonia UI using `AudioSimulationFactory`.
- Display probe/scope output or an initial non-real-time render result.
- Keep Linux validation available without requiring JACK.

### Milestone F: Real-time audio

- Add JACK driver assembly for Linux.
- Validate callback stability, buffer sizes, sample rate selection, xrun behavior, and clean shutdown.
- Add CoreAudio driver or document that macOS support currently depends on plugin-host processing.

### Milestone G: Plugin investigation

- Prove whether AudioPlugSharp can host a non-WPF editor on Linux/macOS.
- Decide between Avalonia plugin UI, no-editor plugin, or Windows-only plugin UI for the first release.

## Technical risks

- Custom drawing is the largest standalone GUI task. `SymbolControl` and `WireControl` depend directly on WPF drawing APIs.
- Docking and property grid packages are WPF-specific and should be replaced, not ported first.
- File dialogs, clipboard, tooltips, command routing, keyboard modifiers, and mouse capture all need Avalonia-specific implementations.
- Real-time audio should not run on UI abstractions. Keep audio callback code isolated from Avalonia dispatching.
- Plugin UI portability may be constrained by the plugin host API more than by Avalonia itself.
- Packaging needs per-platform handling for native audio libraries, app icons, file associations, and macOS signing/notarization.

## First implementation checklist

1. Create `linux-gui-port` from `linux` after cleaning or stashing unrelated worktree changes.
2. Add `LiveSPICE.Avalonia` with a minimal window and solution entry.
3. Add a tiny `SchematicDocumentViewModel` that loads `Circuit.Schematic` from disk.
4. Port read-only drawing into a new Avalonia schematic canvas.
5. Add open-file flow and zoom/pan.
6. Add focused editor tools and property editing.
7. Add non-real-time simulation validation before JACK/CoreAudio.
8. Investigate AudioPlugSharp non-WPF editor support in a separate proof-of-concept branch or folder.
59 changes: 59 additions & 0 deletions LiveSPICE.Avalonia.Tests/AppSettingsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using LiveSPICE.Avalonia;
using Xunit;

namespace LiveSPICE.Avalonia.Tests;

public sealed class AppSettingsTests
{
[Fact]
public void SaveAndLoadRoundTripsWindowRecentAndAudioSettings()
{
string path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "settings.json");
AppSettings settings = new AppSettings
{
WindowWidth = 1400,
WindowHeight = 900,
AudioDriver = "Virtual Audio",
AudioDevice = "Managed loopback",
AudioInputs = new List<string> { "Input 1" },
AudioOutputs = new List<string> { "Output 1" }
};
settings.MarkUsed("Tests/Examples/MXR Phase 90.schx");

settings.Save(path);
AppSettings loaded = AppSettings.Load(path);

Assert.Equal(1400, loaded.WindowWidth);
Assert.Equal(900, loaded.WindowHeight);
Assert.Equal("Virtual Audio", loaded.AudioDriver);
Assert.Equal("Managed loopback", loaded.AudioDevice);
Assert.Equal("Input 1", Assert.Single(loaded.AudioInputs));
Assert.Equal("Output 1", Assert.Single(loaded.AudioOutputs));
Assert.EndsWith("MXR Phase 90.schx", Assert.Single(loaded.RecentFiles));
}

[Fact]
public void MarkUsedDeduplicatesAndCapsRecentFiles()
{
AppSettings settings = new AppSettings();

for (int i = 0; i < 25; i++)
settings.MarkUsed($"file-{i}.schx");
settings.MarkUsed("file-10.schx");

Assert.Equal(20, settings.RecentFiles.Count);
Assert.EndsWith("file-10.schx", settings.RecentFiles[0]);
Assert.Equal(settings.RecentFiles.Count, settings.RecentFiles.Distinct(StringComparer.OrdinalIgnoreCase).Count());
}

[Fact]
public void ExistingRecentFilesFiltersMissingEntries()
{
string existing = Path.GetTempFileName();
AppSettings settings = new AppSettings();
settings.MarkUsed("missing-file.schx");
settings.MarkUsed(existing);

Assert.Equal(existing, Assert.Single(settings.ExistingRecentFiles()));
}
}
22 changes: 22 additions & 0 deletions LiveSPICE.Avalonia.Tests/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Headless;
using LiveSPICE.Avalonia;
using LiveSPICE.Avalonia.Tests;
using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

// One headless Avalonia application for the whole test process. AppBuilder.Setup can only run
// once per process, so per-class setup helpers ("Setup was already called") and desktop
// SetupWithoutStarting (no dispatcher loop, so Dispatcher.UIThread.Invoke hangs) both fail when
// the suite runs together. Tests that touch controls or windows use [AvaloniaFact], which runs
// them on the headless dispatcher thread.
[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))]

namespace LiveSPICE.Avalonia.Tests;

public class TestAppBuilder
{
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>().UseHeadless(new AvaloniaHeadlessPlatformOptions());
}
27 changes: 27 additions & 0 deletions LiveSPICE.Avalonia.Tests/AudioDriverDiscoveryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Linq;
using System.Runtime.InteropServices;
using Xunit;

namespace LiveSPICE.Avalonia.Tests;

public class AudioDriverDiscoveryTests
{
[Fact]
public void CoreAudioDriverIsDiscoveredOnMacOS()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return;

// The driver must appear even with no audio hardware (CI runners), so assert on the
// driver, not its devices.
var drivers = AvaloniaAudioDrivers.Available();
Assert.Contains(drivers, d => d.Name == "Core Audio");
}

[Fact]
public void VirtualDriverIsAlwaysAvailable()
{
var drivers = AvaloniaAudioDrivers.Available();
Assert.Contains(drivers, d => d.Devices.Any(dev => dev.OutputChannels.Length > 0));
}
}
Loading
Loading