diff --git a/src/AquariumSynth.Dsl/BuiltInScripts.cs b/src/AquariumSynth.Dsl/BuiltInScripts.cs index fa80053..0d3493c 100644 --- a/src/AquariumSynth.Dsl/BuiltInScripts.cs +++ b/src/AquariumSynth.Dsl/BuiltInScripts.cs @@ -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 = """ diff --git a/src/AquariumSynth.Dsl/PatchScript.cs b/src/AquariumSynth.Dsl/PatchScript.cs index f7e7314..6a3d2c8 100644 --- a/src/AquariumSynth.Dsl/PatchScript.cs +++ b/src/AquariumSynth.Dsl/PatchScript.cs @@ -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 fields, params string[] keys) => + keys.Select(CanonicalField).Any(fields.ContainsKey); + private static float GetFloat(IReadOnlyDictionary fields, int line, float fallback, params string[] keys) => TryGetAny(fields, keys, out var value) ? ParseFloat(value, line) : fallback; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0a960df..d6f93f3 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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."} diff --git a/state/memory.json b/state/memory.json index 9ddc171..b7309b7 100644 --- a/state/memory.json +++ b/state/memory.json @@ -85,7 +85,7 @@ }, "zyn_reference_path": { "status": "Initial ZynInstrumentReader parses plain XML and gzip-compressed .xiz-shaped XML from project-authored fixtures into ZynInstrument records and ReferencePatch features. First Zyn-style Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer. `ZynInstrumentSurvey` can rank .xiz directories by feature pressure. Named `layer` declarations provide the first Zyn-driven DSL abstraction; additive `harmonics`, PAD `spectrum`, and staged `env=rl` voice envelopes now attach to those layers. The pinned Zyn source now builds as a test-only MSYS2/MinGW oracle through `tools/zyn-reference/build-zyn-reference.ps1`, which patches an ignored artifact source copy, proves `PadNoteTest`, and builds `ZynPadReference`.", - "lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The ZynAddSubFX engine source is pinned as GPL test/development material under `external/zynaddsubfx` at 3ab608c432996ba4d582176572c0b0f82328c825; use it as a parity oracle only, never as Aquarium runtime or NuGet package content. Zyn build fixes belong in `tools/zyn-reference/patches/` and apply to ignored artifact copies, not the clean pinned submodule. The PAD oracle now renders both generated PAD sample tables and note audio through Zyn `PADnoteParameters`/`PADnote`; optional parity artifacts live under `artifacts/parity/zyn-pad-reference`. 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" } } diff --git a/state/spine.yaml b/state/spine.yaml index 7255183..c63a813 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -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 diff --git a/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs b/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs index 266a9e5..dd0ecb9 100644 --- a/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs @@ -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() {