Map Zyn coverage pressure

This commit is contained in:
Meta Vens 2026-05-16 10:05:54 +01:00
commit 15c72e2721
7 changed files with 565 additions and 2 deletions

View file

@ -75,6 +75,36 @@ library candidates.
- **Drum kits should not be first golf targets.** They are the best stress test
for the model, but probably too broad for the first new syntax slice.
## Coverage Map
`ZynInstrumentSurvey.CoverageDirectory` now emits a handled/counted/unknown
coverage report instead of only ranking instruments by aggregate pressure. The
upstream GPL bank report is written to
`artifacts/parity/zyn-upstream-bank-survey/coverage.md` by
`SurveysUpstreamGplInstrumentBankWhenAvailable`.
Current upstream bank coverage:
- Files parsed: `1318/1318`
- Enabled kit items: `1459`
- Layered instruments: `63`
- Handled with parity-backed or native AquaSynth coverage: PAD layers, named
kit layers, PAD table modes/bandwidth, PAD source-table extraction for the
implemented OscilGen subset, analog PAD LP/HP cutoff/order/Q/envelopes, and
staged non-free envelopes.
- Counted but not translated: ADD, SUB, mixed-engine kits, free envelopes,
active LFO routing, formant filters/motion, and effects.
- Unknown PAD filter pressure: non-analog or unimplemented PAD global filter
categories/types such as `cat=1 type=0`, `cat=2 type=0`, `cat=2 type=2`,
`cat=0 type=4`, `cat=0 type=6`, and smaller buckets.
- Unknown PAD OscilGen pressure: unimplemented source filter types
`10`, `12`, `11`, `4`, `5`, `3`, and `8`; unimplemented waveshapers
`2`, `14`, `5`, `6`, `3`, `13`, `8`, and `9`.
This map is not a clone checklist. It is a workout list. A bucket graduates from
counted or unknown only when a parity target proves which AquaSynth authority
should own the behavior.
## Recommended Next Cut
Start with **named layer routing**, not PAD synthesis.

View file

