Map Zyn OscilGen adaptive source shaping

This commit is contained in:
Meta Vens 2026-05-15 17:33:23 +01:00
commit b4ab616513
6 changed files with 186 additions and 13 deletions

View file

@ -286,7 +286,7 @@ public static class ZynInstrumentReader
var amplitude = pad.Element("AMPLITUDE_PARAMETERS");
var envelope = amplitude?.Element("AMPLITUDE_ENVELOPE");
var oscillator = pad.Element("OSCIL");
var harmonics = ParsePadOscillatorHarmonics(oscillator, maxHarmonics: 64).ToList();
var harmonics = ParsePadOscillatorHarmonics(oscillator, tableRootFrequencyHz, maxHarmonics: 64).ToList();
if (harmonics.Count == 0)
{
harmonics.Add(new HarmonicPartial(1, 0.16f));
@ -321,6 +321,14 @@ public static class ZynInstrumentReader
var featurePrefix = includeKitSuffix ? $"pad_kit_{kitId}_" : "pad_";
matched.Add(new($"{featurePrefix}oscillator_harmonics", harmonics.Count.ToString(CultureInfo.InvariantCulture), "Mapped OSCIL/HARMONICS magnitudes into Aquarium spectrum partials."));
matched.Add(new($"{featurePrefix}oscillator_base_function", IntParam(oscillator, "base_function", 0).ToString(CultureInfo.InvariantCulture), "Expanded Zyn OscilGen base function into harmonic partials before PAD synthesis."));
if (IntParam(oscillator, "base_function_modulation", 0) != 0)
{
matched.Add(new($"{featurePrefix}oscillator_base_function_modulation", IntParam(oscillator, "base_function_modulation", 0).ToString(CultureInfo.InvariantCulture), "Applied Zyn OscilGen base-function phase modulation before PAD synthesis."));
}
if (IntParam(oscillator, "adaptive_harmonics", 0) != 0)
{
matched.Add(new($"{featurePrefix}oscillator_adaptive_harmonics", IntParam(oscillator, "adaptive_harmonics", 0).ToString(CultureInfo.InvariantCulture), "Applied Zyn adaptive harmonic remapping at the PAD table root frequency."));
}
matched.Add(new($"{featurePrefix}oscillator_spectrum_adjust", $"{IntParam(oscillator, "spectrum_adjust_type", 0)}:{IntParam(oscillator, "spectrum_adjust_par", 64)}", "Applied Zyn OscilGen spectrum adjustment to generated harmonic partials."));
matched.Add(new($"{featurePrefix}table_root", F(tableRootFrequencyHz), "Root frequency comes from the Zyn oracle generated sample basefreq."));
matched.Add(new($"{featurePrefix}note_frequency", F(noteFrequencyHz), "Mapped Zyn PAD coarse/fine detune into Aquarium table playback frequency."));
@ -442,7 +450,7 @@ public static class ZynInstrumentReader
private static string? AttributeValue(XElement element, string name) =>
element.Attribute(name)?.Value;
private static IReadOnlyList<HarmonicPartial> ParsePadOscillatorHarmonics(XElement? oscillator, int maxHarmonics)
private static IReadOnlyList<HarmonicPartial> ParsePadOscillatorHarmonics(XElement? oscillator, float tableRootFrequencyHz, int maxHarmonics)
{
var harmonicMagnitudes = oscillator?.Element("HARMONICS")?.Elements("HARMONIC")
.Select(ParsePadHarmonicMagnitude)
@ -464,7 +472,7 @@ public static class ZynInstrumentReader
.ToArray() ?? [];
}
var freqs = ZynOscilGenFrequencies(oscillator, harmonicMagnitudes, maxHarmonics);
var freqs = ZynOscilGenFrequencies(oscillator, harmonicMagnitudes, tableRootFrequencyHz, maxHarmonics);
var gains = freqs.Select(value => value.Magnitude).ToArray();
ApplyZynSpectrumAdjust(
gains,
@ -485,6 +493,7 @@ public static class ZynInstrumentReader
IntParam(oscillator, "wave_shaping_function", 0) != 0 ||
IntParam(oscillator, "filter_type", 0) != 0 ||
IntParam(oscillator, "modulation", 0) != 0 ||
IntParam(oscillator, "base_function_modulation", 0) != 0 ||
IntParam(oscillator, "spectrum_adjust_type", 0) != 0 ||
(IntParam(oscillator, "base_function", 0) != 0 && harmonicMagnitudes.Count > 1) ||
IntParam(oscillator, "harmonic_shift", 0) != 0 ||
@ -493,6 +502,7 @@ public static class ZynInstrumentReader
private static Complex[] ZynOscilGenFrequencies(
XElement? oscillator,
IReadOnlyList<HarmonicPartial> harmonicMagnitudes,
float tableRootFrequencyHz,
int maxHarmonics)
{
var freqs = Dft(ZynOscilGenBaseSamples(oscillator, harmonicMagnitudes), maxHarmonics);
@ -519,6 +529,7 @@ public static class ZynInstrumentReader
ApplyZynHarmonicShift(freqs, IntParam(oscillator, "harmonic_shift", 0));
}
ApplyZynAdaptiveHarmonics(freqs, oscillator, tableRootFrequencyHz);
return freqs;
}
@ -536,9 +547,10 @@ public static class ZynInstrumentReader
for (var i = 0; i < sampleCount; i++)
{
var t = i / (double)sampleCount * harmonicNumber;
var phase = ZynBaseFunctionPhase(oscillator, t);
var sample = baseFunction == 0
? -Math.Sin(Math.Tau * t)
: ZynBaseFunction(baseFunction, t, parameterValue);
? -Math.Sin(Math.Tau * phase)
: ZynBaseFunction(baseFunction, phase, parameterValue);
samples[i] += sample * harmonic.Gain;
}
}
@ -547,6 +559,43 @@ public static class ZynInstrumentReader
return samples;
}
private static double ZynBaseFunctionPhase(XElement? oscillator, double phase)
{
var type = IntParam(oscillator, "base_function_modulation", 0);
if (type == 0)
{
return phase - Math.Floor(phase);
}
var p1 = IntParam(oscillator, "base_function_modulation_par1", 64) / 127.0;
var p2 = IntParam(oscillator, "base_function_modulation_par2", 64) / 127.0;
var p3 = IntParam(oscillator, "base_function_modulation_par3", 32) / 127.0;
phase = type switch
{
1 => phase * ZynBaseModulationRevMultiplier(p3) +
Math.Sin((phase + p2) * Math.Tau) * ((Math.Pow(2.0, p1 * 5.0) - 1.0) / 10.0),
2 => phase +
Math.Sin((phase * (1.0 + Math.Floor(Math.Pow(2.0, p3 * 5.0) - 1.0)) + p2) * Math.Tau) *
((Math.Pow(2.0, p1 * 5.0) - 1.0) / 10.0),
3 => phase +
Math.Pow((1.0 - Math.Cos((phase + p2) * Math.Tau)) * 0.5,
0.01 + (Math.Pow(2.0, p3 * 16.0) - 1.0) / 10.0) *
((Math.Pow(2.0, p1 * 7.0) - 1.0) / 10.0),
4 => phase * Math.Pow(2.0,
IntParam(oscillator, "base_function_modulation_par1", 64) / 32.0 +
IntParam(oscillator, "base_function_modulation_par2", 64) / 2048.0) + p3,
_ => phase
};
return phase - Math.Floor(phase);
}
private static double ZynBaseModulationRevMultiplier(double parameter)
{
var multiplier = Math.Floor(Math.Pow(2.0, parameter * 5.0) - 1.0);
return multiplier < 0.9999 ? -1.0 : multiplier;
}
private static Complex[] Dft(IReadOnlyList<double> samples, int maxHarmonics)
{
var freqs = new Complex[maxHarmonics + 1];
@ -702,6 +751,103 @@ public static class ZynInstrumentReader
}
}
private static void ApplyZynAdaptiveHarmonics(Complex[] freqs, XElement? oscillator, float baseFrequencyHz)
{
var mode = IntParam(oscillator, "adaptive_harmonics", 0);
if (mode == 0)
{
return;
}
var input = freqs.ToArray();
Array.Clear(freqs);
input[0] = Complex.Zero;
var baseFrequency = 30.0 * Math.Pow(10.0, IntParam(oscillator, "adaptive_harmonics_base_frequency", 128) / 128.0);
var power = (IntParam(oscillator, "adaptive_harmonics_power", 100) + 1.0) / 101.0;
var ratio = Math.Pow(Math.Max(1.0, baseFrequencyHz) / baseFrequency, power);
var down = false;
if (ratio > 1.0)
{
ratio = 1.0 / ratio;
down = true;
}
for (var i = 0; i < freqs.Length - 2; i++)
{
var position = i * ratio;
var high = (int)position;
var low = position - high;
if (high >= freqs.Length - 2)
{
break;
}
if (down)
{
freqs[high] += (1.0 - low) * input[i];
freqs[high + 1] += low * input[i];
}
else
{
freqs[i] = (1.0 - low) * input[high] + low * input[high + 1];
}
}
if (!down)
{
freqs[0] *= ratio;
}
if (freqs.Length > 1)
{
freqs[1] += freqs[0];
}
freqs[0] = Complex.Zero;
ApplyZynAdaptiveHarmonicPostprocess(freqs, mode, IntParam(oscillator, "adaptive_harmonics_par", 50));
}
private static void ApplyZynAdaptiveHarmonicPostprocess(Complex[] freqs, int mode, int parameter)
{
if (mode <= 1)
{
return;
}
var par = 1.0 - Math.Pow(1.0 - parameter * 0.01, 1.5);
var input = freqs.ToArray();
for (var i = 1; i < freqs.Length; i++)
{
freqs[i] *= 1.0 - par;
input[i] *= par;
}
if (mode == 2)
{
for (var i = 1; i < freqs.Length; i += 2)
{
freqs[i] += input[i];
}
return;
}
var harmonic = (mode - 3) / 2 + 2;
var subtractVsAdd = (mode - 3) % 2;
if (subtractVsAdd == 0)
{
for (var i = harmonic; i < freqs.Length; i += harmonic)
{
freqs[i] += input[i];
}
}
else
{
for (var source = 1; source * harmonic < freqs.Length; source++)
{
freqs[source * harmonic] += input[source];
}
}
}
private static void NormalizePeak(double[] samples)
{
var peak = samples.Select(Math.Abs).DefaultIfEmpty(0).Max();

View file

@ -87,3 +87,4 @@
{"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."}
{"date":"2026-05-15","topic":"zyn-organ-highpass-selected-root","lesson":"Organ Choir's coherent octave blast needs real Zyn PAD high-pass ownership and selected table roots for OscilGen shift-base sources, but selected roots must not become the global default. The source rule is narrow: keep sample-0 roots unless the PAD OscilGen base function is the shift source (`base_function=7`) and the oracle provides the selected note table root.","evidence":"Organ Choir kit0 has analog HP2 with stages and FILTER_ENVELOPE, while kit1 uses base_function=7. Mapping hpf_order/hpf_env and using the selected 622.25 Hz table root only for kit1 moved the candidate's dominant peak to 522.67 Hz with 261 Hz at -1.29 dB, close to Zyn's 523.33 Hz lead and 261 Hz at -2.23 dB. Latest Organ log-mel is 0.499122 / score 0.51966, while accepted Soft Pad, Ghost, and DoublePadBass remain on sample-0 roots; global selected roots had regressed DoublePadBass."}
{"date":"2026-05-15","topic":"zyn-organ-basefunc-mod-adaptive","lesson":"Organ Choir's violin-vs-wind mismatch was OscilGen source-table ownership, not pitch, envelope, or gain. Zyn `base_function_modulation` warps the phase before base-function sampling, and `adaptive_harmonics` remaps the prepared oscillator spectrum at the PAD table root; both must happen before PAD bandwidth/profile spreading.","evidence":"Before the fix, Aqua kit1 partials formed a smooth ramp (h3 -3.5 dB, h4 -5.1 dB, h8 -10.2 dB relative to h1) while Zyn's harmonic dump was sparse (h3 -27.1 dB, h4 -70.3 dB, h8 -50.2 dB). Applying base_function_modulation=1 and adaptive_harmonics=1 moved kit1 table energy to 400-800 Hz 0.2713 vs Zyn 0.2780 and 8-16 kHz 0.1471 vs Zyn 0.1355; Organ log-mel improved to 0.471483 and score to 0.541933 without changing accepted DoublePadBass, Ghost, or Soft Pad."}

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/high-pass order, PAD frequency-detune, low-pass/high-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, Q+order cascade, one-based OscilGen oscillator-filter index, and base-function-7 selected-table-root 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 reports log-mel 0.247674, envelope distance 0.208351, RMS ratio 0.888011, score 0.650319. Soft Pad is accepted by ear. Organ Choir now maps kit0 HP2/stage/filter-envelope motion and uses the selected table root only for its OscilGen shift source (`base_function=7`) kit; latest Organ log-mel is 0.499122, envelope distance 0.295493, RMS ratio 0.900147, centroid ratio 1.010911, and score 0.51966, with the 523 Hz choir voice leading instead of the 261 Hz body. ZynPadReference has test-only `harmonics` and `selected` modes; selected roots are diagnostic/source-specific, not a global default, because global selected roots regressed accepted DoublePadBass. sin2x remains loud/concentrated after the one-based filter-index fix, so do not claim full Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter 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/high-pass order, PAD frequency-detune, low-pass/high-pass filter-envelope, logarithmic Zyn envelope-time, generated Zyn PAD output-calibration, low-pass note tracking, explicit low-pass Q, Q+order cascade, one-based OscilGen oscillator-filter index, base-function-7 selected-table-root, base-function phase modulation, and adaptive-harmonics 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 reports log-mel 0.247674, envelope distance 0.208351, RMS ratio 0.888011, score 0.650319. Soft Pad is accepted by ear. Organ Choir now maps kit0 HP2/stage/filter-envelope motion and kit1's selected table root, `base_function_modulation=1`, and `adaptive_harmonics=1`; latest Organ log-mel is 0.471483, envelope distance 0.302978, RMS ratio 0.878, centroid ratio 1.011328, and score 0.541933. Its kit1 PAD table energy now matches the wind-body shape much more closely: 400-800 Hz 0.2713 vs Zyn 0.2780 and 8-16 kHz 0.1471 vs Zyn 0.1355. ZynPadReference has test-only `harmonics` and `selected` modes; selected roots are diagnostic/source-specific, not a global default, because global selected roots regressed accepted DoublePadBass. sin2x remains loud/concentrated after the one-based filter-index fix, so do not claim full Zyn PAD parity until broader OscilGen filtering/waveshaping/adaptive-harmonic behavior and note/global filter 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.",
@ -102,7 +102,7 @@
},
"last_verified": {
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "Mapped Zyn PAD high-pass motion and base-function-7 selected table roots for Organ Choir; C# suite 107 passed",
"commit": "pending Zyn Organ Choir PAD root/filter mapping"
"result": "Mapped Zyn base-function modulation and adaptive harmonics for Organ Choir; C# suite 107 passed",
"commit": "pending Zyn Organ Choir OscilGen source-table mapping"
}
}

