Split PAD table root from note pitch

This commit is contained in:
Meta Vens 2026-05-15 00:46:39 +01:00
commit 998d3f0d18
6 changed files with 52 additions and 18 deletions

View file

@ -826,11 +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.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
layer name=pad_low engine=pad min_key=36 max_key=84 gain=0.14 vibrato=0 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 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.003 partials=1:0.14,1.5:0.032
spectrum layer=pad_high root=261.6256 spread=0.002 partials=1:0.04
spectrum layer=pad_low root=77.7813 freq=261.6256 spread=0 partials=1:0.14,1.5:0.032
spectrum layer=pad_high root=77.7813 freq=261.6256 spread=0 partials=1:0.04
""";
public const string ZynStyleVocalLayer = """

View file

@ -201,7 +201,7 @@ public static class PatchScript
throw new PatchScriptException(line, $"unknown layer `{layerName}`");
}
var rootFrequency = GetFloat(fields, line, 440, "root", "base", "freq", "frequency");
var rootFrequency = GetFloat(fields, line, 440, "root", "base", "basefreq", "table_freq", "table_frequency");
if (rootFrequency <= 0)
{
throw new PatchScriptException(line, "spectral root frequency must be greater than zero");
@ -225,8 +225,9 @@ public static class PatchScript
"layer",
"root",
"base",
"freq",
"frequency",
"basefreq",
"table_freq",
"table_frequency",
"spread",
"width",
"detune",
@ -234,7 +235,10 @@ public static class PatchScript
"bank",
"tones");
treatmentFields["layer"] = layerName;
treatmentFields["freq"] = F(rootFrequency);
if (!HasAny(treatmentFields, "freq", "frequency", "f"))
{
treatmentFields["freq"] = F(rootFrequency);
}
var treatment = ParseVoice(
ExpandVoiceFields(treatmentFields, line),
SpectralPath(_spectralBanks.Count),
@ -1145,6 +1149,9 @@ public static class PatchScript
return false;
}
private static bool HasAny(IReadOnlyDictionary<string, string> fields, params string[] keys) =>
keys.Select(CanonicalField).Any(fields.ContainsKey);
private static float GetFloat(IReadOnlyDictionary<string, string> fields, int line, float fallback, params string[] keys) =>
TryGetAny(fields, keys, out var value) ? ParseFloat(value, line) : fallback;

View file

@ -68,3 +68,4 @@
{"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."}
{"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."}

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 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.",
"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. 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, and removing gratuitous spread/vibrato, improved the report to log-mel 0.353365, envelope 0.348752, RMS ratio 0.97283, centroid ratio 0.999243, score 0.612832. 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 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": {
@ -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": "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"
"result": "PAD root/playback frequency split fixed the octave-low read and no-spread/no-vibrato reduced drone; score 0.612832; C# suite 97 passed",
"commit": "pending Zyn PAD pitch/drone pass"
}
}

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-15T01:15:00+01:00
updated: 2026-05-15T01:40:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -79,7 +79,7 @@ live_invariants:
- 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.
- PAD `spectrum` declarations own authored partial clouds under named layers. They generate PADsynth-style FFT wavetables with frequency-domain spreading and deterministic random phase, then emit Faust table readers before ordinary voice treatment. Do not pretend this covers every Zyn PAD harmonic profile, randomness control, or pitch-zone table behavior.
- PAD `spectrum` declarations own authored partial clouds under named layers and split table root from playback pitch: `root` is source/base frequency, while `freq` is the note played by the spectral voice. They generate PADsynth-style FFT wavetables with frequency-domain spreading and deterministic random phase, then emit Faust table readers before ordinary voice treatment. Do not pretend this covers every Zyn PAD harmonic profile, randomness control, or pitch-zone table behavior.
- 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.
- Harsh calibration probes belong in tests and ignored artifacts, not in the patch library unless they are musically useful stock.
@ -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 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."
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, raising Aquarium spectral tables to 131072 frames, splitting table root from playback frequency, and removing gratuitous spread/vibrato, the project-authored PAD report is better but still not passing: log-mel 0.353365, envelope 0.348752, centroid ratio 0.999243, score 0.612832. 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."
@ -145,10 +145,12 @@ next_agent_start_here:
samples or note audio from Zyn. The first optional project-authored PAD
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.
The current de-bee/pitch pass cuts unearned noise/drive/soft-clip,
interpolates spectral wavetable reads, uses 131072-frame PAD tables, splits
table `root` from playback `freq`, removes gratuitous spread/vibrato, and
now reports log-mel 0.353365, envelope 0.348752, centroid ratio 0.999243,
score 0.612832. Next pressure is source-table/profile and envelope
semantics, not more decorative layers.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
@ -161,6 +163,17 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-15
summary: >
Fixed the octave-low Zyn PAD rebuild by splitting spectral table root
from playback frequency. `spectrum root=...` now owns the source/base
frequency used for wavetable playback scaling, while `freq=...` remains
the note frequency on the spectral voice. The project-authored PAD rebuild
now uses Zyn's reported table base frequency 77.7813 and plays 261.6256.
Removing gratuitous spread/vibrato reduced the drone the user heard.
Latest report: log-mel 0.353365, envelope 0.348752, RMS ratio 0.97283,
centroid ratio 0.999243, score 0.612832.
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 97 passed"
- date: 2026-05-15
summary: >
De-beed the project-authored Zyn PAD rebuild after listening identified

View file

@ -411,6 +411,19 @@ public sealed class PatchScriptTests
Assert.Contains("process =", export.Source);
}
[Fact]
public void SpectralBankSeparatesTableRootFromPlaybackFrequency()
{
var patch = PatchScript.Parse("""
layer name=pad engine=pad gain=.08
spectrum layer=pad root=77.7813 freq=261.6256 spread=0 partials=1:.08
""");
var bank = Assert.Single(patch.SpectralBanks);
Assert.Equal(77.7813f, bank.RootFrequencyHz, 4);
Assert.Equal(261.6256f, bank.Treatment.Note.FrequencyHz, 4);
}
[Fact]
public void SpectralBankSyntaxRejectsUnknownLayerOrBadSpread()
{