@ -73,6 +73,35 @@ public sealed record ZynInstrumentSurveyItem(
int ComplexityScore,
IReadOnlyList<ReferenceFeature> Features);
public enum ZynCoverageStatus
{
Handled,
Counted,
Unknown
}
public sealed record ZynCoverageBucket(
string Area,
string Key,
ZynCoverageStatus Status,
int Count,
IReadOnlyList<string> Examples,
string Notes);
public sealed record ZynCoverageReport(
int FileCount,
int ParsedFileCount,
int EnabledKitItemCount,
int LayeredInstrumentCount,
IReadOnlyList<ZynCoverageBucket> Buckets)
{
public IReadOnlyList<ZynCoverageBucket> UnknownBuckets =>
Buckets.Where(bucket => bucket.Status == ZynCoverageStatus.Unknown).ToList();
public IReadOnlyList<ZynCoverageBucket> CountedBuckets =>
Buckets.Where(bucket => bucket.Status == ZynCoverageStatus.Counted).ToList();
}
public sealed record ZynPadRebuild(
string InstrumentName,
int KitItemId,
@ -412,6 +441,9 @@ public static class ZynInstrumentReader
private static XDocument ParseDocument(ReadOnlySpan<byte> bytes) =>
XDocument.Parse(XmlText(bytes).TrimStart(), LoadOptions.None);
internal static XDocument ParseDocumentForSurvey(ReadOnlySpan<byte> bytes) =>
ParseDocument(bytes);
private static int CountElements(XElement root, Func<string, bool> predicate) =>
root.Descendants().Count(element => predicate(element.Name.LocalName));
@ -1309,6 +1341,34 @@ public static class ZynInstrumentReader
public static class ZynInstrumentSurvey
{
private static readonly HashSet<string> HandledPadOscilFilters = new(StringComparer.OrdinalIgnoreCase)
{
"0",
"1",
"2",
"6",
"7",
"13"
};
private static readonly HashSet<string> HandledPadWaveShapes = new(StringComparer.OrdinalIgnoreCase)
{
"0",
"1",
"4",
"7",
"11",
"15"
};
private static readonly HashSet<string> HandledPadGlobalFilters = new(StringComparer.OrdinalIgnoreCase)
{
"cat=0 type=0",
"cat=0 type=1",
"cat=0 type=2",
"cat=0 type=3"
};
public static IReadOnlyList<ZynInstrumentSurveyItem> RankDirectory(string root, int take = 25)
{
return Directory.GetFiles(root, "*.xiz", SearchOption.AllDirectories)
@ -1346,4 +1406,380 @@ public static class ZynInstrumentSurvey
int Int(string name) => int.TryParse(byName.GetValueOrDefault(name), out var value) ? value : 0;
}
public static ZynCoverageReport CoverageDirectory(string root, int examplesPerBucket = 3)
{
if (examplesPerBucket < 1) throw new ArgumentOutOfRangeException(nameof(examplesPerBucket));
var files = Directory.GetFiles(root, "*.xiz", SearchOption.AllDirectories);
var buckets = new Dictionary<(string Area, string Key), CoverageAccumulator>();
var parsed = 0;
var enabledItems = 0;
var layeredInstruments = 0;
foreach (var file in files)
{
XDocument document;
try
{
document = ZynInstrumentReader.ParseDocumentForSurvey(File.ReadAllBytes(file));
parsed++;
}
catch
{
AddBucket(
buckets,
"file",
"parse_error",
ZynCoverageStatus.Unknown,
Path.GetRelativePath(root, file),
"The survey could not parse this .xiz file.");
continue;
}
var relativePath = Path.GetRelativePath(root, file);
var instrument = document.Descendants("INSTRUMENT").FirstOrDefault();
if (instrument is null)
{
AddBucket(
buckets,
"file",
"missing_instrument",
ZynCoverageStatus.Unknown,
relativePath,
"The .xiz file has no INSTRUMENT block.");
continue;
}
var enabledInFile = 0;
foreach (var item in instrument.Descendants("INSTRUMENT_KIT_ITEM"))
{
if (!BoolParam(item, "enabled"))
{
continue;
}
enabledItems++;
enabledInFile++;
var engines = EnabledEngines(item).ToList();
AddEngineBuckets(buckets, engines, relativePath);
AddElementPressureBuckets(buckets, item, relativePath);
if (engines.Contains(ZynEngine.PadSynth))
{
AddPadPressureBuckets(buckets, item, relativePath);
}
}
if (enabledInFile > 1)
{
layeredInstruments++;
AddBucket(
buckets,
"kit",
"multi_item_layering",
ZynCoverageStatus.Handled,
relativePath,
"Named layer declarations own kit/layer identity; richer routing still needs target-specific parity.");
}
}
return new ZynCoverageReport(
files.Length,
parsed,
enabledItems,
layeredInstruments,
buckets.Values
.Select(bucket => bucket.ToBucket(examplesPerBucket))
.OrderBy(bucket => bucket.Status)
.ThenBy(bucket => bucket.Area, StringComparer.OrdinalIgnoreCase)
.ThenByDescending(bucket => bucket.Count)
.ThenBy(bucket => bucket.Key, StringComparer.OrdinalIgnoreCase)
.ToList());
}
public static string CoverageMarkdown(ZynCoverageReport report)
{
var builder = new StringBuilder();
builder.AppendLine("# ZynAddSubFX Coverage Survey");
builder.AppendLine();
builder.AppendLine($"Files: {report.FileCount}");
builder.AppendLine($"Parsed: {report.ParsedFileCount}");
builder.AppendLine($"Enabled kit items: {report.EnabledKitItemCount}");
builder.AppendLine($"Layered instruments: {report.LayeredInstrumentCount}");
builder.AppendLine();
foreach (var status in new[] { ZynCoverageStatus.Unknown, ZynCoverageStatus.Counted, ZynCoverageStatus.Handled })
{
builder.AppendLine($"## {status}");
builder.AppendLine();
foreach (var bucket in report.Buckets.Where(bucket => bucket.Status == status))
{
var examples = bucket.Examples.Count == 0 ? "" : $" Examples: {string.Join("; ", bucket.Examples)}.";
builder.AppendLine($"- `{bucket.Area}:{bucket.Key}` count={bucket.Count}. {bucket.Notes}{examples}");
}
builder.AppendLine();
}
return builder.ToString();
}
private static void AddEngineBuckets(
Dictionary<(string Area, string Key), CoverageAccumulator> buckets,
IReadOnlyList<ZynEngine> engines,
string relativePath)
{
if (engines.Count == 0)
{
AddBucket(buckets, "engine", "none", ZynCoverageStatus.Unknown, relativePath, "Enabled kit item has no active ADD/SUB/PAD engine.");
return;
}
foreach (var engine in engines)
{
var (key, status, notes) = engine switch
{
ZynEngine.PadSynth => ("pad", ZynCoverageStatus.Handled, "PAD layers have generated spectrum rebuilds and parity pressure."),
ZynEngine.AddSynth => ("add", ZynCoverageStatus.Counted, "ADD is counted and hand-sketched in rebuilds, but not parity-translated from Zyn."),
ZynEngine.SubSynth => ("sub", ZynCoverageStatus.Counted, "SUB is counted and hand-sketched in rebuilds, but not parity-translated from Zyn."),
_ => ("unknown", ZynCoverageStatus.Unknown, "Unknown Zyn engine.")
};
AddBucket(buckets, "engine", key, status, relativePath, notes);
}
if (engines.Count > 1)
{
AddBucket(
buckets,
"engine",
string.Join("+", engines.Select(EngineKey).Order(StringComparer.OrdinalIgnoreCase)),
ZynCoverageStatus.Counted,
relativePath,
"Mixed engines are counted; only PAD members are generated by the current parity translator.");
}
}
private static void AddElementPressureBuckets(
Dictionary<(string Area, string Key), CoverageAccumulator> buckets,
XElement item,
string relativePath)
{
foreach (var envelope in item.Descendants().Where(element => element.Name.LocalName.EndsWith("_ENVELOPE", StringComparison.OrdinalIgnoreCase)))
{
var free = BoolParam(envelope, "free_mode");
AddBucket(
buckets,
"envelope",
free ? $"{envelope.Name.LocalName}.free" : $"{envelope.Name.LocalName}.staged",
free ? ZynCoverageStatus.Counted : ZynCoverageStatus.Handled,
relativePath,
free
? "Free-mode envelopes are counted pressure; AquaSynth has staged rate/level contours, not arbitrary Zyn point envelopes."
: "Standard ADSR-ish Zyn envelopes can be approximated by AquaSynth staged rate/level or ADSR surfaces.");
}
foreach (var lfo in item.Descendants().Where(element => element.Name.LocalName.EndsWith("_LFO", StringComparison.OrdinalIgnoreCase)))
{
var intensity = IntParam(lfo, "intensity", 0);
var status = intensity == 0 ? ZynCoverageStatus.Handled : ZynCoverageStatus.Counted;
AddBucket(
buckets,
"lfo",
lfo.Name.LocalName,
status,
relativePath,
intensity == 0
? "Inactive or neutral LFO block."
: "LFO presence is counted; Zyn LFO routing and waveform semantics are not parity-translated.");
}
foreach (var formant in item.Descendants("FORMANT_FILTER"))
{
AddBucket(
buckets,
"formant",
"FORMANT_FILTER",
ZynCoverageStatus.Counted,
relativePath,
"Static formant intent can be sketched; Zyn vowel/formant morphing remains parity pressure.");
}
foreach (var effect in item.Descendants().Where(element => element.Name.LocalName.Contains("EFFECT", StringComparison.OrdinalIgnoreCase)))
{
AddBucket(
buckets,
"effect",
effect.Name.LocalName,
ZynCoverageStatus.Counted,
relativePath,
"Effects are inventory only; AquaSynth does not translate Zyn effect chains.");
}
}
private static void AddPadPressureBuckets(
Dictionary<(string Area, string Key), CoverageAccumulator> buckets,
XElement item,
string relativePath)
{
var pad = item.Element("PAD_SYNTH_PARAMETERS");
if (pad is null)
{
AddBucket(buckets, "pad", "missing_parameters", ZynCoverageStatus.Unknown, relativePath, "PAD engine is enabled but PAD_SYNTH_PARAMETERS is absent.");
return;
}
AddBucket(buckets, "pad", $"mode={IntParam(pad, "mode", 0)}", ZynCoverageStatus.Handled, relativePath, "PAD mode is represented by AquaSynth pad_mode table-generation syntax.");
AddBucket(buckets, "pad", $"bandwidth_scale={IntParam(pad, "bandwidth_scale", 0)}", ZynCoverageStatus.Handled, relativePath, "PAD bandwidth scale is represented by AquaSynth pad_bwscale syntax.");
var oscillator = pad.Element("OSCIL");
AddBucket(buckets, "pad.oscil.base_function", IntParam(oscillator, "base_function", 0).ToString(CultureInfo.InvariantCulture), ZynCoverageStatus.Handled, relativePath, "Base functions are approximated during PAD harmonic extraction.");
AddBucket(buckets, "pad.oscil.base_modulation", IntParam(oscillator, "base_function_modulation", 0).ToString(CultureInfo.InvariantCulture), ZynCoverageStatus.Handled, relativePath, "Base-function modulation is approximated before PAD table spreading.");
AddBucket(buckets, "pad.oscil.modulation", IntParam(oscillator, "modulation", 0).ToString(CultureInfo.InvariantCulture), ZynCoverageStatus.Handled, relativePath, "OscilGen modulation is approximated during source-table extraction.");
AddBucket(buckets, "pad.oscil.adaptive", IntParam(oscillator, "adaptive_harmonics", 0).ToString(CultureInfo.InvariantCulture), ZynCoverageStatus.Handled, relativePath, "Adaptive harmonics are approximated at the PAD table root.");
AddBucket(buckets, "pad.oscil.spectrum_adjust", IntParam(oscillator, "spectrum_adjust_type", 0).ToString(CultureInfo.InvariantCulture), ZynCoverageStatus.Handled, relativePath, "Spectrum adjustment is approximated after source-table extraction.");
var oscilFilter = IntParam(oscillator, "filter_type", 0).ToString(CultureInfo.InvariantCulture);
AddBucket(
buckets,
"pad.oscil.filter_type",
oscilFilter,
HandledPadOscilFilters.Contains(oscilFilter) ? ZynCoverageStatus.Handled : ZynCoverageStatus.Unknown,
relativePath,
HandledPadOscilFilters.Contains(oscilFilter)
? "This OscilGen filter type has an AquaSynth approximation."
: "Observed OscilGen filter type is not implemented in AquaSynth's Zyn PAD source extractor.");
var waveShape = IntParam(oscillator, "wave_shaping_function", 0).ToString(CultureInfo.InvariantCulture);
AddBucket(
buckets,
"pad.oscil.wave_shape",
waveShape,
HandledPadWaveShapes.Contains(waveShape) ? ZynCoverageStatus.Handled : ZynCoverageStatus.Unknown,
relativePath,
HandledPadWaveShapes.Contains(waveShape)
? "This waveshaper has an AquaSynth approximation."
: "Observed OscilGen waveshaper is not implemented in AquaSynth's Zyn PAD source extractor.");
var filter = pad.Element("FILTER_PARAMETERS");
if (filter is not null)
{
var filterKey = $"cat={DescendantIntParam(filter, "category", 0)} type={DescendantIntParam(filter, "type", 0)}";
AddBucket(
buckets,
"pad.filter",
filterKey,
HandledPadGlobalFilters.Contains(filterKey) ? ZynCoverageStatus.Handled : ZynCoverageStatus.Unknown,
relativePath,
HandledPadGlobalFilters.Contains(filterKey)
? "Analog LP/HP PAD filters map to AquaSynth cutoff, order, Q, and envelope fields."
: "Observed PAD filter category/type is not translated by the current PAD rebuild path.");
foreach (var filterLfo in filter.Elements("FILTER_LFO"))
{
AddBucket(
buckets,
"pad.filter_lfo",
$"intensity={IntParam(filterLfo, "intensity", 0)}",
IntParam(filterLfo, "intensity", 0) == 0 ? ZynCoverageStatus.Handled : ZynCoverageStatus.Counted,
relativePath,
IntParam(filterLfo, "intensity", 0) == 0
? "Neutral PAD filter LFO."
: "PAD filter LFO is reported by rebuilds as missing lowering pressure.");
}
}
}
private static IEnumerable<ZynEngine> EnabledEngines(XElement item)
{
if (EngineEnabled(item, "add_enabled", "ADD_SYNTH_PARAMETERS")) yield return ZynEngine.AddSynth;
if (EngineEnabled(item, "sub_enabled", "SUB_SYNTH_PARAMETERS")) yield return ZynEngine.SubSynth;
if (EngineEnabled(item, "pad_enabled", "PAD_SYNTH_PARAMETERS")) yield return ZynEngine.PadSynth;
}
private static bool EngineEnabled(XElement item, string flagName, string sectionName) =>
BoolParamValue(item, flagName) ?? item.Element(sectionName) is not null;
private static bool BoolParam(XElement root, string name) =>
BoolParamValue(root, name) ?? false;
private static bool? BoolParamValue(XElement root, string name)
{
var value = root.Elements("par_bool")
.FirstOrDefault(element => AttributeValue(element, "name") == name)
?.Attribute("value")
?.Value;
if (value is null) return null;
return value.Equals("yes", StringComparison.OrdinalIgnoreCase) ||
value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
value == "1";
}
private static int IntParam(XElement? root, string name, int fallback)
{
var value = root?.Elements("par")
.FirstOrDefault(element => AttributeValue(element, "name") == name)
?.Attribute("value")
?.Value;
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
}
private static int DescendantIntParam(XElement? root, string name, int fallback)
{
var value = root?.Descendants("par")
.FirstOrDefault(element => AttributeValue(element, "name") == name)
?.Attribute("value")
?.Value;
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
}
private static string? AttributeValue(XElement element, string name) =>
element.Attribute(name)?.Value;
private static string EngineKey(ZynEngine engine) => engine switch
{
ZynEngine.AddSynth => "add",
ZynEngine.SubSynth => "sub",
ZynEngine.PadSynth => "pad",
_ => "unknown"
};
private static void AddBucket(
Dictionary<(string Area, string Key), CoverageAccumulator> buckets,
string area,
string key,
ZynCoverageStatus status,
string example,
string notes)
{
var bucketKey = (area, key);
if (!buckets.TryGetValue(bucketKey, out var bucket))
{
bucket = new CoverageAccumulator(area, key, status, notes);
buckets.Add(bucketKey, bucket);
}
bucket.Count++;
if (status > bucket.Status)
{
bucket.Status = status;
bucket.Notes = notes;
}
if (bucket.Examples.Count < 10 && !bucket.Examples.Contains(example, StringComparer.OrdinalIgnoreCase))
{
bucket.Examples.Add(example);
}
}
private sealed class CoverageAccumulator(string area, string key, ZynCoverageStatus status, string notes)
{
public string Area { get; } = area;
public string Key { get; } = key;
public ZynCoverageStatus Status { get; set; } = status;
public int Count { get; set; }
public List<string> Examples { get; } = [];
public string Notes { get; set; } = notes;
public ZynCoverageBucket ToBucket(int exampleCount) =>
new(Area, Key, Status, Count, Examples.Take(exampleCount).ToList(), Notes);
}
}

View file

@ -91,3 +91,4 @@
{"date":"2026-05-15","topic":"zyn-pad-ear-vs-logmel","lesson":"For sustained Zyn PAD fixtures, log-mel distance can remain pessimistic after the perceptual target has been reached. Keep it as a regression and triage signal, but do not keep grinding accepted fixtures solely to chase a DX7-like numeric threshold when listening and structural source-table evidence agree.","evidence":"After mapping selected roots, HP filter motion, Q/order cascades, one-based OscilGen filter indices, base-function phase modulation, and adaptive harmonics, the user accepted DoublePadBass, Ghost Ensemble, Soft Pad, Organ Choir, and the other recent community PAD fixtures by ear. Organ Choir still reports log-mel 0.471483 despite direct kit1 table energy matching the Zyn body/air balance and the listener judging all fixtures equivalent."}
{"date":"2026-05-16","topic":"pad-syntax-generalization","lesson":"Reference-synth names must not become canonical AquaSynth syntax or public patch-model vocabulary. Importers may translate Zyn controls, and parser aliases may keep old parity artifacts readable, but generated/authored `.aqua` should use AquaSynth-native `pad_*` terms for general spectral-table intent.","evidence":"Zyn PAD parity had produced generated scripts with `zyn_mode`, `zyn_bandwidth`, `zyn_bwscale`, `zyn_profile`, and `zyn_position`, plus public model names like `ZynHarmonicProfile`. The generalization pass changed generated scripts to `pad_mode`, `pad_bandwidth`, `pad_bwscale`, `pad_profile`, and `pad_position`, renamed the public profile/position records and enums to PAD-native names, preserved legacy `zyn_*` parser aliases, and passed `dotnet test AquaSynth.slnx --no-restore` with 108 tests."}
{"date":"2026-05-16","topic":"project-rename-aquasynth","lesson":"The C# synth repo is now AquaSynth and the Rust reference branch is AquaSynth-rs. Keep repo wiring, package metadata, namespaces, solution/project names, env vars, docs, and state aligned to AquaSynth; do not reintroduce AquariumSynthCSharp/AquariumSynth.Dsl names.","evidence":"Renamed solution to AquaSynth.slnx, projects to src/AquaSynth.Dsl and tests/AquaSynth.Dsl.Tests, namespaces/package to AquaSynth.Dsl, package version suffix to 0.1.0-aquasynth.1, DX7 env var to AQUASYNTH_DX7_PYTHON, and origin to https://github.com/GameCult/AquaSynth.git. Verified `dotnet build AquaSynth.slnx --no-restore`, `dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-build -v minimal` with 108 passed, and `dotnet pack src\\AquaSynth.Dsl\\AquaSynth.Dsl.csproj -c Release --no-restore` producing AquaSynth.Dsl.0.1.0-aquasynth.1.nupkg."}
{"date":"2026-05-16","topic":"zyn-coverage-map","lesson":"Zyn coverage must distinguish handled parity-backed behavior, counted pressure, and unknown subtypes. Broad parse success is inventory, not expressivity. The upstream GPL bank currently parses 1318/1318 files and yields 1459 enabled kit items / 63 layered instruments; PAD coverage is strongest, while ADD, SUB, active LFO routing, free envelopes, formant motion, effects, non-analog PAD filters, and several PAD OscilGen filter/waveshaper subtypes remain workout buckets.","evidence":"Added `ZynCoverageStatus`, `ZynCoverageBucket`, `ZynCoverageReport`, `ZynInstrumentSurvey.CoverageDirectory`, and `CoverageMarkdown`; `SurveysUpstreamGplInstrumentBankWhenAvailable` writes `artifacts/parity/zyn-upstream-bank-survey/coverage.md`. Focused ZynInstrumentTests passed 13/13."}

View file

@ -30,6 +30,7 @@ modules:
faust_rendering: src/AquaSynth.Dsl/FaustExport.cs
dx7_reference_render_tests: tests/AquaSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
zyn_reference_reader: src/AquaSynth.Dsl/ZynInstrument.cs
zyn_coverage_survey: src/AquaSynth.Dsl/ZynInstrument.cs
package_boundary_tests: tests/AquaSynth.Dsl.Tests/PackageBoundaryTests.cs
sfxr: src/AquaSynth.Dsl/SfxrParams.cs
analysis: src/AquaSynth.Dsl/AudioAnalysis.cs

View file

@ -86,7 +86,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 AquaSynth 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. Zyn PAD `spectrum` banks can now carry explicit `zyn_mode`, `zyn_bandwidth`, `zyn_bwscale`, `zyn_profile`, and `zyn_position` table-generation fields, and `PadSynthWaveform` lowers those through Zyn-style profile/bandwidth math. 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, then move into explicit scoped rebuilds. 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 AquaSynth 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 AquaSynth `spectrum` was not a close Zyn PAD emulation. User listening identified Zyn as pure wineglass and AquaSynth as angry bees. Cutting unearned air/noise/drive/soft-clip from the rebuild, adding interpolated wavetable reads, and raising AquaSynth 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. `RebuildFirstPadAsAquaSynthScript` now generates first-pass per-preset PAD `.aqua` candidates from the first enabled PAD kit item, OSCIL harmonic magnitudes, oracle-reported table base frequency, basic volume, and simple rate/level envelope scaffolding while reporting missing bandwidth/profile/filter semantics. 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 AquaSynth 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. AquaSynth 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, then move into explicit scoped rebuilds. 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 AquaSynth 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 AquaSynth `spectrum` was not a close Zyn PAD emulation. User listening identified Zyn as pure wineglass and AquaSynth as angry bees. Cutting unearned air/noise/drive/soft-clip from the rebuild, adding interpolated wavetable reads, and raising AquaSynth 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, a handled/counted/unknown coverage map, and five upstream PAD listening fixtures under ignored artifacts. Coverage currently says PAD is the strongest exercised path, while ADD, SUB, active LFO routing, free envelopes, formant motion, effects, non-analog PAD filters, and several PAD OscilGen filter/waveshaper subtypes remain workout buckets. `RebuildFirstPadAsAquaSynthScript` now generates first-pass per-preset PAD `.aqua` candidates from the first enabled PAD kit item, OSCIL harmonic magnitudes, oracle-reported table base frequency, basic volume, and simple rate/level envelope scaffolding while reporting missing bandwidth/profile/filter semantics. 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 AquaSynth 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. AquaSynth 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": "Continue Zyn PAD parity from the sampled OscilGen stage-order, multi-kit PAD, Zyn-curve static global-filter, low-pass/high-pass order, PAD frequency-detune, low-pass/high-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, Q+order cascade, one-based OscilGen oscillator-filter index, base-function-7 selected-table-root, base-function phase modulation, and adaptive-harmonics paths. Ghost Ensemble now expands its power-base multi-harmonic OscilGen source, applies Zyn filter note tracking from `freq_track`, emits `lpf_q` from the Zyn analog filter `q`, and preserves low-pass slope by cascading resonant Faust stages according to `lpf_order`; latest Ghost log-mel is 0.369402, envelope distance is 0.363685, RMS ratio is 1.054875, centroid ratio is 1.006808, and score is 0.57229. DoublePadBass is accepted by ear and latest survey reports log-mel 0.247674, envelope distance 0.208351, RMS ratio 0.888011, score 0.650319. Soft Pad is accepted by ear. Organ Choir now maps kit0 HP2/stage/filter-envelope motion and kit1's selected table root, `base_function_modulation=1`, and `adaptive_harmonics=1`; latest Organ log-mel is 0.471483, envelope distance 0.302978, RMS ratio 0.878, centroid ratio 1.011328, and score 0.541933. Its kit1 PAD table energy now matches the wind-body shape much more closely: 400-800 Hz 0.2713 vs Zyn 0.2780 and 8-16 kHz 0.1471 vs Zyn 0.1355. ZynPadReference has test-only `harmonics` and `selected` modes; selected roots are diagnostic/source-specific, not a global default, because global selected roots regressed accepted DoublePadBass. sin2x remains loud/concentrated after the one-based filter-index fix, so do not claim full Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter LFO ownership are mapped or deliberately excluded."
},
"note_and_envelope_model": {

View file

@ -86,6 +86,7 @@ live_invariants:
- Zyn PAD selected table roots are not a global default in AquaSynth lowering. Keep sample-0 roots unless a source rule earns the selected root; currently OscilGen shift-base sources (`base_function=7`) may use the selected note table root because Organ Choir proved sample-0 roots leave the octave voice misaligned.
- Zyn OscilGen base-function modulation is source-table authority: apply it as phase modulation before base-function sampling, not as later oscillator modulation or filter/EQ compensation.
- Zyn adaptive harmonics remap the prepared oscillator spectrum at the PAD table root frequency before PAD bandwidth/profile spreading. Do not skip it for sources whose wind/body character depends on sparse remapped harmonics.
- Zyn coverage is now tracked as handled/counted/unknown buckets by `ZynInstrumentSurvey.CoverageDirectory`; do not treat broad parse counts as expressivity. Unknown PAD filter/waveshaper/OscilGen buckets and counted ADD/SUB/LFO/free-envelope/formant/effect buckets need parity workouts before they graduate into AquaSynth model changes.
- Filter damping is first-class: `lpf_q`/`lpq` owns explicit low-pass Q when a reference synth exposes analog filter damping. Low-pass order remains slope authority even when Q is present; Faust lowering cascades resonant low-pass stages instead of replacing order with one generic resonant filter. Do not fake Q by secretly moving cutoff, slope, or layer gain.
- Structured `.aqua` stock lives under `patches/` as development source material. Every library patch must parse/export, and the patch library must stay out of the NuGet package unless intentionally promoted.
- Project-authored Dexed patches may be generated inside tests for calibration; do not ship them in the NuGet package or treat them as runtime dependencies.
@ -108,7 +109,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 AquaSynth 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. `ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript` now emits per-preset PAD `.aqua` rebuilds for enabled PAD kit items with OSCIL harmonic expansion, oracle-reported table base frequency, PAD detune, Zyn PAD profile/bandwidth/position semantics, static/filter-envelope low-pass mapping, low-pass note tracking, explicit low-pass Q, logarithmic Zyn envelope times, and calibrated PAD output gain. 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`, including a test-only `harmonics` mode for dumping normalized OscilGen amplitudes before PAD generation. The optional PAD reports emit note and source-table WAV/listening artifacts. Latest accepted upstream PAD pressure has DoublePadBass close enough by ear with log-mel 0.247748 / score 0.650029 after Q/order cascade, while Ghost Ensemble improves to log-mel 0.369402 / score 0.57229. The remaining Ghost gap is no longer first-32 OscilGen harmonic extraction; oracle harmonic dumps match those ratios. 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."
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. `ZynInstrumentSurvey.CoverageDirectory` now classifies observed Zyn features into handled/counted/unknown buckets and writes `artifacts/parity/zyn-upstream-bank-survey/coverage.md` from the upstream GPL bank. First AquaSynth 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. `ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript` now emits per-preset PAD `.aqua` rebuilds for enabled PAD kit items with OSCIL harmonic expansion, oracle-reported table base frequency, PAD detune, Zyn PAD profile/bandwidth/position semantics, static/filter-envelope low-pass mapping, low-pass note tracking, explicit low-pass Q, logarithmic Zyn envelope times, and calibrated PAD output gain. 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`, including a test-only `harmonics` mode for dumping normalized OscilGen amplitudes before PAD generation. The optional PAD reports emit note and source-table WAV/listening artifacts. Latest accepted upstream PAD pressure has DoublePadBass close enough by ear with log-mel 0.247748 / score 0.650029 after Q/order cascade, while Ghost Ensemble improves to log-mel 0.369402 / score 0.57229. The remaining Ghost gap is no longer first-32 OscilGen harmonic extraction; oracle harmonic dumps match those ratios. 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 coverage map, 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."

View file

@ -264,6 +264,100 @@ public sealed class ZynInstrumentTests
}
await File.WriteAllLinesAsync(Path.Combine(artifactDir, "report.txt"), report);
var coverage = ZynInstrumentSurvey.CoverageDirectory(bankRoot);
await File.WriteAllTextAsync(Path.Combine(artifactDir, "coverage.md"), ZynInstrumentSurvey.CoverageMarkdown(coverage));
}
[Fact]
public void CoverageSurveyClassifiesProjectAuthoredZynPressure()
{
var root = Path.Combine(AppContext.BaseDirectory, "Fixtures", "ZynAddSubFX", "ProjectAuthored");
var report = ZynInstrumentSurvey.CoverageDirectory(root);
Assert.Equal(5, report.FileCount);
Assert.Equal(5, report.ParsedFileCount);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "engine" &&
bucket.Key == "pad" &&
bucket.Status == ZynCoverageStatus.Handled);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "engine" &&
bucket.Key == "add" &&
bucket.Status == ZynCoverageStatus.Counted);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "engine" &&
bucket.Key == "sub" &&
bucket.Status == ZynCoverageStatus.Counted);
Assert.Contains(report.Buckets, bucket =>
bucket.Area == "envelope" &&
bucket.Key.EndsWith(".free", StringComparison.Ordinal) &&
bucket.Status == ZynCoverageStatus.Counted);
var markdown = ZynInstrumentSurvey.CoverageMarkdown(report);
Assert.Contains("## Unknown", markdown);
Assert.Contains("## Counted", markdown);
Assert.Contains("## Handled", markdown);
}
[Fact]
public void CoverageSurveyExposesUnhandledZynPadSubtypes()
{
var dir = Directory.CreateTempSubdirectory("aquasynth-zyn-coverage-");
try
{
File.WriteAllText(Path.Combine(dir.FullName, "unknown-pad.xiz"), """
<?xml version="1.0" encoding="UTF-8"?>
<ZynAddSubFX-data>
<INSTRUMENT>
<INFO><string name="name">Unknown PAD Modes</string></INFO>
<INSTRUMENT_KIT>
<INSTRUMENT_KIT_ITEM id="0">
<par_bool name="enabled" value="yes"/>
<par_bool name="pad_enabled" value="yes"/>
<PAD_SYNTH_PARAMETERS>
<OSCIL>
<par name="filter_type" value="12"/>
<par name="wave_shaping_function" value="14"/>
<HARMONICS>
<HARMONIC id="1"><par name="mag" value="127"/></HARMONIC>
</HARMONICS>
</OSCIL>
<FILTER_PARAMETERS>
<FILTER>
<par name="category" value="2"/>
<par name="type" value="3"/>
</FILTER>
<FILTER_LFO>
<par name="intensity" value="40"/>
</FILTER_LFO>
</FILTER_PARAMETERS>
</PAD_SYNTH_PARAMETERS>
</INSTRUMENT_KIT_ITEM>
</INSTRUMENT_KIT>
</INSTRUMENT>
</ZynAddSubFX-data>
""");
var report = ZynInstrumentSurvey.CoverageDirectory(dir.FullName);
Assert.Contains(report.UnknownBuckets, bucket =>
bucket.Area == "pad.oscil.filter_type" &&
bucket.Key == "12");
Assert.Contains(report.UnknownBuckets, bucket =>
bucket.Area == "pad.oscil.wave_shape" &&
bucket.Key == "14");
Assert.Contains(report.UnknownBuckets, bucket =>
bucket.Area == "pad.filter" &&
bucket.Key == "cat=2 type=3");
Assert.Contains(report.CountedBuckets, bucket =>
bucket.Area == "pad.filter_lfo" &&
bucket.Key == "intensity=40");
}
finally
{
dir.Delete(recursive: true);
}
}
private static void AssertFeature(ZynInstrument instrument, string name, string value) =>