View file

@ -27,11 +27,16 @@ Current slice:
selected note table root when the parity oracle provides it.
- Latest upstream PAD survey after this pass:
`Organ Choir Pad2` artifact
`artifacts/parity/zyn-upstream-pad-fixtures/0008-organ-choir-pad2-20260515T153520870`,
log-mel `0.499122`, envelope `0.295493`, RMS ratio `0.900147`,
centroid ratio `1.010911`, score `0.51966`. Peak anatomy now has the
candidate led by the 523 Hz choir voice, with 261 Hz body at about `-1.29dB`
versus Zyn's `-2.23dB`.
`artifacts/parity/zyn-upstream-pad-fixtures/0008-organ-choir-pad2-20260515T162034810`,
log-mel `0.471483`, envelope `0.302978`, RMS ratio `0.878`,
centroid ratio `1.011328`, score `0.541933`. The violin-vs-wind issue was
kit1 source-table shape: Aqua had ignored Zyn `base_function_modulation=1`
and `adaptive_harmonics=1`, producing a smooth upper harmonic ramp. The
importer now applies base-function phase modulation before DFT and adaptive
harmonic remapping at the PAD table root.
- Direct kit1 table spectrum moved from violin-shaped to wind-shaped:
400-800 Hz energy is now `0.2713` against Zyn `0.2780`; 8-16 kHz is
`0.1471` against Zyn `0.1355`.
- The accepted fixtures stayed on the sample-0 root path: DoublePadBass
log-mel `0.247674`, score `0.650319`; Ghost Ensemble log-mel `0.369402`,
score `0.57229`; Soft Pad log-mel `0.317897`, score `0.608833`.

