Add GPL Zyn pad survey fixtures

This commit is contained in:
Meta Vens 2026-05-15 01:22:33 +01:00
commit 2c1684e7a7
7 changed files with 186 additions and 14 deletions

View file

@ -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

View file

@ -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."}

View file

@ -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

View file

@ -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": {

View file

@ -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

View file

@ -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<string>
{
"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");
}
}

View file

@ -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<string>
{
"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<float[]> ReadFloat32Async(string path)
{
var bytes = await File.ReadAllBytesAsync(path);