diff --git a/README.md b/README.md index b9a7a34..8c86641 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ dotnet pack src\AquaSynth.Faust\AquaSynth.Faust.csproj -c Release `AquaSynth.Core` owns patch meaning: model records, the `.aqua` parser, authoring helpers, analysis/scoring tools, presets, and Faust source emission. `AquaSynth.Faust` depends on Core and owns toolchain/rendering work: Faust CLI -validation, target-code generation, native `libfaust` loading, compile -manifests, DSP factory lifetime, and offline/sample rendering. +validation, target-code generation, live patch compilation, native `libfaust` +loading, compile manifests, DSP factory lifetime, and offline/sample rendering. The test suite verifies that development fixtures, Python render helpers, external reference synth sources, and SysEx banks do not ship in @@ -63,9 +63,10 @@ published packages. from `AquaSynth.Faust`. - `FaustCompiler.CompileAsync(source, options)` writes generated backend code through an installed or resolved Faust compiler from `AquaSynth.Faust`. -- `AquaSynthNativeCompiler` from `AquaSynth.Faust` loads a Faust native - toolchain, compiles `.aqua` scripts into hosted DSP factories, emits - manifests, and renders buffers for engine hosts. +- `AquaSynthPatchCompiler` from `AquaSynth.Faust` is the live host-facing + boundary for compiling `.aqua` scripts into hosted DSP factories. +- `AquaSynthNativeCompiler` remains the lower native `libfaust` compiler + machinery behind that boundary. - `BuiltInScripts.ReferenceScripts()` carries readable SFXR, BFXR-flavored, 808, FM bell, wobble bass, and advanced layered patches. They are stable references for testing and for judging whether the DSL can express useful @@ -98,11 +99,11 @@ host the finished artifact, bind controls, and schedule audio. Native Faust targets are the preferred shipping path when the platform allows them; C# output is convenient authoring machinery, not the ceiling. -`AquaSynthNativeCompiler` is the first runtime-facing version of that boundary: -it owns native `libfaust` loading, script parsing, Faust emission, compile keys, -DSP source artifacts, manifests, and sample rendering. Engine callers keep their -device and scheduling code, and ask AquaSynth for compiled/renderable synth -products. +`AquaSynthPatchCompiler` is the live runtime-facing version of that boundary: it +owns native `libfaust` loading policy, script parsing, Faust emission, compile +keys, DSP source artifacts, manifests, and compiled products. Engine callers +keep their device and scheduling code, and ask AquaSynth for compiled/renderable +synth products. See [`docs/faust-toolchain-boundary.md`](docs/faust-toolchain-boundary.md). diff --git a/docs/faust-toolchain-boundary.md b/docs/faust-toolchain-boundary.md index 20646c0..2aa1319 100644 --- a/docs/faust-toolchain-boundary.md +++ b/docs/faust-toolchain-boundary.md @@ -10,8 +10,8 @@ Package ownership follows that boundary: - `AquaSynth.Core` owns patch meaning: model records, `.aqua` parsing, analysis, presets, and Faust source emission. - `AquaSynth.Faust` depends on Core and owns toolchain/rendering: Faust CLI - validation, target-code generation, native `libfaust` loading, compile - manifests, factory lifetime, and offline/sample rendering. + validation, target-code generation, live patch compilation, native `libfaust` + loading, compile manifests, factory lifetime, and offline/sample rendering. ## Pipeline Map @@ -41,6 +41,7 @@ AquaSynth owns: - diagnostics and compile provenance - dynamic patch compilation outside the realtime callback - native Faust factory lifetime and offline/sample-buffer rendering for hosts +- live patch compiler sessions requested by engine hosts Aquarium owns: @@ -72,10 +73,12 @@ ordinary patch controls move through the compiled DSP parameter API. - Consumer runtimes should normally bundle compiled DSP artifacts plus manifests, not the compiler itself, unless live user patch authoring is a product feature. -Current API foothold: `AquaSynthNativeCompiler` loads `libfaust`, compiles -`.aqua` scripts into native Faust factories, writes optional `.dsp` artifacts, -returns `AquaSynthNativeManifest`, and renders mono sample buffers for hosts that -need triggered one-shot playback. +Current API foothold: `AquaSynthPatchCompiler` is the host-facing live compiler +surface. It owns lazy `libfaust` loading through AquaSynth, compiles `.aqua` +scripts into native Faust factories, writes optional `.dsp` artifacts, and +returns `AquaSynthCompiledPatch` products with `AquaSynthNativeManifest`. +`AquaSynthRenderSession` is only a render-proof convenience wrapper for tests +and offline one-shot buffers. ## Invariants diff --git a/src/AquaSynth.Core/AquaSynth.Core.csproj b/src/AquaSynth.Core/AquaSynth.Core.csproj index acb11ad..286448d 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-alpha.2 + 0.1.0-alpha.3 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 dcbc7bb..c638f96 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-alpha.2 + 0.1.0-alpha.3 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 b37e65d..a6f320a 100644 --- a/src/AquaSynth.Faust/NativeFaustRuntime.cs +++ b/src/AquaSynth.Faust/NativeFaustRuntime.cs @@ -123,14 +123,14 @@ public sealed class AquaSynthCompiledPatch : IDisposable } } -public sealed class AquaSynthRenderSession : IDisposable +public sealed class AquaSynthPatchCompiler : IDisposable { private readonly AquaSynthNativeOptions options; private AquaSynthNativeCompiler? compiler; private string? loadError; private bool disposed; - public AquaSynthRenderSession(AquaSynthNativeOptions? options = null) + public AquaSynthPatchCompiler(AquaSynthNativeOptions? options = null) { this.options = options ?? new AquaSynthNativeOptions(); } @@ -591,6 +591,56 @@ internal sealed class NativeUtf8String : IDisposable } } +public sealed class AquaSynthRenderSession : IDisposable +{ + private readonly AquaSynthPatchCompiler compiler; + private bool disposed; + + public AquaSynthRenderSession(AquaSynthNativeOptions? options = null) + { + compiler = new AquaSynthPatchCompiler(options); + } + + public string? LoadError => compiler.LoadError; + + public bool IsReady => compiler.IsReady; + + public string? FaustVersion => compiler.FaustVersion; + + public bool TryCompileScript(AquaSynthCompileIdentity identity, out AquaSynthCompiledPatch? patch, out string? error) + { + ObjectDisposedException.ThrowIf(disposed, this); + return compiler.TryCompileScript(identity, out patch, out error); + } + + 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; + } +} + internal sealed class NativeStringArray : IDisposable { private readonly NativeUtf8String[] strings; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 2506161..a7ef5a0 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -97,8 +97,8 @@ {"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."} -{"date":"2026-05-17","topic":"package-versioning","lesson":"Use normal SemVer prerelease labels for AquaSynth packages. Do not encode the repo name into the version suffix; `0.1.0-alpha.2` is a real prerelease version, while `0.1.0-aquasynth.2` is project metadata cosplay.","evidence":"Renamed AquaSynth.Core and AquaSynth.Faust package versions from `0.1.0-aquasynth.2` to `0.1.0-alpha.2` before repacking Aquarium's local feed."} +{"date":"2026-05-18","topic":"render-session-ownership","lesson":"`AquaSynthRenderSession` is only an offline render-proof convenience. Engine hosts should use `AquaSynthPatchCompiler` for live compiled products and should not hold a render session as their Faust authority.","evidence":"Added `AquaSynthPatchCompiler` to AquaSynth.Faust, made `AquaSynthRenderSession` wrap it, and moved AquariumSynthHost from a live render session to the compiler surface."} +{"date":"2026-05-17","topic":"package-versioning","lesson":"Use normal SemVer prerelease labels for AquaSynth packages. Do not encode the repo name into the version suffix; `0.1.0-alpha.N` is a real prerelease version, while `0.1.0-aquasynth.N` is project metadata cosplay.","evidence":"Renamed AquaSynth.Core and AquaSynth.Faust package versions from `0.1.0-aquasynth.2` to the `0.1.0-alpha.N` line before repacking Aquarium's local feed."} {"date":"2026-05-17","topic":"solution-format","lesson":"AquaSynth uses a normal Visual Studio `.sln` solution because Rider does not reliably like `.slnx`. Keep tests, state, and docs looking for `AquaSynth.sln`, not `AquaSynth.slnx`.","evidence":"Generated `AquaSynth.sln` with `dotnet new sln --format sln`, added Core/Faust/tests projects, removed `AquaSynth.slnx`, and replaced solution references across tests/state."} {"date":"2026-05-17","topic":"zyn-formant-motion","lesson":"AquaSynth now owns a first vowel-frame motion surface instead of treating Zyn formants as static filters or loose counted pressure. Voice `vowels=` stores `|`-separated formant banks and `vowel_hz` scans them; Zyn `FORMANT_FILTER` sequences can seed this, while exact Zyn cutoff-position driving, formant slowness, and vowel clearness remain named pressure.","evidence":"Added `FormantFrame`, voice `FormantFrames`/`FormantFrameRateHz`, parser support for `vowels=`/`vowel_hz`, Faust cyclic frame blending, and `ZynInstrumentReader.RebuildFirstFormantMotionAsAquaSynthScript`. `UpstreamWideBassExportsFormantMotionWorkoutWhenAvailable` exercises `Companion/0056-Wide Bass.xiz`; focused PatchScript/ZynInstrument tests passed 73/73 and non-reference tests passed 103/103."} {"date":"2026-05-17","topic":"zyn-formant-parity-fixtures","lesson":"The first formant-motion parity workout needs multiple multi-frame ADD/formant fixtures and a real Zyn ADD note oracle. Avoid letting drum-kit envelope/source failures masquerade as formant evidence. Use Wide Bass, Ghost Ensemble, and Ghost Choir as the initial chorus; all currently fail enough to be useful.","evidence":"Added test-only `tools/zyn-reference/ZynVoiceReference.cpp`, wired it into the MSYS reference build, and added `ZynVoiceReferenceRendererSurveysUpstreamFormantFixturesWhenBuilt`. The batch writes ignored artifacts under `artifacts/parity/zyn-upstream-formant-fixtures/`; baseline scores were Wide Bass 0.280990, Ghost Ensemble 0.312813, Ghost Choir 0.363644. The native Zyn reference build succeeded and the focused formant fixture test passed."} diff --git a/state/map.yaml b/state/map.yaml index 24afe02..2f123d6 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -25,8 +25,8 @@ surfaces: zyn_voice_reference_renderer: tools/zyn-reference/ZynVoiceReference.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-alpha.2 - faust_package: AquaSynth.Faust 0.1.0-alpha.2 + core_package: AquaSynth.Core 0.1.0-alpha.3 + faust_package: AquaSynth.Faust 0.1.0-alpha.3 modules: model: src/AquaSynth.Core/Model.cs parser: src/AquaSynth.Core/PatchScript.cs @@ -56,7 +56,7 @@ boundaries: - script scoring - Faust source generation in AquaSynth.Core - target-code generation, compile-product contracts, dynamic patch compilation policy, cache identity, generated artifact manifests, and Faust toolchain selection in AquaSynth.Faust - - native Faust factory lifetime and offline sample-buffer rendering for engine hosts in AquaSynth.Faust + - live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof in AquaSynth.Faust dev_only: - public-domain DX7 SysEx fixtures - pinned GPL ZynAddSubFX source for parity tests diff --git a/state/memory.json b/state/memory.json index 06418f8..b48a6ab 100644 --- a/state/memory.json +++ b/state/memory.json @@ -49,7 +49,7 @@ "Faust toolchain selection and compile-product contracts", "dynamic patch compilation policy outside the realtime callback", "generated DSP artifact manifests for engine hosting", - "native Faust factory lifetime and offline sample-buffer rendering for engine hosts", + "live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof for engine hosts", "patch script parser", "SFXR atoms and parameter mapping", "analysis/comparison metrics", diff --git a/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs b/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs index 6a85cac..8dd0177 100644 --- a/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs +++ b/tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs @@ -17,7 +17,7 @@ public sealed class NativeFaustRuntimeTests } [Fact] - public void NativeFaustRendererRendersAudiblePatchWhenToolchainIsAvailable() + public void NativeFaustPatchCompilerRendersAudiblePatchWhenToolchainIsAvailable() { const string script = """ voice @@ -29,8 +29,8 @@ public sealed class NativeFaustRuntimeTests decay=0.12 """; - using var session = new AquaSynthRenderSession(); - if (!session.TryRenderScript("native_smoke", script, 1.0f, out var samples, out var error)) + using var compiler = new AquaSynthPatchCompiler(); + if (!compiler.TryCompileScript(new AquaSynthCompileIdentity("native_smoke", "native_smoke", script), out var patch, out var error)) { if (error?.Contains("Faust toolchain not found", StringComparison.OrdinalIgnoreCase) == true || error?.Contains("Faust DLL not found", StringComparison.OrdinalIgnoreCase) == true) @@ -41,8 +41,12 @@ public sealed class NativeFaustRuntimeTests Assert.Fail($"AquaSynth native Faust render failed: {error}"); } - Assert.True(samples.Length > 2048, $"Rendered too few samples: {samples.Length}."); - Assert.Contains(samples, sample => MathF.Abs(sample) > 0.001f); - Assert.InRange(samples.Max(sample => MathF.Abs(sample)), 0.001f, 1.0f); + using (patch) + { + var samples = patch!.Render(1.0f); + Assert.True(samples.Length > 2048, $"Rendered too few samples: {samples.Length}."); + Assert.Contains(samples, sample => MathF.Abs(sample) > 0.001f); + Assert.InRange(samples.Max(sample => MathF.Abs(sample)), 0.001f, 1.0f); + } } }