diff --git a/src/AquariumSynth.Dsl/PadSynthWaveform.cs b/src/AquariumSynth.Dsl/PadSynthWaveform.cs index bd92b60..bd98270 100644 --- a/src/AquariumSynth.Dsl/PadSynthWaveform.cs +++ b/src/AquariumSynth.Dsl/PadSynthWaveform.cs @@ -141,6 +141,7 @@ internal static class PadSynthWaveform for (var i = 0; i < profileSize * superSample; i++) { var x = i / (double)(profileSize * superSample); + var originalX = x * 2.0 - 1.0; x = (x - 0.5) * width + 0.5; if (x is < 0 or > 1) { @@ -156,7 +157,6 @@ internal static class PadSynthWaveform continue; } - var originalX = x * 2.0 - 1.0; var beforeFreqMult = x; x *= freqMult; x += Math.Sin(beforeFreqMult * Math.PI * modFreq) * modPar; diff --git a/src/AquariumSynth.Dsl/ZynInstrument.cs b/src/AquariumSynth.Dsl/ZynInstrument.cs index 794f789..702f8a1 100644 --- a/src/AquariumSynth.Dsl/ZynInstrument.cs +++ b/src/AquariumSynth.Dsl/ZynInstrument.cs @@ -533,7 +533,7 @@ public static class ZynInstrumentReader private static double ZynOscilFilterGain(int type, int harmonic, double par, double par2) { - var i = Math.Max(0, harmonic - 1); + var i = Math.Max(1, harmonic); return type switch { 1 => ZynOscilLowPass(i, par, par2), diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 354670b..af8108a 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -85,3 +85,4 @@ {"date":"2026-05-15","topic":"zyn-pad-output-calibration","lesson":"After PAD table roots, detune, filter order, filter envelope, and logarithmic envelope times are mapped, remaining Zyn PAD loudness can be a generated-rebuild output calibration rather than another per-layer fixture tweak.","evidence":"Current PAD fixture RMS ratios before calibration were consistently low: DoublePadBass 0.511669, Soft Pad 0.619347, Organ Choir 0.588127, Ghost 0.730155, sin2x 0.775653. Emitting patch gain=1.6 moved DoublePadBass candidate WAV peak to 0.979 against clipped Zyn reference peak 1.0, envelope distance to 0.226563, RMS ratio to 0.818669, and score to 0.631168 while keeping batch RMS plausible."} {"date":"2026-05-15","topic":"zyn-pad-filter-q","lesson":"Zyn PAD analog filter Q is real filter damping authority and belongs as a general Aquarium filter surface (`lpf_q`), not as a fixture-specific cutoff/gain fudge.","evidence":"Ghost Ensemble has FILTER freq_track=97 and q=28. Adding note-tracked cutoff lowered Ghost lpf from 0.369706 to 0.282776 and adding explicit lpf_q=0.499021 moved log-mel from 0.38424 to 0.375637 and RMS ratio from 1.155288 to 1.09995. DoublePadBass remained acceptable and improved to log-mel 0.247729 / score 0.650069 after Q."} {"date":"2026-05-15","topic":"zyn-pad-q-order-cascade","lesson":"Explicit low-pass Q must not erase low-pass order. When a reference filter has both damping and slope, Faust lowering should cascade resonant stages according to `lpf_order` rather than replacing the filter with one generic resonant low-pass.","evidence":"Ghost Ensemble still sounded bright in the middle after lpf_q. Cascading resonlp stages for lpf_order=4 moved Ghost log-mel from 0.375637 to 0.369402 and RMS ratio from 1.09995 to 1.054875 while keeping DoublePadBass near its accepted score: log-mel 0.247748, score 0.650029. ZynPadReference harmonic dumps showed Ghost's first 32 imported OscilGen partial ratios already matched Zyn, so this was filter slope, not first-page harmonic extraction."} +{"date":"2026-05-15","topic":"zyn-oscil-filter-index","lesson":"Zyn OscilGen oscillator filter functions use one-based harmonic indices. Do not translate them through a zero-based Aquarium index; that moves spectral features to the wrong partial and can make a preset sound like the wrong overtone is being emphasized.","evidence":"sin2x uses oscillator filter type 13. With zero-based indexing, Aquarium put the spike on h3 and had centroid ratio 1.227953. After preserving Zyn's one-based index, candidate partials are h2-dominant, centroid ratio improves to 0.998145, log-mel moves from 0.491437 to 0.479782, and UpstreamSin2xOscilFilterKeepsSpikeOnSecondHarmonicWhenAvailable asserts h2 > h3 * 10."} diff --git a/state/memory.json b/state/memory.json index e143d06..4cd9c93 100644 --- a/state/memory.json +++ b/state/memory.json @@ -87,7 +87,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. 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 Aquarium runtime or NuGet package content. Zyn build fixes belong in `tools/zyn-reference/patches/` and apply to ignored artifact copies, not the clean pinned submodule. The PAD oracle now renders both generated PAD sample tables and note audio through Zyn `PADnoteParameters`/`PADnote`; optional parity artifacts live under `artifacts/parity/zyn-pad-reference` and `artifacts/parity/zyn-pad-fixtures`. The first project-authored PAD comparison proved the harness and showed Aquarium `spectrum` was not a close Zyn PAD emulation. User listening identified Zyn as pure wineglass and Aquarium as angry bees. Cutting unearned air/noise/drive/soft-clip from the rebuild, adding interpolated wavetable reads, and raising Aquarium spectral tables to Zyn's 131072-frame PAD size improved the report. A later pitch/drone pass split PAD table root from playback note frequency: `spectrum root=...` owns table/base frequency and `freq=...` owns the played note. Using Zyn's reported sample basefreq 77.7813 while playing 261.6256, removing gratuitous spread/vibrato, collapsing two same-note PAD voices into one coherent spectral table, and trimming gain improved the report to log-mel 0.35669, envelope 0.347572, RMS ratio 1.001704, centroid ratio 1.004072, score 0.6211. The first PAD fixture survey shows `pad-pure` and the old `pad-texture` render identically through Zyn, so the filter/category XML in `pad-texture` is not live pressure for this oracle path. `pad-harmonic` changes the source table and note output through explicit OSCIL harmonics, making it the next real PAD reference rung. The remaining gap is source-table/profile and envelope semantics, not more decorative patch layers. The report now also emits source-table WAVs for listening. The hardened reader parses the upstream GPL instrument bank at commit c5c912131b31df5fdf372d2f06a25aaf2375837f: 1318/1318 .xiz files; optional tests write a ranked report and render five upstream PAD listening fixtures under ignored artifacts. `RebuildFirstPadAsAquariumScript` 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 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": "Continue Zyn PAD parity from the sampled OscilGen stage-order, multi-kit PAD, Zyn-curve static global-filter, low-pass-order, PAD frequency-detune, low-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, and Q+order cascade 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 after Q/order cascade reports log-mel 0.247748, envelope distance 0.208433, RMS ratio 0.887555, score 0.650029. ZynPadReference now has a test-only `harmonics` mode; Ghost's first 32 imported harmonic ratios match Zyn's OscilGen output, so the remaining Ghost pressure is not first-page harmonic extraction. A 1024-sample OscilGen table experiment improved Ghost but regressed DoublePadBass and was cut; a 64-partial expansion worsened Ghost and was cut. Do not claim Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter envelope/LFO ownership are mapped or deliberately excluded." + "next": "Continue Zyn PAD parity from the sampled OscilGen stage-order, multi-kit PAD, Zyn-curve static global-filter, low-pass-order, PAD frequency-detune, low-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, Q+order cascade, and one-based OscilGen oscillator-filter index 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 after Q/order cascade reports log-mel 0.247748, envelope distance 0.208433, RMS ratio 0.887555, score 0.650029. Soft Pad is accepted by ear. ZynPadReference now has a test-only `harmonics` mode; Ghost's first 32 imported harmonic ratios match Zyn's OscilGen output, so the remaining Ghost pressure is not first-page harmonic extraction. sin2x exposed that Zyn oscillator filters are one-based by harmonic number; preserving that moves the type-13 spike from Aquarium's accidental h3 back to h2, improving centroid ratio from 1.227953 to 0.998145 and log-mel from 0.491437 to 0.479782. Organ Choir remains slightly tinny by ear and sin2x remains loud/concentrated, so do not claim full Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter envelope/LFO ownership are mapped or deliberately excluded." }, "note_and_envelope_model": { "rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.", @@ -101,8 +101,8 @@ "binding_rule": "Parameter binding uses field-site references such as `lpf=@/macro/brightness` and `/opgraphs/0/operators/2/level`; the field owns meaning and the parameter owns the runtime value. Avoid target-routing semantics unless a new graph authority is deliberately mapped." }, "last_verified": { - "command": "powershell -NoProfile -ExecutionPolicy Bypass -File tools\\zyn-reference\\build-zyn-reference.ps1; dotnet test AquariumSynthCSharp.slnx --no-restore", - "result": "Added PAD fixture survey; pad-pure and pad-texture match, pad-harmonic gives a real second rung; C# suite 98 passed", - "commit": "pending Zyn PAD fixture survey" + "command": "dotnet test AquariumSynthCSharp.slnx --no-restore", + "result": "Fixed Zyn OscilGen oscillator-filter harmonic indexing for sin2x; C# suite 106 passed", + "commit": "pending Zyn oscillator filter harmonic indexing" } } diff --git a/state/scratch.md b/state/scratch.md index fbc8e17..08a4836 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -180,6 +180,25 @@ Current user-ear correction: centroid ratio `1.006808`, score `0.57229`. DoublePadBass remains at log-mel `0.247748`, envelope `0.208433`, RMS ratio `0.887555`, score `0.650029`. +- User accepted Ghost, DoublePadBass, and Soft Pad as nigh-indistinguishable + by ear, with Organ Choir still a bit tinny and sin2x still overdoing a high + harmonic. The sin2x issue was a real OscilGen indexing bug, not a fixture + brightness preference: Zyn oscillator filters receive one-based harmonic + indices (`filter(1)` is the first harmonic), while Aquarium had translated + them through a zero-based index and moved the type-13 spike filter from h2 to + h3. `ZynOscilFilterGain` now preserves Zyn's one-based index, and the + upstream sin2x regression asserts h2 dominates h3 by more than 10x. +- Latest sin2x after the oscillator-filter index fix: + artifact `artifacts/parity/zyn-upstream-pad-fixtures/0002-sin2x-pad-20260515T140540381`, + log-mel `0.479782` (from `0.491437`), envelope distance `0.490113`, RMS + ratio `1.28196`, centroid ratio `0.998145` (from `1.227953`), score + `0.430193`. The wrong overtone placement is fixed: remaining sin2x pressure + is energy concentration/loudness/profile shape, not h3 accidentally wearing + h2's jacket. +- Zyn PAD harmonic profile lowering also now computes the profile amplitude + multiplier coordinate before width scaling, matching Zyn's `origx` + behavior. It was effectively neutral for sin2x because that preset's width is + near full, but the ownership is now faithful to the source formula. Completed this slice: diff --git a/state/spine.yaml b/state/spine.yaml index d875937..a4ad935 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -1,6 +1,6 @@ project: AquariumSynthCSharp spine_version: 1 -updated: 2026-05-15T13:45:00+01:00 +updated: 2026-05-15T14:15:00+01:00 prime_directive: > Build coherent synth machinery. References exist to reveal missing structure, @@ -81,6 +81,7 @@ live_invariants: - 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 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. Zyn-fed PAD banks may additionally carry `zyn_mode`, `zyn_bandwidth`, `zyn_bwscale`, `zyn_profile`, and `zyn_position`; those fields belong to the spectral table generator, not to parity-test side channels. Do not pretend this covers every Zyn OscilGen modulation/waveshaping/filter/adaptive-harmonic path yet. + - Zyn OscilGen oscillator filters use one-based harmonic indices: first harmonic is `1`, not `0`. Preserve that index when translating filter types or spectral peaks move to the wrong partial. - 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. @@ -169,6 +170,19 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-15 + summary: > + Fixed Zyn OscilGen oscillator-filter harmonic indexing after user listening + accepted Ghost, DoublePadBass, and Soft Pad but still heard sin2x + over-emphasizing the wrong high harmonic. Zyn filter functions receive + one-based harmonic numbers, so Aquarium's zero-based translation moved + sin2x's type-13 spike from h2 to h3. Preserving the Zyn index makes the + generated sin2x table h2-dominant, improves centroid ratio from + `1.227953` to `0.998145`, and moves log-mel from `0.491437` to + `0.479782`. Organ Choir remains slightly tinny by ear and sin2x is still + louder/concentrated, so the next pressure is output/profile shape rather + than the wrong overtone index. + verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 106 passed" - date: 2026-05-15 summary: > Added generated Zyn PAD output calibration after user listening found the diff --git a/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs b/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs index 160f75d..2eb94a8 100644 --- a/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs @@ -169,6 +169,25 @@ public sealed class ZynInstrumentTests Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_harmonic_profile"); } + [Fact] + public void UpstreamSin2xOscilFilterKeepsSpikeOnSecondHarmonicWhenAvailable() + { + var root = RepositoryRoot(); + var path = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks", "Pads", "0002-sin2x pad.xiz"); + if (!File.Exists(path)) + { + return; + } + + var rebuild = ZynInstrumentReader.RebuildFirstPadAsAquariumScript(path, tableRootFrequencyHz: 109.999f); + var patch = PatchScript.Parse(rebuild.Script); + + var bank = Assert.Single(patch.SpectralBanks); + var partials = bank.Partials.ToDictionary(partial => (int)MathF.Round(partial.Ratio), partial => partial.Gain); + Assert.True(partials[2] > partials[3] * 10); + Assert.True(partials[2] > partials[1]); + } + [Fact] public async Task SurveysUpstreamGplInstrumentBankWhenAvailable() {