Fix Zyn oscillator filter harmonic indexing

This commit is contained in:
Meta Vens 2026-05-15 15:17:01 +01:00
commit e53ea515ab
7 changed files with 60 additions and 7 deletions

View file

@ -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;

View file

@ -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),

View file

@ -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."}

View file

@ -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"
}
}

View file

@ -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:

View file

@ -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

View file

@ -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()
{