From 2c1684e7a7c96aa09bc02e73b5fbf61c14d94a03 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 15 May 2026 01:22:33 +0100 Subject: [PATCH] Add GPL Zyn pad survey fixtures --- docs/zynaddsubfx-pressure-survey.md | 28 ++++-- state/evidence.jsonl | 1 + state/map.yaml | 2 + state/memory.json | 2 +- state/spine.yaml | 19 +++- .../ZynInstrumentTests.cs | 52 ++++++++++ .../ZynReferenceParityTests.cs | 96 ++++++++++++++++++- 7 files changed, 186 insertions(+), 14 deletions(-) diff --git a/docs/zynaddsubfx-pressure-survey.md b/docs/zynaddsubfx-pressure-survey.md index 685b0d4..01fc336 100644 --- a/docs/zynaddsubfx-pressure-survey.md +++ b/docs/zynaddsubfx-pressure-survey.md @@ -6,16 +6,17 @@ Find the ZynAddSubFX instruments most likely to break Aquarium's current patch language before spending time on syntax polish. This is a feature-pressure survey, not an audio parity claim. The upstream Zyn -instrument bank is not vendored in this repo because preset-level license and -provenance are not explicit enough at the bank root. The survey records derived -metadata only. +instrument bank is GPL reference feedstock through the pinned Zyn submodule +tree. It is development/test material only: useful for reports, parity renders, +and pressure selection, never package content or Aquarium patch-library stock. ## Source Checked - Repository: `https://github.com/zynaddsubfx/instruments` -- Observed commit: `e9f64a9 Add files via upload` -- Files scanned: `1358` `.xiz` instruments -- Reader result after hardening: `1358` parsed, `0` errors +- Observed commit: `c5c912131b31df5fdf372d2f06a25aaf2375837f` +- Files scanned: `1318` `.xiz` instruments +- Reader result after hardening: `1318` parsed, `0` errors +- Local path: `external/zynaddsubfx/instruments/banks` ## Scoring @@ -38,6 +39,21 @@ thesis. | 220 | `Drums/0001-Drums Kit1.xiz` | Drums Kit | 11 enabled kit items, ADD/SUB mix, 30 envelopes, 11 free envelopes, 15 LFOs, 12 filters. A smaller drum-kit target than Oliver's monster. | | 181 | `olivers-100/0124-Simple Clonewheel.xiz` | Simple Clonewheel | 8 enabled kit items with 7 PAD engines. Best early pressure for layered PAD routing without drum semantics. | +## Upstream PAD Listening Batch + +`ZynPadReferenceRendererSurveysUpstreamGplPadFixturesWhenBuilt` renders a small +GPL PAD batch through the real Zyn oracle when the renderer is built: + +- `Pads/0002-sin2x pad.xiz` +- `Pads/0065-Soft Pad.xiz` +- `Dual/0008-Organ Choir Pad2.xiz` +- `Laba170bank/0098-DoublePadBass.xiz` +- `Companion/0121-Ghost Ensemble.xiz` + +The WAVs and table previews land under +`artifacts/parity/zyn-upstream-pad-fixtures/`. These are listening and parity +fixtures, not library candidates. + ## Pressure Conclusions - **Named layers are real.** Anonymous Aquarium voices can sketch small Zyn diff --git a/state/evidence.jsonl b/state/evidence.jsonl index ce4fc14..e47e9ba 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -71,3 +71,4 @@ {"date":"2026-05-15","area":"zyn-pad-parity","lesson":"The octave-low Zyn PAD mismatch came from conflating spectral table root with note playback frequency. `spectrum root` must own table/base frequency, while `freq` owns the played note. Using Zyn's sample basefreq 77.7813 with playback freq 261.6256, and removing gratuitous spread/vibrato, improved the PAD report to log-mel 0.353365, envelope 0.348752, RMS ratio 0.97283, centroid ratio 0.999243, score 0.612832.","evidence":"PatchScript now preserves spectrum freq as voice note frequency; PatchScriptTests.SpectralBankSeparatesTableRootFromPlaybackFrequency covers the invariant; artifacts/parity/zyn-pad-reference/report.txt records the improved score."} {"date":"2026-05-15","area":"zyn-pad-parity","lesson":"Aqua's remaining detuned impression was caused by stacking same-note PAD spectral voices. Collapse body/high content into one coherent spectral table before adding layers for richness. A simple gain trim then brought loudness into line: RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211.","evidence":"BuiltInScripts.ZynStylePadTexture now uses one active PAD spectrum with partials 1, 1.5, and 2 plus patch gain 1.1; artifacts/parity/zyn-pad-reference/report.txt records the focused render."} {"date":"2026-05-15","area":"zyn-pad-fixtures","lesson":"More PAD fixtures immediately exposed which pressure is real: `pad-pure` and old `pad-texture` render identically through Zyn, so the filter/category XML in `pad-texture` was not live oracle behavior. `pad-harmonic` changes table and note output through explicit OSCIL harmonics and should be the next PAD parity rung.","evidence":"tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored/pad-pure.xiz and pad-harmonic.xiz plus artifacts/parity/zyn-pad-fixtures/report.txt."} +{"date":"2026-05-15","area":"zyn-upstream-gpl-bank","lesson":"GPL Zyn presets are acceptable as test-only feedstock. The upstream instrument submodule at `external/zynaddsubfx/instruments` commit c5c912131b31df5fdf372d2f06a25aaf2375837f provides 1318 `.xiz` files; survey and PAD render tests write ignored reports/WAVs while the NuGet boundary still excludes `.xiz`, `external/`, and artifacts.","evidence":"ZynInstrumentTests.SurveysUpstreamGplInstrumentBankWhenAvailable and ZynReferenceParityTests.ZynPadReferenceRendererSurveysUpstreamGplPadFixturesWhenBuilt; focused Zyn tests passed 14/14."} diff --git a/state/map.yaml b/state/map.yaml index 2b5e5af..3d45e0a 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -17,6 +17,7 @@ surfaces: reference_roadmap: docs/reference-synth-roadmap.md dx7_public_domain_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain zyn_reference_source: external/zynaddsubfx + zyn_upstream_instrument_bank: external/zynaddsubfx/instruments zyn_reference_build_script: tools/zyn-reference/build-zyn-reference.ps1 zyn_pad_reference_renderer: tools/zyn-reference/ZynPadReference.cpp zyn_reference_build_patches: tools/zyn-reference/patches @@ -60,3 +61,4 @@ checks: 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 + zyn_upstream_instrument_bank: nested GPL submodule used only for test/development pressure surveys and oracle renders; never package content diff --git a/state/memory.json b/state/memory.json index f3c66ad..f6bd8bd 100644 --- a/state/memory.json +++ b/state/memory.json @@ -85,7 +85,7 @@ }, "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. The pinned Zyn source now builds as a test-only MSYS2/MinGW oracle through `tools/zyn-reference/build-zyn-reference.ps1`, which patches an ignored artifact source copy, proves `PadNoteTest`, and builds `ZynPadReference`.", - "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 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. Zyn build fixes belong in `tools/zyn-reference/patches/` and apply to ignored artifact copies, not the clean pinned submodule. The PAD oracle now renders both generated PAD sample tables and note audio through Zyn `PADnoteParameters`/`PADnote`; optional parity artifacts live under `artifacts/parity/zyn-pad-reference` and `artifacts/parity/zyn-pad-fixtures`. The first project-authored PAD comparison proved the harness and showed Aquarium `spectrum` was not a close Zyn PAD emulation. User listening identified Zyn as pure wineglass and Aquarium as angry bees. Cutting unearned air/noise/drive/soft-clip from the rebuild, adding interpolated wavetable reads, and raising Aquarium spectral tables to Zyn's 131072-frame PAD size improved the report. A later pitch/drone pass split PAD table root from playback note frequency: `spectrum root=...` owns table/base frequency and `freq=...` owns the played note. Using Zyn's reported sample basefreq 77.7813 while playing 261.6256, removing gratuitous spread/vibrato, collapsing two same-note PAD voices into one coherent spectral table, and trimming gain improved the report to log-mel 0.35669, envelope 0.347572, RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211. The first PAD fixture survey shows `pad-pure` and the old `pad-texture` render identically through Zyn, so the filter/category XML in `pad-texture` is not live pressure for this oracle path. `pad-harmonic` changes the source table and note output through explicit OSCIL harmonics, making it the next real PAD reference rung. The remaining gap is source-table/profile and envelope semantics, not more decorative patch layers. The report now also emits source-table WAVs for listening. 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 named layers and generated table base frequency; playback `freq` remains note authority. 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.", + "lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a nested GPL instrument-bank submodule and may be used as test-only parity/pressure feedstock once provenance is recorded. The ZynAddSubFX engine source is 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. Zyn build fixes belong in `tools/zyn-reference/patches/` and apply to ignored artifact copies, not the clean pinned submodule. The PAD oracle now renders both generated PAD sample tables and note audio through Zyn `PADnoteParameters`/`PADnote`; optional parity artifacts live under `artifacts/parity/zyn-pad-reference` and `artifacts/parity/zyn-pad-fixtures`. The first project-authored PAD comparison proved the harness and showed Aquarium `spectrum` was not a close Zyn PAD emulation. User listening identified Zyn as pure wineglass and Aquarium as angry bees. Cutting unearned air/noise/drive/soft-clip from the rebuild, adding interpolated wavetable reads, and raising Aquarium spectral tables to Zyn's 131072-frame PAD size improved the report. A later pitch/drone pass split PAD table root from playback note frequency: `spectrum root=...` owns table/base frequency and `freq=...` owns the played note. Using Zyn's reported sample basefreq 77.7813 while playing 261.6256, removing gratuitous spread/vibrato, collapsing two same-note PAD voices into one coherent spectral table, and trimming gain improved the report to log-mel 0.35669, envelope 0.347572, RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211. The first PAD fixture survey shows `pad-pure` and the old `pad-texture` render identically through Zyn, so the filter/category XML in `pad-texture` is not live pressure for this oracle path. `pad-harmonic` changes the source table and note output through explicit OSCIL harmonics, making it the next real PAD reference rung. The remaining gap is source-table/profile and envelope semantics, not more decorative patch layers. The report now also emits source-table WAVs for listening. The hardened reader parses the upstream GPL instrument bank at commit c5c912131b31df5fdf372d2f06a25aaf2375837f: 1318/1318 .xiz files; optional tests write a ranked report and render five upstream PAD listening fixtures under ignored artifacts. 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 named layers and generated table base frequency; playback `freq` remains note authority. 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": "Use the Zyn PAD oracle report to isolate whether the main mismatch is source table generation, note playback/gain, envelope shape, or fixture mapping before adding more Zyn DSL surface." }, "note_and_envelope_model": { diff --git a/state/spine.yaml b/state/spine.yaml index 782569d..4511e13 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -24,6 +24,7 @@ current_mechanism: 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_upstream_instrument_bank: external/zynaddsubfx/instruments zyn_reference_build_script: tools/zyn-reference/build-zyn-reference.ps1 zyn_pad_reference_renderer: tools/zyn-reference/ZynPadReference.cpp zyn_reference_reader: src/AquariumSynth.Dsl/ZynInstrument.cs @@ -74,8 +75,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, 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. + - Public-domain reference fixtures, GPL Zyn instrument-bank 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. Upstream GPL `.xiz` instruments may be used as test-only pressure/parity feedstock with provenance reports, while project-authored `.xiz` fixtures remain the clean baseline. The Zyn engine source and instrument bank are 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. @@ -101,7 +102,7 @@ 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. ZynAddSubFX source is pinned under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825 for test-only parity work, and `tools/zyn-reference/build-zyn-reference.ps1` now builds an ignored patched source copy with MSYS2/MinGW, proves `PadNoteTest`, and builds `ZynPadReference`. The optional PAD reports emit note and source-table WAV/listening artifacts. After cutting unearned noise/drive/soft-clip, adding interpolated table reads, raising Aquarium spectral tables to 131072 frames, splitting table root from playback frequency, removing gratuitous spread/vibrato, collapsing same-note PAD voices into one coherent table, and trimming gain, the project-authored PAD report is close by ear and still not formally passing: log-mel 0.35669, envelope 0.347572, RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211. The PAD fixture survey shows `pad-pure` and `pad-texture` render identically through Zyn, while `pad-harmonic` changes the source and becomes the next real rung. 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 pinned under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825 for test-only parity work, and `tools/zyn-reference/build-zyn-reference.ps1` now builds an ignored patched source copy with MSYS2/MinGW, proves `PadNoteTest`, and builds `ZynPadReference`. The optional PAD reports emit note and source-table WAV/listening artifacts. After cutting unearned noise/drive/soft-clip, adding interpolated table reads, raising Aquarium spectral tables to 131072 frames, splitting table root from playback frequency, removing gratuitous spread/vibrato, collapsing same-note PAD voices into one coherent table, and trimming gain, the project-authored PAD report is close by ear and still not formally passing: log-mel 0.35669, envelope 0.347572, RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211. The PAD fixture survey shows `pad-pure` and `pad-texture` render identically through Zyn, while `pad-harmonic` changes the source and becomes the next real rung. The upstream GPL instrument bank is initialized at `external/zynaddsubfx/instruments` commit c5c912131b31df5fdf372d2f06a25aaf2375837f; survey tests parse 1318/1318 .xiz files, write a pressure report, and render a five-patch upstream PAD listening batch under ignored artifacts." first_slice: - "Use the Zyn PAD oracle report to isolate source-table, playback/gain, envelope, and fixture-mapping mismatch before further Zyn DSL golf." - "Use named layers as the owner before promoting richer formant morphing syntax." @@ -167,6 +168,18 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-15 + summary: > + Initialized the upstream GPL ZynAddSubFX instrument bank submodule and + made it explicit test-only feedstock. Added an optional survey test that + parses 1318 `.xiz` instruments and writes + `artifacts/parity/zyn-upstream-bank-survey/report.txt`, plus an optional + oracle render batch for five upstream PAD instruments under + `artifacts/parity/zyn-upstream-pad-fixtures/`. The batch includes + `Pads/0002-sin2x pad.xiz`, `Pads/0065-Soft Pad.xiz`, + `Dual/0008-Organ Choir Pad2.xiz`, `Laba170bank/0098-DoublePadBass.xiz`, + and `Companion/0121-Ghost Ensemble.xiz`. + verification: "dotnet test AquariumSynthCSharp.slnx --no-restore --filter FullyQualifiedName~Zyn: 14 passed" - date: 2026-05-15 summary: > Added more project-authored PAD fixtures and a Zyn-only PAD fixture diff --git a/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs b/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs index dfcba9c..b336132 100644 --- a/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs @@ -147,9 +147,61 @@ public sealed class ZynInstrumentTests Assert.Contains(survey[0].Features, feature => feature.Name == "formant_filter_count" && feature.Value == "1"); } + [Fact] + public async Task SurveysUpstreamGplInstrumentBankWhenAvailable() + { + var root = RepositoryRoot(); + var bankRoot = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks"); + if (!Directory.Exists(bankRoot)) + { + return; + } + + var files = Directory.GetFiles(bankRoot, "*.xiz", SearchOption.AllDirectories); + if (files.Length == 0) + { + return; + } + + var survey = ZynInstrumentSurvey.RankDirectory(bankRoot, take: 20); + Assert.NotEmpty(survey); + Assert.Contains(survey, item => item.Features.Any(feature => feature.Name == "engine_pad" && feature.Value != "0")); + + var artifactDir = Path.Combine(root, "artifacts", "parity", "zyn-upstream-bank-survey"); + Directory.CreateDirectory(artifactDir); + var report = new List + { + "ZynAddSubFX upstream GPL instrument bank survey", + "source: https://github.com/zynaddsubfx/instruments", + "license: GPL test/development corpus via upstream ZynAddSubFX instruments submodule", + "usage: feature pressure and parity fixtures only; never NuGet/package content", + $"bank_root: {bankRoot}", + $"xiz_count: {files.Length}", + "" + }; + foreach (var item in survey) + { + var features = item.Features.ToDictionary(feature => feature.Name, feature => feature.Value); + report.Add($"{item.ComplexityScore}\t{item.Path}\t{item.Name}\tpad={features["engine_pad"]} add={features["engine_add"]} sub={features["engine_sub"]} layers={features["enabled_kit_items"]} env={features["envelope_count"]} free={features["free_envelope_count"]} lfo={features["lfo_count"]} filter={features["filter_count"]} formant={features["formant_filter_count"]} fx={features["effect_count"]}"); + } + + await File.WriteAllLinesAsync(Path.Combine(artifactDir, "report.txt"), report); + } + private static void AssertFeature(ZynInstrument instrument, string name, string value) => Assert.Contains(instrument.Features(), feature => feature.Name == name && feature.Value == value); private static string FixturePath(params string[] parts) => Path.Combine([AppContext.BaseDirectory, "Fixtures", .. parts]); + + 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"); + } } diff --git a/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs b/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs index cddbf7d..81ce5e8 100644 --- a/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs @@ -146,9 +146,9 @@ public sealed class ZynReferenceParityTests var tableRaw = Path.Combine(artifactDir, $"{name}-table0.f32"); var tableWav = Path.Combine(artifactDir, $"{name}-table0.wav"); - var note = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, noteRaw, "note", "261.6256", "1.5")]); + var note = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, noteRaw, 0, "note", "261.6256", "1.5")]); Assert.Equal(0, note.ExitCode); - var table = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, tableRaw, "0")]); + var table = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, tableRaw, 0, "0")]); Assert.Equal(0, table.ExitCode); var noteSamples = await ReadFloat32Async(noteRaw); @@ -174,6 +174,88 @@ public sealed class ZynReferenceParityTests await File.WriteAllLinesAsync(Path.Combine(artifactDir, "report.txt"), report); } + [Fact] + public async Task ZynPadReferenceRendererSurveysUpstreamGplPadFixturesWhenBuilt() + { + var root = RepositoryRoot(); + var bash = @"C:\msys64\usr\bin\bash.exe"; + var renderer = Path.Combine(root, "artifacts", "zyn-reference-build-msys", "src", "Tests", "ZynPadReference.exe"); + var bankRoot = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks"); + if (!File.Exists(bash) || !File.Exists(renderer) || !Directory.Exists(bankRoot)) + { + return; + } + + var selected = new[] + { + Path.Combine("Pads", "0002-sin2x pad.xiz"), + Path.Combine("Pads", "0065-Soft Pad.xiz"), + Path.Combine("Dual", "0008-Organ Choir Pad2.xiz"), + Path.Combine("Laba170bank", "0098-DoublePadBass.xiz"), + Path.Combine("Companion", "0121-Ghost Ensemble.xiz") + }; + var artifactDir = Path.Combine(root, "artifacts", "parity", "zyn-upstream-pad-fixtures"); + Directory.CreateDirectory(artifactDir); + + var report = new List + { + "Zyn upstream GPL PAD fixture survey", + "source: https://github.com/zynaddsubfx/instruments", + "license: GPL test/development corpus via upstream ZynAddSubFX instruments submodule", + $"renderer: {renderer}" + }; + + foreach (var relative in selected) + { + var input = Path.Combine(bankRoot, relative); + if (!File.Exists(input)) + { + continue; + } + + var instrument = ZynInstrumentReader.ParseFile(input); + var kitIndex = instrument.KitItems.FirstOrDefault(item => item.Enabled && item.Engines.Contains(ZynEngine.PadSynth))?.Id ?? -1; + if (kitIndex < 0) + { + continue; + } + + var name = SanitizeArtifactName(Path.GetFileNameWithoutExtension(input)); + var noteRaw = Path.Combine(artifactDir, $"{name}-zyn.f32"); + var noteWav = Path.Combine(artifactDir, $"{name}-zyn.wav"); + var tableRaw = Path.Combine(artifactDir, $"{name}-table0.f32"); + var tableWav = Path.Combine(artifactDir, $"{name}-table0.wav"); + + var note = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, noteRaw, kitIndex, "note", "261.6256", "1.5")]); + Assert.Equal(0, note.ExitCode); + var table = await RunAsync(bash, ["-lc", ZynPadCommand(root, input, tableRaw, kitIndex, "0")]); + Assert.Equal(0, table.ExitCode); + + var noteSamples = await ReadFloat32Async(noteRaw); + var tableSamples = await ReadFloat32Async(tableRaw); + Assert.True(noteSamples.Length > 0); + Assert.True(tableSamples.Length > noteSamples.Length); + WriteWav(noteWav, noteSamples, 44100, 0.9f); + WriteWav(tableWav, tableSamples, 44100, 0.9f); + + var features = AudioAnalyzer.AnalyzeAudio(noteSamples).Features; + report.Add(""); + report.Add($"fixture: {relative}"); + report.Add($"instrument: {instrument.Name}"); + report.Add($"kit_index: {kitIndex}"); + report.Add($"note_stdout: {note.Stdout.Trim()}"); + report.Add($"table_stdout: {table.Stdout.Trim()}"); + report.Add($"peak: {Peak(noteSamples):0.######}"); + report.Add($"rms: {Rms(noteSamples):0.######}"); + report.Add($"duration: {features.DurationSeconds:0.######}"); + report.Add($"centroid: {features.SpectralCentroidHz:0.######}"); + report.Add($"table_peak: {Peak(tableSamples):0.######}"); + report.Add($"table_rms: {Rms(tableSamples):0.######}"); + } + + await File.WriteAllLinesAsync(Path.Combine(artifactDir, "report.txt"), report); + } + private static string RepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); @@ -217,17 +299,23 @@ public sealed class ZynReferenceParityTests private static string BashQuote(string value) => "'" + value.Replace("'", "'\\''") + "'"; - private static string ZynPadCommand(string root, string input, string output, params string[] modeArguments) => + private static string ZynPadCommand(string root, string input, string output, int kitIndex, params string[] modeArguments) => string.Join(' ', [ "export PATH=/mingw64/bin:/usr/bin:/bin:$PATH;", $"cd {BashQuote(ToMsysPath(root))};", "./artifacts/zyn-reference-build-msys/src/Tests/ZynPadReference.exe", BashQuote(ToMsysPath(input)), BashQuote(ToMsysPath(output)), - "0", + kitIndex.ToString(System.Globalization.CultureInfo.InvariantCulture), ..modeArguments ]); + private static string SanitizeArtifactName(string name) + { + var chars = name.Select(ch => char.IsLetterOrDigit(ch) ? char.ToLowerInvariant(ch) : '-').ToArray(); + return string.Join('-', new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries)); + } + private static async Task ReadFloat32Async(string path) { var bytes = await File.ReadAllBytesAsync(path);