View file

@ -84,6 +84,8 @@ live_invariants:
- 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.
- Zyn PAD analog high-pass filters are filter authority too: HP1/HP2 type, stages, and FILTER_ENVELOPE motion lower through `hpf_order` and `hpf_env=rl`, not ad hoc layer gain or brightness compensation.
- Zyn PAD selected table roots are not a global default in Aquarium lowering. Keep sample-0 roots unless a source rule earns the selected root; currently OscilGen shift-base sources (`base_function=7`) may use the selected note table root because Organ Choir proved sample-0 roots leave the octave voice misaligned.
- Zyn OscilGen base-function modulation is source-table authority: apply it as phase modulation before base-function sampling, not as later oscillator modulation or filter/EQ compensation.
- Zyn adaptive harmonics remap the prepared oscillator spectrum at the PAD table root frequency before PAD bandwidth/profile spreading. Do not skip it for sources whose wind/body character depends on sparse remapped harmonics.
- 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.
@ -172,6 +174,19 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-15
summary: >
Fixed Organ Choir's violin-vs-wind source-table mismatch by mapping Zyn
OscilGen base-function phase modulation and adaptive harmonics. Kit1's
`base_function=7` source also has `base_function_modulation=1` and
`adaptive_harmonics=1`; ignoring them produced a smooth bowed-string
harmonic ramp. The importer now phase-warps the base function before DFT
and remaps the prepared oscillator spectrum at the selected PAD table
root. Kit1 table energy now lands near the Zyn body/air balance
(400-800 Hz 0.2713 vs 0.2780; 8-16 kHz 0.1471 vs 0.1355), and Organ
improves to log-mel 0.471483 / score 0.541933 without changing accepted
DoublePadBass, Ghost, or Soft Pad.
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 107 passed"
- date: 2026-05-15
summary: >
Mapped Organ Choir's remaining PAD voice-alignment issue to two real Zyn

View file

@ -213,8 +213,14 @@ public sealed class ZynInstrumentTests
Assert.Contains("spectrum layer=organ_choir_pad2_1 root=622.25", rebuild.Script);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_0_filter_hpf_envelope");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_1_oscillator_base_function" && feature.Value == "7");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_1_oscillator_base_function_modulation" && feature.Value == "1");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "pad_kit_1_oscillator_adaptive_harmonics" && feature.Value == "1");
Assert.Equal(77.7813f, patch.SpectralBanks[0].RootFrequencyHz, precision: 4);
Assert.Equal(622.25f, patch.SpectralBanks[1].RootFrequencyHz, precision: 4);
var octavePartials = patch.SpectralBanks[1].Partials.ToDictionary(partial => (int)MathF.Round(partial.Ratio), partial => partial.Gain);
Assert.True(octavePartials[2] > 0.09f);
Assert.True(octavePartials[3] < 0.01f);
Assert.True(octavePartials[4] < 0.001f);
}
[Fact]