De-bee Zyn PAD rebuild

This commit is contained in:
Meta Vens 2026-05-15 00:35:20 +01:00
commit 2096189a3e
8 changed files with 83 additions and 32 deletions

View file

@ -799,27 +799,25 @@ public static class BuiltInScripts
public const string ZynStylePadTexture = """
patch
gain=0.56
soft_clip=true
gain=1.4
param
name=spread
path=/macro/spread
default=0.018
default=0.006
min=0
max=0.04
step=0.001
unit=ratio
defaults
wave=saw
attack=0.55
sustain=1.8
decay=1.4
sustain_level=0.78
lpf=0.42
hpf=0.025
drive=0.04
wave=sine
attack=0.28
sustain=1.4
decay=1.2
sustain_level=0.9
lpf=1
hpf=0
mod
name=slow_air
@ -828,13 +826,11 @@ public static class BuiltInScripts
lpf=0.18
gain=0.05
layer name=pad_low engine=pad min_key=36 max_key=84 gain=0.07 vibrato=@/macro/spread vibrato_hz=0.09 env=rl rates=0.55,0.68,0.95,1.4 levels=1,0.86,0.78,0 curves=lin,exp,lin,lin gate=2.18
layer name=pad_high engine=pad min_key=48 max_key=108 gain=0.044 vibrato=0.009 vibrato_hz=0.11 env=rl rates=0.42,0.58,0.9,1.25 levels=1,0.84,0.72,0 curves=lin,exp,lin,lin gate=1.9
layer name=air engine=add min_key=0 max_key=127 gain=0.018 wave=noise noise=0.82 env=rl rates=0.8,0.6,1.1,1.1 levels=1,0.82,0.55,0 curves=lin,lin,exp,lin gate=2.5 lpf=0.36 hpf=0.22
layer name=pad_low engine=pad min_key=36 max_key=84 gain=0.14 vibrato=0.002 vibrato_hz=0.09 env=rl rates=0.28,0.42,0.7,1.2 levels=1,0.94,0.88,0 curves=lin,exp,lin,lin gate=2.18
layer name=pad_high engine=pad min_key=48 max_key=108 gain=0.04 vibrato=0.0015 vibrato_hz=0.11 env=rl rates=0.28,0.42,0.7,1.2 levels=1,0.94,0.88,0 curves=lin,exp,lin,lin gate=2.18
spectrum layer=pad_low root=130.8128 spread=0.012 partials=1:0.07,1.5:0.052
spectrum layer=pad_high root=261.6256 spread=0.009 partials=1:0.044 wave=triangle
voice layer=air freq=1800
spectrum layer=pad_low root=130.8128 spread=0.003 partials=1:0.14,1.5:0.032
spectrum layer=pad_high root=261.6256 spread=0.002 partials=1:0.04
""";
public const string ZynStyleVocalLayer = """

View file

@ -259,13 +259,16 @@ public static class FaustEmitter
ParameterMap parameters,
List<string> warnings)
{
const int tableSize = 4096;
const int tableSize = 131072;
var table = PadSynthWaveform.Generate(bank, tableSize);
var frequency = parameters.Expression(OwnerField(SpectralPath(bankIndex), "note/frequency"), bank.Treatment.Note.FrequencyHz);
var readFrequency = $"({F(PadSynthWaveform.SampleRate)} / {F(tableSize)} * ({frequency}) / {F(bank.RootFrequencyHz)})";
source.AppendLine($"{name}_wave = waveform {{{string.Join(",", table.Select(F))}}};");
source.AppendLine($"{name}_read_index = int(os.phasor({tableSize}, {readFrequency}));");
source.AppendLine($"{name}_wavetable = {name}_wave, {name}_read_index : rdtable;");
source.AppendLine($"{name}_read_pos = os.phasor({tableSize}, {readFrequency});");
source.AppendLine($"{name}_read_index = int({name}_read_pos);");
source.AppendLine($"{name}_read_frac = {name}_read_pos - float({name}_read_index);");
source.AppendLine($"{name}_read_next = ({name}_read_index + 1) % {tableSize};");
source.AppendLine($"{name}_wavetable = ({name}_wave, {name}_read_index : rdtable) * (1 - {name}_read_frac) + ({name}_wave, {name}_read_next : rdtable) * {name}_read_frac;");
EmitVoice(source, patch, bank.Treatment, SpectralPath(bankIndex), name, parameters, warnings, $"{name}_wavetable");
}

View file

@ -64,6 +64,17 @@ internal static class PadSynthWaveform
var bandwidthHz = (Math.Pow(2.0, bandwidthCents / 1200.0) - 1.0) * bank.RootFrequencyHz * ratio;
var bandwidthCyclesPerSample = Math.Max(1e-9, bandwidthHz / (2.0 * SampleRate));
var centerBin = centerCyclesPerSample * tableSize;
if (bandwidthCyclesPerSample * tableSize < 0.5)
{
var bin = (int)Math.Round(centerBin);
if (bin > 0 && bin < amplitudes.Length)
{
amplitudes[bin] += partial.Gain;
}
continue;
}
var radiusBins = Math.Max(2, (int)Math.Ceiling(bandwidthCyclesPerSample * tableSize * 6));
var left = Math.Max(1, (int)Math.Floor(centerBin - radiusBins));
var right = Math.Min(amplitudes.Length - 1, (int)Math.Ceiling(centerBin + radiusBins));

View file

@ -67,3 +67,4 @@
{"kind":"zyn_reference_oracle","lesson":"ZynAddSubFX source is pinned as GPL test/development material under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825. Treat it like dexed-py: a parity oracle for tests and artifacts, not Aquarium implementation, runtime dependency, or NuGet package content. Before further Zyn DSL golf, build a test-only PAD renderer/wrapper from this source and compare Aquarium rebuilds against actual Zyn behavior.","source":"Zyn source pinning for parity on 2026-05-14"}
{"date":"2026-05-14","area":"zyn-reference","lesson":"Pinned Zyn can build on Windows through MSYS2/MinGW when GUI/plugins are disabled, tests are enabled, Werror is off, and the stale non-async MiddleWare Part constructor call is patched. Apply Zyn compatibility patches to ignored artifact source copies, not the clean pinned submodule.","evidence":"tools/zyn-reference/build-zyn-reference.ps1 copies external/zynaddsubfx to artifacts/zyn-source-msys, applies tools/zyn-reference/patches/0001-msys-part-constructor.patch, builds PadNoteTest, and ctest -R PadNoteTest passes."}
{"date":"2026-05-15","area":"zyn-pad-parity","lesson":"The Zyn PAD oracle now emits real reference material through Zyn PADnoteParameters/PADnote, and the first project-authored PAD comparison is not close: log-mel 0.552296, envelope 0.774501, RMS ratio 0.234763, centroid ratio 1.025735, score 0.236117. Treat this as evidence to isolate source table generation, playback/gain, envelope, and fixture mapping before adding more Zyn DSL syntax.","evidence":"tools/zyn-reference/ZynPadReference.cpp plus tests/AquariumSynth.Dsl.Tests/ZynReferenceParityTests.cs write artifacts/parity/zyn-pad-reference/pad-texture-zyn.wav, pad-texture-aqua.wav, and report.txt when the Zyn artifact renderer is built."}
{"date":"2026-05-15","area":"zyn-pad-parity","lesson":"User listening correctly identified the first Aquarium PAD rebuild as too noisy. The fix was architectural cleanup before new syntax: cut unearned noise/drive/soft-clip from the rebuild, interpolate spectral table reads, and use Zyn-sized 131072-frame PAD tables. Score improved from 0.236117 to 0.487325, with RMS ratio 1.008516 and centroid ratio 1.083082. Remaining mismatch is source profile/table generation and envelope semantics, not decorative layers.","evidence":"artifacts/parity/zyn-pad-reference/report.txt after the de-bee pass; full dotnet test passed 96/96."}

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`. The first project-authored PAD comparison is intentionally not a passing gate: Zyn vs Aquarium currently reports log-mel 0.552296, envelope 0.774501, RMS ratio 0.234763, centroid ratio 1.025735, score 0.236117. That proves the harness and shows Aquarium `spectrum` is not yet a close Zyn PAD emulation. The hardened reader parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. The worst target is `olivers-100/0032-Drum Kit.xiz`, with 16 enabled kit items, mixed ADD/SUB/PAD engines, 106 envelopes, 25 free envelopes, 73 LFOs, 37 filters, and 3 formant filters. The first rebuilds show Aquarium can sketch small Zyn fixtures with existing voices/macros/formants/noise, and the upstream survey correctly pushed named kit/layer routing first. `layer` owns name, engine tag, optional key range metadata, default gain, and effect-send labels while lowering to ordinary voices. `harmonics` owns authored ratio/gain partial banks under those layers. `spectrum` owns PAD-style root/spread/partial clouds under those layers and generates PADsynth-style FFT wavetables with frequency-domain spreading, deterministic random phase, and one inverse FFT before ordinary voice treatment. Staged `env=rl` now applies to voices as well as operators, preserving Note as gate authority and envelope data as level-shape authority. Aquarium still does not claim Zyn ADDsynth phase/bandwidth details, full Zyn PAD harmonic-profile/randomness/pitch-zone behavior, arbitrary free-envelope point curves, or richer formant morphing.",
"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`. 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 to log-mel 0.405313, envelope 0.35139, RMS ratio 1.008516, centroid ratio 1.083082, score 0.487325. The remaining gap is source-table/profile and envelope semantics, not more decorative patch layers. The report now also emits `pad-texture-zyn-table0.wav` for source-table 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 those layers and generates PADsynth-style FFT wavetables with frequency-domain spreading, deterministic random phase, and one inverse FFT before ordinary voice treatment. Staged `env=rl` now applies to voices as well as operators, preserving Note as gate authority and envelope data as level-shape authority. Aquarium still does not claim Zyn ADDsynth phase/bandwidth details, full Zyn PAD harmonic-profile/randomness/pitch-zone behavior, arbitrary free-envelope point curves, or richer formant morphing.",
"next": "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": {
@ -101,7 +101,7 @@
},
"last_verified": {
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File tools\\zyn-reference\\build-zyn-reference.ps1; dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "Zyn source copied to ignored artifact tree, patched, built with MSYS2/MinGW, PadNoteTest passed, ZynPadReference built; C# suite 96 passed and package boundary remains clean",
"commit": "pending Zyn PAD renderer"
"result": "ZynPadReference artifacts include note and source-table WAVs; Aquarium PAD render improved to score 0.487325; C# suite 96 passed",
"commit": "pending Zyn PAD de-bee pass"
}
}

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-15T00:35:00+01:00
updated: 2026-05-15T01:15:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -101,7 +101,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 first optional PAD report against the project-authored fixture emits WAV/listening artifacts and scores poorly but usefully: log-mel 0.552296, envelope 0.774501, score 0.236117. 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 report emits note and source-table WAV/listening artifacts. After cutting unearned noise/drive/soft-clip, adding interpolated table reads, and raising Aquarium spectral tables to 131072 frames, the project-authored PAD report is better but still not passing: log-mel 0.405313, envelope 0.35139, score 0.487325. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first owner abstraction."
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."
@ -143,10 +143,12 @@ next_agent_start_here:
through `tools/zyn-reference/build-zyn-reference.ps1` using an ignored
patched artifact copy and builds `ZynPadReference`, which emits PAD table
samples or note audio from Zyn. The first optional project-authored PAD
comparison writes WAV/report artifacts under `artifacts/parity/zyn-pad-reference`
and is not close yet: log-mel 0.552296, envelope 0.774501, score 0.236117.
Next pressure is isolating whether the mismatch lives in source-table
generation, note playback/gain, envelope shape, or fixture mapping.
comparison writes WAV/report artifacts under `artifacts/parity/zyn-pad-reference`.
User listening called the reference wineglass-pure and Aquarium angry bees.
The current de-bee pass cuts unearned noise/drive/soft-clip, interpolates
spectral wavetable reads, uses 131072-frame PAD tables, and now reports
log-mel 0.405313, envelope 0.35139, score 0.487325. Next pressure is
source-table/profile and envelope semantics, not more decorative layers.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
@ -159,6 +161,20 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-15
summary: >
De-beed the project-authored Zyn PAD rebuild after listening identified
Aquarium as a noisy swarm while Zyn sounded like a pure blown wineglass.
Cut the unearned `air` noise layer, drive, and soft clipping from the PAD
rebuild; added interpolated Faust wavetable reads for spectral banks; and
raised spectral PAD tables to Zyn's 131072-frame sample size. The optional
Zyn PAD report now also writes the source table WAV
`pad-texture-zyn-table0.wav`. Latest report: log-mel 0.405313, envelope
0.35139, RMS ratio 1.008516, centroid ratio 1.083082, score 0.487325.
This is a real improvement from 0.236117 but not passing; remaining
pressure is exact source profile/table generation and Zyn amplitude
semantics.
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 96 passed"
- date: 2026-05-15
summary: >
Added `ZynPadReference`, a test-only executable copied into the ignored

View file

@ -262,8 +262,9 @@ public sealed class PatchScriptTests
if (rebuild.ReferenceId == "zyn/project/pad-texture")
{
Assert.NotEmpty(patch.SpectralBanks);
Assert.Contains(patch.SpectralBanks, bank => bank.Treatment.RateLevelEnvelope is not null);
}
if (rebuild.ReferenceId is "zyn/project/pad-texture" or "zyn/project/vocal-layer")
if (rebuild.ReferenceId == "zyn/project/vocal-layer")
{
Assert.Contains(patch.Voices, voice => voice.RateLevelEnvelope is not null);
}
@ -405,7 +406,8 @@ public sealed class PatchScriptTests
Assert.Equal(100, bank.Treatment.Oscillator.FrequencyHz);
Assert.Equal(.08f, bank.Treatment.Gain, 5);
Assert.Contains("spectral_0_wave = waveform", export.Source);
Assert.Contains("spectral_0_wavetable = spectral_0_wave, spectral_0_read_index : rdtable", export.Source);
Assert.Contains("spectral_0_read_frac", export.Source);
Assert.Contains("spectral_0_wavetable = (spectral_0_wave, spectral_0_read_index : rdtable)", export.Source);
Assert.Contains("process =", export.Source);
}

View file

@ -38,6 +38,8 @@ public sealed class ZynReferenceParityTests
var input = Path.Combine(root, "tests", "AquariumSynth.Dsl.Tests", "Fixtures", "ZynAddSubFX", "ProjectAuthored", "pad-texture.xiz");
var zynRaw = Path.Combine(artifactDir, "pad-texture-zyn.f32");
var zynWav = Path.Combine(artifactDir, "pad-texture-zyn.wav");
var zynTableRaw = Path.Combine(artifactDir, "pad-texture-zyn-table0.f32");
var zynTableWav = Path.Combine(artifactDir, "pad-texture-zyn-table0.wav");
var aquaWav = Path.Combine(artifactDir, "pad-texture-aqua.wav");
var reportPath = Path.Combine(artifactDir, "report.txt");
@ -55,10 +57,26 @@ public sealed class ZynReferenceParityTests
var reference = await RunAsync(bash, ["-lc", command]);
Assert.Equal(0, reference.ExitCode);
var tableCommand = 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(zynTableRaw)),
"0",
"0"
]);
var tableReference = await RunAsync(bash, ["-lc", tableCommand]);
Assert.Equal(0, tableReference.ExitCode);
var zynSamples = await ReadFloat32Async(zynRaw);
var zynTableSamples = await ReadFloat32Async(zynTableRaw);
Assert.Equal(66150, zynSamples.Length);
Assert.True(Peak(zynSamples) > 0.001f);
Assert.True(zynTableSamples.Length > zynSamples.Length);
Assert.True(Peak(zynTableSamples) > 0.001f);
WriteWav(zynWav, zynSamples, 44100, 0.9f);
WriteWav(zynTableWav, zynTableSamples, 44100, 0.9f);
var report = new List<string>
{
@ -66,9 +84,13 @@ public sealed class ZynReferenceParityTests
$"renderer: {renderer}",
$"input: {input}",
$"stdout: {reference.Stdout.Trim()}",
$"table_stdout: {tableReference.Stdout.Trim()}",
$"zyn_samples: {zynSamples.Length}",
$"zyn_peak: {Peak(zynSamples):0.######}",
$"zyn_rms: {Rms(zynSamples):0.######}"
$"zyn_rms: {Rms(zynSamples):0.######}",
$"zyn_table_samples: {zynTableSamples.Length}",
$"zyn_table_peak: {Peak(zynTableSamples):0.######}",
$"zyn_table_rms: {Rms(zynTableSamples):0.######}"
};
var aquaExport = FaustEmitter.EmitScript(BuiltInScripts.ZynStylePadTexture, new FaustExportOptions("zyn_pad_texture"));