diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5ec6a0b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/zynaddsubfx"] + path = external/zynaddsubfx + url = https://github.com/zynaddsubfx/zynaddsubfx diff --git a/README.md b/README.md index 016ebed..bfbf442 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ DX7 reference-render tests use `dexed-py` when a Python with `dexed` and specific runtime; otherwise the tests skip the render path and still parse the vendored public-domain fixture. +ZynAddSubFX is pinned as a GPL test/development reference under +`external/zynaddsubfx`. Initialize it with: + +```powershell +git submodule update --init --recursive +``` + +That source is a parity oracle only. Aquarium does not ship Zyn code, link it +from `AquariumSynth.Dsl`, or treat it as runtime machinery. + ## Package Boundary Aquarium consumes this library as a pinned `AquariumSynth.Dsl` NuGet package, @@ -30,8 +40,9 @@ version is packed and tested. dotnet pack src\AquariumSynth.Dsl\AquariumSynth.Dsl.csproj -c Release ``` -The test suite verifies that development fixtures, Python render helpers, and -SysEx banks do not ship in `AquariumSynth.Dsl.nupkg`. +The test suite verifies that development fixtures, Python render helpers, +external reference synth sources, and SysEx banks do not ship in +`AquariumSynth.Dsl.nupkg`. ## Shape diff --git a/docs/reference-synth-roadmap.md b/docs/reference-synth-roadmap.md index 2d38722..bf8c92a 100644 --- a/docs/reference-synth-roadmap.md +++ b/docs/reference-synth-roadmap.md @@ -41,6 +41,9 @@ credible parity test path should not be consumed. - DX7 reference audio uses `dexed-py` when available, with public-domain SysEx fixtures stored under the test project. These are development references, not package content. +- ZynAddSubFX source is pinned under `external/zynaddsubfx` as GPL + test/development material. It may be compiled or wrapped only for parity + tests; Aquarium must not ship, link, or runtime-depend on it. - Do not add a generic graph escape hatch until a specific family of references forces it. - When a reference target exceeds the current voice-centric DSL, document the @@ -126,7 +129,10 @@ First deliverable: curves, fuller kit/effect routing, and richer formant/vowel morphing remain pressure. -Do not vendor the upstream Zyn instrument-bank submodule until its preset +The upstream ZynAddSubFX engine source is pinned as `external/zynaddsubfx` +for parity tests, currently at `3ab608c432996ba4d582176572c0b0f82328c825`. +That checkout is GPL reference machinery, not Aquarium implementation. Do not +vendor the upstream Zyn instrument-bank submodule until its preset license/provenance is explicit enough for this repo. Use project-authored fixtures for parser shape and only promote external fixtures with clean source notes. diff --git a/external/zynaddsubfx b/external/zynaddsubfx new file mode 160000 index 0000000..3ab608c --- /dev/null +++ b/external/zynaddsubfx @@ -0,0 +1 @@ +Subproject commit 3ab608c432996ba4d582176572c0b0f82328c825 diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0ab7e15..30809e2 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -64,3 +64,4 @@ {"kind":"harmonic_bank_language_golf","lesson":"Additive `harmonics` syntax now records authored ratio/gain partial banks under named layers and lowers them to ordinary Aquarium voices. This generalizes additive lead and future drawbar/partial-bank targets without hiding Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics behind a fake parity claim.","source":"Zyn harmonic-bank DSL slice on 2026-05-14"} {"kind":"voice_rate_level_envelopes","lesson":"The existing readable `env=rl rates=... levels=... curves=... gate=...` contour now applies to normal voices and therefore to named layer defaults, not only operator graphs. This gives PAD and vocal Zyn rebuilds staged non-ADSR onset/release shape while preserving the invariant that Note owns gate timing and envelope data owns level shape. Arbitrary Zyn free-envelope point curves remain missing pressure.","source":"Zyn staged voice envelope DSL slice on 2026-05-14"} {"kind":"pad_spectral_cloud","lesson":"PAD-style layers now have `spectrum layer=... root=... spread=... partials=ratio:gain,...`, recorded as `SpectralBank` and generated as PADsynth-style FFT tables: frequency-domain harmonic spreading, deterministic random phase, one inverse FFT, normalization, then Faust waveform/rdtable playback before ordinary voice treatment. This gives PAD texture rebuilds a real spectral source authority without inflating the voice graph; roots/spreads/partials are authoring-time table shape, not runtime controls. It still does not claim full Zyn PAD harmonic-profile/randomness/pitch-zone behavior.","source":"Zyn PAD spectral-cloud DSL slice on 2026-05-14"} +{"kind":"zyn_reference_oracle","lesson":"ZynAddSubFX source is pinned as GPL test/development material under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825. Treat it like dexed-py: a parity oracle for tests and artifacts, not Aquarium implementation, runtime dependency, or NuGet package content. Before further Zyn DSL golf, build a test-only PAD renderer/wrapper from this source and compare Aquarium rebuilds against actual Zyn behavior.","source":"Zyn source pinning for parity on 2026-05-14"} diff --git a/state/map.yaml b/state/map.yaml index efc2257..4b00349 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -16,6 +16,7 @@ surfaces: tests: tests/AquariumSynth.Dsl.Tests reference_roadmap: docs/reference-synth-roadmap.md dx7_public_domain_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain + zyn_reference_source: external/zynaddsubfx zyn_project_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored package: AquariumSynth.Dsl 0.1.0-aquarium.1 modules: @@ -46,6 +47,7 @@ boundaries: - Faust source generation, target-code generation, and candidate rendering dev_only: - public-domain DX7 SysEx fixtures + - pinned GPL ZynAddSubFX source for parity tests - project-authored ZynAddSubFX .xiz fixtures - dexed-py reference renderer - structured .aqua patch library @@ -54,3 +56,4 @@ checks: package: dotnet pack src/AquariumSynth.Dsl/AquariumSynth.Dsl.csproj -c Release faust_validation: automatic in tests when Faust is installed dexed_py_reference: optional in tests when AQUARIUM_DX7_PYTHON or a Python with dexed-py is available + zyn_reference_source: pinned submodule used only for parity tests; never package content diff --git a/state/memory.json b/state/memory.json index bbb1f9b..603e1c5 100644 --- a/state/memory.json +++ b/state/memory.json @@ -85,8 +85,8 @@ }, "zyn_reference_path": { "status": "Initial ZynInstrumentReader parses plain XML and gzip-compressed .xiz-shaped XML from project-authored fixtures into ZynInstrument records and ReferencePatch features. First Zyn-style Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer. `ZynInstrumentSurvey` can rank .xiz directories by feature pressure. Named `layer` declarations provide the first Zyn-driven DSL abstraction; additive `harmonics`, PAD `spectrum`, and staged `env=rl` voice envelopes now attach to those layers.", - "lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The hardened reader now parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. The worst target is `olivers-100/0032-Drum Kit.xiz`, with 16 enabled kit items, mixed ADD/SUB/PAD engines, 106 envelopes, 25 free envelopes, 73 LFOs, 37 filters, and 3 formant filters. The first rebuilds show Aquarium can sketch small Zyn fixtures with existing voices/macros/formants/noise, and the upstream survey correctly pushed named kit/layer routing first. `layer` owns name, engine tag, optional key range metadata, default gain, and effect-send labels while lowering to ordinary voices. `harmonics` owns authored ratio/gain partial banks under those layers. `spectrum` owns PAD-style root/spread/partial clouds under those layers and generates PADsynth-style FFT wavetables with frequency-domain spreading, deterministic random phase, and one inverse FFT before ordinary voice treatment. Staged `env=rl` now applies to voices as well as operators, preserving Note as gate authority and envelope data as level-shape authority. Aquarium still does not claim Zyn ADDsynth phase/bandwidth details, full Zyn PAD harmonic-profile/randomness/pitch-zone behavior, arbitrary free-envelope point curves, or richer formant morphing.", - "next": "Choose the next syntax that attaches to named layers: richer formant/vowel morphing. Prove it across more than one target." + "lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The ZynAddSubFX engine source is now pinned as GPL test/development material under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825; use it as a parity oracle only, never as Aquarium runtime or NuGet package content. The hardened reader parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. The worst target is `olivers-100/0032-Drum Kit.xiz`, with 16 enabled kit items, mixed ADD/SUB/PAD engines, 106 envelopes, 25 free envelopes, 73 LFOs, 37 filters, and 3 formant filters. The first rebuilds show Aquarium can sketch small Zyn fixtures with existing voices/macros/formants/noise, and the upstream survey correctly pushed named kit/layer routing first. `layer` owns name, engine tag, optional key range metadata, default gain, and effect-send labels while lowering to ordinary voices. `harmonics` owns authored ratio/gain partial banks under those layers. `spectrum` owns PAD-style root/spread/partial clouds under those layers and generates PADsynth-style FFT wavetables with frequency-domain spreading, deterministic random phase, and one inverse FFT before ordinary voice treatment. Staged `env=rl` now applies to voices as well as operators, preserving Note as gate authority and envelope data as level-shape authority. Aquarium still does not claim Zyn ADDsynth phase/bandwidth details, full Zyn PAD harmonic-profile/randomness/pitch-zone behavior, arbitrary free-envelope point curves, or richer formant morphing.", + "next": "Build a test-only Zyn PAD reference renderer/wrapper from the pinned source before further Zyn DSL golf." }, "note_and_envelope_model": { "rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.", @@ -101,7 +101,7 @@ }, "last_verified": { "command": "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore", - "result": "94 passed; PAD spectral-cloud DSL now uses PADsynth-style FFT table generation without regressing DX7 parity", - "commit": "pending Zyn PAD FFT spectral tables" + "result": "95 passed; Zyn source is pinned as test-only parity oracle and package boundary remains clean", + "commit": "pending Zyn reference oracle pin" } } diff --git a/state/scratch.md b/state/scratch.md index 686172b..f744680 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -12,6 +12,23 @@ Doctrine update: Aquarium can reproduce the behavior in terse, readable syntax. - Parsing a synth format is inventory. Rebuilding and testing behavior is the proof. +- Pinned reference synths are test-only oracles, not Aquarium organs. ZynAddSubFX + now lives under `external/zynaddsubfx` as GPL development material pinned at + `3ab608c432996ba4d582176572c0b0f82328c825`; it must not enter the NuGet + package or runtime dependency graph. + +Current slice: + +- Added ZynAddSubFX as a submodule at `external/zynaddsubfx`. +- Added guard tests proving the pinned Zyn source exists, keeps its GPL license, + exposes the PAD implementation files, and is fixed to the expected commit. +- Tightened the package-boundary test so `external/`, C++ sources, and headers + do not ship in `AquariumSynth.Dsl.nupkg`. +- Full verification: + `AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore`: + 95 passed. +- Next coherent cut: build a test-only Zyn PAD reference renderer/wrapper from + the pinned source and compare it to the Aquarium `spectrum` rebuild. Completed this slice: diff --git a/state/spine.yaml b/state/spine.yaml index ef665bb..be387af 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -23,6 +23,7 @@ current_mechanism: dx7_references: src/AquariumSynth.Dsl/Dx7SysEx.cs dx7_reference_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain dx7_reference_renderer_tests: tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs + zyn_reference_source: external/zynaddsubfx zyn_reference_reader: src/AquariumSynth.Dsl/ZynInstrument.cs zyn_project_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored reference_rebuilds: src/AquariumSynth.Dsl/ReferenceRebuilds.cs @@ -71,8 +72,8 @@ live_invariants: - Do not replace applied-envelope release with a naive traced threshold duration; it shortened releases too aggressively and broke broader DX7 parity. - Recompile only for graph shape changes; vary declared parameters through the hosted DSP control API. - Reference fixtures must preserve provenance, license status, source hash, and feature pressure. - - Public-domain reference fixtures and dexed-py helpers are test/development material only; the NuGet package must ship only AquariumSynth.Dsl. - - ZynAddSubFX import starts as feature extraction, not translation. Use project-authored `.xiz` fixtures until external preset-bank license/provenance is explicit. + - Public-domain reference fixtures, dexed-py helpers, and pinned ZynAddSubFX source are test/development material only; the NuGet package must ship only AquariumSynth.Dsl. + - ZynAddSubFX import starts as feature extraction, not translation. Use project-authored `.xiz` fixtures until external preset-bank license/provenance is explicit. The Zyn engine source is pinned under `external/zynaddsubfx` for parity oracles only, not Aquarium runtime code. - Named `layer` declarations own Zyn-style kit/layer identity: engine tag, optional key range metadata, default gain, and effect-send labels. They lower to ordinary voices for now; do not smuggle PAD/free-envelope/effect behavior into them. - Additive `harmonics` declarations own authored ratio/gain partial banks under named layers. They lower to ordinary voices for now; do not pretend this covers Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics. - Normal voices may use staged `env=rl` rate/level envelopes. Notes still own gate timing; envelopes own level shape. This is not arbitrary Zyn free-envelope point editing. @@ -98,10 +99,11 @@ reference_targets: priority: 2 purpose: "Additive, subtractive, PAD, formant, kit, and layered-instrument pressure test." source: "https://github.com/zynaddsubfx/zynaddsubfx" - status: "Initial ZynInstrumentReader reads plain or gzip-compressed .xiz XML from project-authored fixtures and classifies active ADD/SUB/PAD engines, enabled kit items, layering, envelopes, free envelopes, LFOs, filters, formants, and effects as ReferencePatch features. First Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer and now use named `layer` declarations. Additive rebuilds can use `harmonics` banks attached to layers, PAD rebuilds can use `spectrum` PADsynth-style FFT wavetable banks attached to layers, and PAD/vocal rebuilds use staged voice `env=rl` envelopes through layer defaults. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first owner abstraction." + status: "Initial ZynInstrumentReader reads plain or gzip-compressed .xiz XML from project-authored fixtures and classifies active ADD/SUB/PAD engines, enabled kit items, layering, envelopes, free envelopes, LFOs, filters, formants, and effects as ReferencePatch features. First Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer and now use named `layer` declarations. Additive rebuilds can use `harmonics` banks attached to layers, PAD rebuilds can use `spectrum` PADsynth-style FFT wavetable banks attached to layers, and PAD/vocal rebuilds use staged voice `env=rl` envelopes through layer defaults. ZynAddSubFX source is now pinned under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825 for test-only parity work. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first owner abstraction." first_slice: + - "Build a test-only Zyn PAD reference renderer/wrapper from the pinned source before further Zyn DSL golf." - "Use named layers as the owner before promoting richer formant morphing syntax." - - "Additive harmonic banks, PAD spectral clouds, and staged voice envelopes now exist; prove any next syntax across more than one target." + - "Additive harmonic banks, PAD spectral clouds, and staged voice envelopes now exist; prove any next syntax across more than one target with parity, not parser-only fixtures." - "Do not add all remaining pressures at once." faust: priority: 3 @@ -150,6 +152,16 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-14 + summary: > + Pinned ZynAddSubFX source as a GPL test/development reference under + `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825. + Added guard tests proving the source, license, PAD implementation files, + and commit are present, and tightened the package-boundary test so + `external/`, C++ sources, and headers do not ship in + `AquariumSynth.Dsl.nupkg`. This establishes Zyn as a parity oracle, not + Aquarium implementation. + verification: "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore: 95 passed" - date: 2026-05-14 summary: > Added PAD spectral-cloud syntax for named layers. `spectrum diff --git a/tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs b/tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs index 710f801..7164879 100644 --- a/tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs @@ -31,6 +31,9 @@ public sealed class PackageBoundaryTests Assert.DoesNotContain(entries, entry => entry.EndsWith(".syx", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(entries, entry => entry.EndsWith(".xiz", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(entries, entry => entry.EndsWith(".py", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(entries, entry => entry.StartsWith("external/", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(entries, entry => entry.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(entries, entry => entry.EndsWith(".h", StringComparison.OrdinalIgnoreCase)); } finally { diff --git a/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs b/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs new file mode 100644 index 0000000..b5b174f --- /dev/null +++ b/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs @@ -0,0 +1,53 @@ +using System.Diagnostics; + +namespace AquariumSynth.Dsl.Tests; + +public sealed class ZynReferenceParityTests +{ + [Fact] + public async Task ZynAddSubFxReferenceSourceIsPinnedForTestOnlyParity() + { + var root = RepositoryRoot(); + var zynRoot = Path.Combine(root, "external", "zynaddsubfx"); + + Assert.True(Directory.Exists(zynRoot), "ZynAddSubFX reference source submodule is missing. Run `git submodule update --init --recursive`."); + Assert.True(File.Exists(Path.Combine(zynRoot, "COPYING")), "ZynAddSubFX reference source should keep its GPL license file."); + Assert.True(File.Exists(Path.Combine(zynRoot, "src", "Params", "PADnoteParameters.cpp")), "PAD parameter implementation is the parity source, not Aquarium runtime code."); + Assert.True(File.Exists(Path.Combine(zynRoot, "src", "Synth", "PADnote.cpp")), "PAD note implementation is the parity source, not Aquarium runtime code."); + Assert.Contains("GNU GENERAL PUBLIC LICENSE", await File.ReadAllTextAsync(Path.Combine(zynRoot, "COPYING"))); + + var revision = await RunAsync("git", ["-C", zynRoot, "rev-parse", "HEAD"]); + Assert.Equal(0, revision.ExitCode); + Assert.Equal("3ab608c432996ba4d582176572c0b0f82328c825", revision.Stdout.Trim()); + } + + private static string RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AquariumSynthCSharp.slnx"))) + { + directory = directory.Parent; + } + + return directory?.FullName ?? throw new InvalidOperationException("could not find repository root"); + } + + private static async Task<(int ExitCode, string Stdout, string Stderr)> RunAsync( + string fileName, + IReadOnlyList arguments) + { + var start = new ProcessStartInfo(fileName) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + foreach (var argument in arguments) start.ArgumentList.Add(argument); + + using var process = Process.Start(start) ?? throw new InvalidOperationException($"failed to start `{fileName}`"); + var stdout = process.StandardOutput.ReadToEndAsync(); + var stderr = process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + return (process.ExitCode, await stdout, await stderr); + } +}