From ae4abf32cc0ad0d9a2ca7784a94303e8348dd286 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 17 May 2026 19:56:22 +0100 Subject: [PATCH] Add Faust render session boundary --- src/AquaSynth.Core/AquaSynth.Core.csproj | 2 +- src/AquaSynth.Faust/AquaSynth.Faust.csproj | 2 +- src/AquaSynth.Faust/NativeFaustRuntime.cs | 116 ++++++++++++++---- state/evidence.jsonl | 1 + state/map.yaml | 4 +- .../NativeFaustRuntimeTests.cs | 3 +- 6 files changed, 102 insertions(+), 26 deletions(-) diff --git a/src/AquaSynth.Core/AquaSynth.Core.csproj b/src/AquaSynth.Core/AquaSynth.Core.csproj index 0a916d0..72b4543 100644 --- a/src/AquaSynth.Core/AquaSynth.Core.csproj +++ b/src/AquaSynth.Core/AquaSynth.Core.csproj @@ -6,7 +6,7 @@ enable AquaSynth.Core AquaSynth.Core - 0.1.0-aquasynth.1 + 0.1.0-aquasynth.2 GameCult GameCult AquaSynth patch model, DSL parser, analysis helpers, presets, and Faust source emitter. diff --git a/src/AquaSynth.Faust/AquaSynth.Faust.csproj b/src/AquaSynth.Faust/AquaSynth.Faust.csproj index 519f168..57eb8a7 100644 --- a/src/AquaSynth.Faust/AquaSynth.Faust.csproj +++ b/src/AquaSynth.Faust/AquaSynth.Faust.csproj @@ -6,7 +6,7 @@ enable AquaSynth.Faust AquaSynth.Faust - 0.1.0-aquasynth.1 + 0.1.0-aquasynth.2 GameCult GameCult Native Faust toolchain loading, compile manifests, DSP factory lifetime, and offline rendering for AquaSynth patches. diff --git a/src/AquaSynth.Faust/NativeFaustRuntime.cs b/src/AquaSynth.Faust/NativeFaustRuntime.cs index 5797b3b..b37e65d 100644 --- a/src/AquaSynth.Faust/NativeFaustRuntime.cs +++ b/src/AquaSynth.Faust/NativeFaustRuntime.cs @@ -123,6 +123,99 @@ public sealed class AquaSynthCompiledPatch : IDisposable } } +public sealed class AquaSynthRenderSession : IDisposable +{ + private readonly AquaSynthNativeOptions options; + private AquaSynthNativeCompiler? compiler; + private string? loadError; + private bool disposed; + + public AquaSynthRenderSession(AquaSynthNativeOptions? options = null) + { + this.options = options ?? new AquaSynthNativeOptions(); + } + + public string? LoadError => loadError; + + public bool IsReady => compiler is not null; + + public string? FaustVersion => compiler?.FaustVersion; + + public bool TryCompileScript(AquaSynthCompileIdentity identity, out AquaSynthCompiledPatch? patch, out string? error) + { + ObjectDisposedException.ThrowIf(disposed, this); + patch = null; + if (!EnsureCompiler(out error)) + { + return false; + } + + try + { + patch = compiler!.CompileScript(identity); + error = null; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public bool TryRenderScript(string name, string script, float gain, out float[] samples, out string? error) + { + samples = []; + if (!TryCompileScript(new AquaSynthCompileIdentity(name, name, script), out var patch, out error)) + { + return false; + } + + using (patch) + { + samples = patch!.Render(gain); + error = null; + return true; + } + } + + public void Dispose() + { + if (disposed) + { + return; + } + + compiler?.Dispose(); + disposed = true; + } + + private bool EnsureCompiler(out string? error) + { + if (compiler is not null) + { + error = null; + return true; + } + + if (loadError is not null) + { + error = loadError; + return false; + } + + if (AquaSynthNativeCompiler.TryLoad(out var loaded, out loadError, options)) + { + compiler = loaded; + error = null; + return true; + } + + error = loadError; + return false; + } +} + public sealed class AquaSynthNativeCompiler : IDisposable { private readonly FaustNativeToolchain toolchain; @@ -160,27 +253,8 @@ public sealed class AquaSynthNativeCompiler : IDisposable out string? error, AquaSynthNativeOptions? options = null) { - samples = []; - if (!TryLoad(out var compiler, out error, options)) - { - return false; - } - - using (compiler) - { - try - { - using var patch = compiler!.CompileScript(new AquaSynthCompileIdentity(name, name, script)); - samples = patch.Render(gain); - error = null; - return true; - } - catch (Exception ex) - { - error = ex.Message; - return false; - } - } + using var session = new AquaSynthRenderSession(options); + return session.TryRenderScript(name, script, gain, out samples, out error); } public AquaSynthCompiledPatch CompileScript(AquaSynthCompileIdentity identity) diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 817250a..9f5c191 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -97,4 +97,5 @@ {"date":"2026-05-17","topic":"faust-toolchain-boundary","lesson":"AquaSynth should own patch-to-DSP compilation products, including Faust toolchain selection, target options, cache identity, manifests, diagnostics, provenance, and dynamic patch compilation outside the realtime callback. Aquarium Engine should request or load finished artifacts and host them, not own the meaning of patch compilation.","evidence":"User confirmed Aquarium currently owns dynamic patch compilation but that this belongs in AquaSynth so Aquarium can plug into it. Added `docs/faust-toolchain-boundary.md`, reframed README C# output as authoring/test lane, and updated doctrine/state to split Aqua compile authority from Aquarium realtime hosting."} {"date":"2026-05-17","topic":"native-faust-runtime-ownership","lesson":"AquaSynth now owns Aquarium's former native Faust audio-rendering internals: toolchain discovery/loading, script parsing, Faust emission, compile identity, DSP source artifact writing, native Faust factory lifetime, manifests, duration estimation, and offline sample-buffer rendering. Engine hosts should schedule and play completed products instead of knowing synth internals.","evidence":"Added `AquaSynthNativeCompiler`, `AquaSynthCompiledPatch`, `AquaSynthNativeManifest`, and native Faust smoke tests. Aquarium's `AquariumSynthHost` was cut down to scheduling/debounce/status/cache/WASAPI playback and now consumes `AquaSynth.Faust`."} {"date":"2026-05-17","topic":"package-split-core-faust","lesson":"AquaSynth package names now follow ownership: `AquaSynth.Core` owns patch meaning, parser/model, analysis, presets, and Faust source emission; `AquaSynth.Faust` depends on Core and owns Faust CLI/native toolchain interaction, compile products, manifests, factory lifetime, and rendering. Do not revive a catch-all `.Dsl` package for runtime-facing behavior.","evidence":"Moved the old `AquaSynth.Dsl` project to `src/AquaSynth.Core`, added `src/AquaSynth.Faust`, moved `FaustCompiler` and `NativeFaustRuntime` to the Faust package, and updated package-boundary tests to pack both packages."} +{"date":"2026-05-17","topic":"render-session-ownership","lesson":"`AquaSynthRenderSession` owns lazy native Faust compiler loading and one-shot render convenience. Engine hosts should hold a render session and request compiled products; they should not expose their own smoke-render helper or call `AquaSynthNativeCompiler.TryLoad` directly.","evidence":"Added `AquaSynthRenderSession` to AquaSynth.Faust and changed native render tests to use it. This removes the remaining reason for AquariumSynthHost to keep compiler-load policy."} diff --git a/state/map.yaml b/state/map.yaml index a5749f4..3fc90ca 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -24,8 +24,8 @@ surfaces: zyn_pad_reference_renderer: tools/zyn-reference/ZynPadReference.cpp zyn_reference_build_patches: tools/zyn-reference/patches zyn_project_fixtures: tests/AquaSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored - core_package: AquaSynth.Core 0.1.0-aquasynth.1 - faust_package: AquaSynth.Faust 0.1.0-aquasynth.1 + core_package: AquaSynth.Core 0.1.0-aquasynth.2 + faust_package: AquaSynth.Faust 0.1.0-aquasynth.2 modules: model: src/AquaSynth.Core/Model.cs parser: src/AquaSynth.Core/PatchScript.cs diff --git a/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs b/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs index 37b55b9..6a85cac 100644 --- a/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs +++ b/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs @@ -29,7 +29,8 @@ public sealed class NativeFaustRuntimeTests decay=0.12 """; - if (!AquaSynthNativeCompiler.TryRenderScript("native_smoke", script, 1.0f, out var samples, out var error)) + using var session = new AquaSynthRenderSession(); + if (!session.TryRenderScript("native_smoke", script, 1.0f, out var samples, out var error)) { if (error?.Contains("Faust toolchain not found", StringComparison.OrdinalIgnoreCase) == true || error?.Contains("Faust DLL not found", StringComparison.OrdinalIgnoreCase) == true)