Add Zyn formant motion workout

This commit is contained in:
Meta Vens 2026-05-17 20:34:52 +01:00
commit 788e72e177
9 changed files with 248 additions and 15 deletions

View file

@ -92,9 +92,10 @@ Current upstream bank coverage:
notch filters, PAD layers, named
kit layers, PAD table modes/bandwidth, PAD source-table extraction for the
implemented OscilGen subset, PAD LP/HP/BPF/notch cutoff/order/Q/envelopes,
and staged non-free envelopes.
staged non-free envelopes, and authored vowel-frame formant motion.
- Counted but not translated: ADD, SUB, mixed-engine kits, free envelopes,
active LFO routing, formant filters/motion, and effects.
active LFO routing beyond explicit control lanes, exact Zyn formant position
drivers/smoothing, and effects.
- Unknown PAD filter pressure: formant filters, peak filters, shelf filters,
and other unimplemented PAD global filter categories/types such as
`cat=1 type=0`, `cat=0 type=6`, `cat=0 type=8`, and smaller buckets.
@ -111,12 +112,13 @@ should own the behavior.
## Recommended Next Cut
Start with **named layer routing**, not PAD synthesis.
Start with **ADD/SUB voice extraction or free-envelope point curves**, not more
PAD synthesis.
Reason: the worst Zyn instruments repeatedly combine multiple kit items and
engines. A named layer surface would give additive banks, PAD sources, free
envelopes, and formant motion somewhere coherent to live later. Without it, the
next abstractions risk becoming loose fields on anonymous voices.
Reason: named layers, PAD spectral sources, staged envelopes, native
band-pass/notch filters, and a first vowel-frame formant-motion surface now
exist. The next counted Zyn pressure is no longer "where does this live?" for
formants; it is either ADD/SUB source detail or arbitrary envelope shape.
## First Responses
@ -168,3 +170,16 @@ a PADsynth-style authoring path: frequency-domain harmonic spreading,
deterministic random phase, one inverse FFT, and normalization. This is not full
Zyn PAD engine parity across every harmonic profile and pitch-zone behavior, but
it is now real spectral-table synthesis rather than voice-count theater.
Wide Bass promoted the first formant-motion rung:
```text
voice layer=wide_bass formant_mix=.7 vowel_hz=.39
vowels=600:90:1,1200:160:.7|500:80:1,900:120:.8
```
The `vowels` field is an AquaSynth voice authority: each `|`-separated frame is
a formant bank, and Faust blends adjacent frames cyclically at `vowel_hz`. The
Zyn workout can seed frames from `FORMANT_FILTER` vowel sequences, but exact Zyn
filter-frequency position driving, slowness, and vowel clearness remain pressure
rather than hidden compatibility sludge.

View file

@ -261,7 +261,7 @@ public static class FaustEmitter
{
source.AppendLine($"{name}_phased = {name}_filtered;");
}
source.AppendLine($"{name}_formants = {FormantExpression(name, voice)};");
source.AppendLine($"{name}_formants = {FormantExpression(name, voice, ownerPath, parameters)};");
source.AppendLine($"{name} = (({name}_phased * (1.0 - {formantMix}) + {name}_formants * {formantMix}) * {envelope}{tremolo} * max(0.0, 1.0 + patch_mod_gain + {gain}) * {parameters.Expression(OwnerField(ownerPath, "gain"), voice.Gain)});");
source.AppendLine();
}
@ -495,11 +495,35 @@ public static class FaustEmitter
return ordered;
}
private static string FormantExpression(string name, Voice voice)
private static string FormantExpression(string name, Voice voice, string ownerPath, ParameterMap parameters)
{
if (voice.Formants.Count == 0) return $"{name}_phased";
var gainSum = Math.Max(voice.Formants.Sum(formant => Math.Abs(formant.Gain)), 0.001f);
var parts = voice.Formants.Select(formant =>
if (voice.FormantFrames.Count > 0)
{
if (voice.FormantFrames.Count == 1)
{
return StaticFormantExpression(name, voice.FormantFrames[0].Formants);
}
var frameCount = voice.FormantFrames.Count;
var rate = parameters.Expression(OwnerField(ownerPath, "color/vowel_rate"), voice.FormantFrameRateHz);
var position = $"wrap01(age * {rate}) * {F(frameCount)}";
var frames = voice.FormantFrames.Select((frame, index) =>
{
var distance = $"min(abs(({position}) - {F(index)}), {F(frameCount)} - abs(({position}) - {F(index)}))";
var weight = $"max(0.0, 1.0 - {distance})";
return $"({StaticFormantExpression(name, frame.Formants)}) * ({weight})";
});
return string.Join(" + ", frames);
}
return StaticFormantExpression(name, voice.Formants);
}
private static string StaticFormantExpression(string name, IReadOnlyList<Formant> formants)
{
if (formants.Count == 0) return $"{name}_phased";
var gainSum = Math.Max(formants.Sum(formant => Math.Abs(formant.Gain)), 0.001f);
var parts = formants.Select(formant =>
{
var q = Math.Clamp(formant.FrequencyHz / Math.Max(formant.BandwidthHz, 10), 0.2f, 40);
return $"({name}_phased : fi.resonbp({F(formant.FrequencyHz)}, {F(q)}, 1.0)) * {F(formant.Gain)}";

View file

@ -136,6 +136,8 @@ public sealed record VoiceColor(
public sealed record Formant(float FrequencyHz, float BandwidthHz, float Gain);
public sealed record FormantFrame(IReadOnlyList<Formant> Formants);
public sealed record Modulator(
ModTarget Target,
ModWaveform Waveform = ModWaveform.Sine,
@ -334,6 +336,8 @@ public sealed record Voice
public FrequencyModulation Fm { get; init; } = new();
public VoiceColor Color { get; init; } = new();
public IReadOnlyList<Formant> Formants { get; init; } = Array.Empty<Formant>();
public IReadOnlyList<FormantFrame> FormantFrames { get; init; } = Array.Empty<FormantFrame>();
public float FormantFrameRateHz { get; init; } = 0.5f;
public IReadOnlyList<Modulator> Modulators { get; init; } = Array.Empty<Modulator>();
public float Gain { get; init; } = 0.2f;
}

View file

@ -397,6 +397,9 @@ public static class PatchScript
var formants = TryGetAny(fields, ["formants", "fs"], out var formantSpec)
? ParseFormants(formantSpec, line)
: [];
var formantFrames = TryGetAny(fields, ["vowels", "vowel_frames", "formant_frames"], out var vowelSpec)
? ParseFormantFrames(vowelSpec, line)
: [];
var modulators = TryGetAny(fields, ["mods", "m"], out var mods)
? ParseVoiceModulators(mods, line)
@ -470,6 +473,8 @@ public static class PatchScript
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "color/tremolo_hz"), "tremolo_hz", "th"),
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "color/formant_mix"), "formant_mix", "fmix")),
Formants = formants,
FormantFrames = formantFrames,
FormantFrameRateHz = GetBoundFloat(fields, line, 0.5f, OwnerField(ownerPath, "color/vowel_rate"), "vowel_hz", "vowel_rate", "vowels_hz"),
Modulators = modulators,
Gain = GetBoundFloat(fields, line, 0.2f, OwnerField(ownerPath, "gain"), "gain", "g") * gainScale
};
@ -1232,6 +1237,11 @@ public static class PatchScript
})
.ToList();
private static List<FormantFrame> ParseFormantFrames(string value, int line) =>
value.Split('|', StringSplitOptions.RemoveEmptyEntries)
.Select(frame => new FormantFrame(ParseFormants(frame, line)))
.ToList();
private static List<Modulator> ParseVoiceModulators(string value, int line) =>
value.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(part =>

View file

@ -109,6 +109,13 @@ public sealed record ZynPadRebuild(
IReadOnlyList<ReferenceFeature> MatchedFeatures,
IReadOnlyList<ReferenceFeature> MissingFeatures);
public sealed record ZynFormantMotionRebuild(
string InstrumentName,
int KitItemId,
string Script,
IReadOnlyList<ReferenceFeature> MatchedFeatures,
IReadOnlyList<ReferenceFeature> MissingFeatures);
public sealed record ZynPadTableRoot(float Sample0RootFrequencyHz, float? SelectedRootFrequencyHz = null);
public enum ZynEngine
@ -164,6 +171,92 @@ public static class ZynInstrumentReader
float playbackFrequencyHz = 261.6256f) =>
RebuildEnabledPadsAsAquaSynthScript(File.ReadAllBytes(path), tableRootFrequencyHzByKitItem, playbackFrequencyHz);
public static ZynFormantMotionRebuild RebuildFirstFormantMotionAsAquaSynthScript(
string path,
float playbackFrequencyHz = 110.0f) =>
RebuildFirstFormantMotionAsAquaSynthScript(File.ReadAllBytes(path), playbackFrequencyHz);
public static ZynFormantMotionRebuild RebuildFirstFormantMotionAsAquaSynthScript(
ReadOnlySpan<byte> bytes,
float playbackFrequencyHz = 110.0f)
{
if (playbackFrequencyHz <= 0) throw new ArgumentOutOfRangeException(nameof(playbackFrequencyHz));
var document = ParseDocument(bytes);
var instrument = document.Descendants("INSTRUMENT").FirstOrDefault()
?? throw new ArgumentException("Zyn instrument XML is missing INSTRUMENT");
var name = StringValue(instrument.Element("INFO"), "name", "unnamed");
var item = instrument
.Descendants("INSTRUMENT_KIT_ITEM")
.Where(item => BoolParam(item, "enabled"))
.FirstOrDefault(item => item.Descendants("FORMANT_FILTER").Any())
?? throw new ArgumentException("Zyn instrument XML has no enabled formant filter kit item");
var formantFilter = item
.Descendants("FORMANT_FILTER")
.FirstOrDefault(filter => filter.Ancestors("FILTER_PARAMETERS")
.Elements("FILTER_LFO")
.Any(lfo => IntParam(lfo, "intensity", 0) > 0))
?? item.Descendants("FORMANT_FILTER").First();
var filterLfo = formantFilter
.Ancestors("FILTER_PARAMETERS")
.Elements("FILTER_LFO")
.FirstOrDefault(lfo => IntParam(lfo, "intensity", 0) > 0);
var frameCount = Math.Clamp(IntParam(formantFilter, "sequence_size", 1), 1, 8);
var formantCount = Math.Clamp(IntParam(formantFilter, "num_formants", 3), 1, 6);
var sequence = formantFilter
.Elements("SEQUENCE_POS")
.OrderBy(element => IntAttribute(element, "id"))
.Select(element => IntParam(element, "vowel_id", 0))
.Take(frameCount)
.ToArray();
if (sequence.Length == 0)
{
sequence = Enumerable.Range(0, frameCount).ToArray();
}
var frames = sequence
.Select(vowelId => ParseZynVowelFrame(formantFilter, vowelId, formantCount))
.Where(frame => frame.Count > 0)
.ToList();
if (frames.Count == 0)
{
throw new ArgumentException("Zyn formant filter has no vowel frame formants");
}
var safeName = SafeIdentifier(name);
var vowelText = string.Join("|", frames.Select(frame => string.Join(",", frame.Select(formant => $"{F(formant.FrequencyHz)}:{F(formant.BandwidthHz)}:{F(formant.Gain)}"))));
var vowelRate = filterLfo is null ? 0.5f : Math.Clamp(RealParam(filterLfo, "freq", 0.5f), 0.01f, 20.0f);
var engines = EnabledEnginesForReader(item).ToArray();
var engine = engines.Contains(ZynEngine.AddSynth) ? "add" :
engines.Contains(ZynEngine.SubSynth) ? "sub" :
engines.Contains(ZynEngine.PadSynth) ? "pad" : "formant";
var script = new StringBuilder();
script.AppendLine("# Generated from a ZynAddSubFX formant-motion fixture for parity pressure.");
script.AppendLine("# Scope: first enabled kit item with a FORMANT_FILTER vowel sequence.");
script.AppendLine("patch gain=0.45 soft_clip=true");
script.AppendLine($"layer name={safeName} engine={engine} gain=0.25");
script.AppendLine($"voice layer={safeName} wave=saw freq={F(playbackFrequencyHz)} gain=0.18 gate=1.8 env=rl rates=0.04,0.18,0.9,0.3 levels=1,0.9,0.75,0 formant_mix=0.7 vowel_hz={F(vowelRate)} vowels={vowelText}");
return new ZynFormantMotionRebuild(
name,
IntAttribute(item, "id"),
script.ToString(),
[
new("formant_filter_count", item.Descendants("FORMANT_FILTER").Count().ToString(CultureInfo.InvariantCulture), "Read Zyn FORMANT_FILTER blocks from the enabled kit item."),
new("formant_sequence", string.Join(",", sequence), "Lowered the Zyn vowel sequence into AquaSynth vowel frames."),
new("formant_frames", frames.Count.ToString(CultureInfo.InvariantCulture), "Each frame owns one static formant bank; Faust blends between frames at control/audio expression time."),
new("formant_frame_rate_hz", F(vowelRate), filterLfo is null ? "No active FILTER_LFO was found; the workout uses a slow audible scan." : "Seeded from the active Zyn FILTER_LFO frequency.")
],
[
new("zyn_formant_position_driver", "filter frequency to vowel position", "Zyn derives vowel position from the formant filter cutoff path; AquaSynth currently states an explicit frame rate."),
new("zyn_formant_smoothing", "formant_slowness/vowel_clearness", "AquaSynth blends adjacent frames with a simple cyclic triangular window."),
new("zyn_add_voice_detail", "full ADD voice oscillator/FM stack", "This workout keeps the source deliberately plain so the formant-motion authority is the thing under test.")
]);
}
public static ZynPadRebuild RebuildFirstPadAsAquaSynthScript(
ReadOnlySpan<byte> bytes,
float tableRootFrequencyHz,
@ -473,6 +566,13 @@ public static class ZynInstrumentReader
private static bool IsEnabledPadItem(XElement item) =>
BoolParam(item, "enabled") && EngineEnabled(item, "pad_enabled", "PAD_SYNTH_PARAMETERS");
private static IEnumerable<ZynEngine> EnabledEnginesForReader(XElement item)
{
if (EngineEnabled(item, "add_enabled", "ADD_SYNTH_PARAMETERS")) yield return ZynEngine.AddSynth;
if (EngineEnabled(item, "sub_enabled", "SUB_SYNTH_PARAMETERS")) yield return ZynEngine.SubSynth;
if (EngineEnabled(item, "pad_enabled", "PAD_SYNTH_PARAMETERS")) yield return ZynEngine.PadSynth;
}
private static string StringValue(XElement? root, string name, string fallback) =>
root?.Elements("string")
.FirstOrDefault(element => AttributeValue(element, "name") == name)
@ -1433,6 +1533,43 @@ public static class ZynInstrumentReader
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
}
private static float RealParam(XElement? root, string name, float fallback)
{
var value = root?.Elements("par_real")
.FirstOrDefault(element => AttributeValue(element, "name") == name)
?.Attribute("value")
?.Value;
return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
}
private static IReadOnlyList<Formant> ParseZynVowelFrame(XElement formantFilter, int vowelId, int formantCount)
{
var center = IntParam(formantFilter, "center_freq", 64);
var octaves = IntParam(formantFilter, "octaves_freq", 64);
return formantFilter
.Elements("VOWEL")
.FirstOrDefault(vowel => IntAttribute(vowel, "id") == vowelId)
?.Elements("FORMANT")
.OrderBy(formant => IntAttribute(formant, "id"))
.Take(formantCount)
.Select(formant =>
{
var frequency = ZynFormantFrequencyHz(IntParam(formant, "freq", 64), center, octaves);
var q = Math.Clamp(MathF.Pow(25.0f, (IntParam(formant, "q", 64) - 32.0f) / 64.0f), 0.2f, 40.0f);
var gain = MathF.Pow(0.1f, (1.0f - IntParam(formant, "amp", 127) / 127.0f) * 4.0f);
return new Formant(frequency, Math.Max(10.0f, frequency / q), gain);
})
.ToArray() ?? [];
}
private static float ZynFormantFrequencyHz(int value, int center, int octaves)
{
var centerHz = 10000.0f * MathF.Pow(10.0f, -(1.0f - center / 127.0f) * 2.0f);
var octaveSpan = 0.25f + 10.0f * octaves / 127.0f;
var octaveFactor = MathF.Pow(2.0f, octaveSpan);
return centerHz / MathF.Sqrt(octaveFactor) * MathF.Pow(octaveFactor, Math.Clamp(value, 0, 127) / 127.0f);
}
private static float EnvelopeTime(XElement? envelope, string name, float fallback)
{
var value = IntParam(envelope, name, -1);

View file

@ -100,5 +100,6 @@
{"date":"2026-05-17","topic":"render-session-ownership","lesson":"`AquaSynthRenderSession` owns lazy native Faust compiler loading and one-shot render convenience. Engine hosts should hold a render session and request compiled products; they should not expose their own smoke-render helper or call `AquaSynthNativeCompiler.TryLoad` directly.","evidence":"Added `AquaSynthRenderSession` to AquaSynth.Faust and changed native render tests to use it. This removes the remaining reason for AquariumSynthHost to keep compiler-load policy."}
{"date":"2026-05-17","topic":"package-versioning","lesson":"Use normal SemVer prerelease labels for AquaSynth packages. Do not encode the repo name into the version suffix; `0.1.0-alpha.2` is a real prerelease version, while `0.1.0-aquasynth.2` is project metadata cosplay.","evidence":"Renamed AquaSynth.Core and AquaSynth.Faust package versions from `0.1.0-aquasynth.2` to `0.1.0-alpha.2` before repacking Aquarium's local feed."}
{"date":"2026-05-17","topic":"solution-format","lesson":"AquaSynth uses a normal Visual Studio `.sln` solution because Rider does not reliably like `.slnx`. Keep tests, state, and docs looking for `AquaSynth.sln`, not `AquaSynth.slnx`.","evidence":"Generated `AquaSynth.sln` with `dotnet new sln --format sln`, added Core/Faust/tests projects, removed `AquaSynth.slnx`, and replaced solution references across tests/state."}
{"date":"2026-05-17","topic":"zyn-formant-motion","lesson":"AquaSynth now owns a first vowel-frame motion surface instead of treating Zyn formants as static filters or loose counted pressure. Voice `vowels=` stores `|`-separated formant banks and `vowel_hz` scans them; Zyn `FORMANT_FILTER` sequences can seed this, while exact Zyn cutoff-position driving, formant slowness, and vowel clearness remain named pressure.","evidence":"Added `FormantFrame`, voice `FormantFrames`/`FormantFrameRateHz`, parser support for `vowels=`/`vowel_hz`, Faust cyclic frame blending, and `ZynInstrumentReader.RebuildFirstFormantMotionAsAquaSynthScript`. `UpstreamWideBassExportsFormantMotionWorkoutWhenAvailable` exercises `Companion/0056-Wide Bass.xiz`; focused PatchScript/ZynInstrument tests passed 73/73 and non-reference tests passed 103/103."}

View file

@ -91,6 +91,7 @@ live_invariants:
- 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.
- Zyn coverage is now tracked as handled/counted/unknown buckets by `ZynInstrumentSurvey.CoverageDirectory`; do not treat broad parse counts as expressivity. Unknown PAD filter/waveshaper/OscilGen buckets and counted ADD/SUB/LFO/free-envelope/formant/effect buckets need parity workouts before they graduate into AquaSynth model changes.
- Voice `vowels=` owns authored formant-frame motion: each frame is a static formant bank and `vowel_hz` scans/blends the frames in Faust. Zyn `FORMANT_FILTER` vowel sequences may seed this surface, but Zyn's exact cutoff-position driver, formant slowness, and vowel clearness are still pressure, not hidden compatibility logic.
- 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.
@ -113,11 +114,11 @@ 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. `ZynInstrumentSurvey.CoverageDirectory` now classifies observed Zyn features into handled/counted/unknown buckets and writes `artifacts/parity/zyn-upstream-bank-survey/coverage.md` from the upstream GPL bank. First AquaSynth 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. `ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript` now emits per-preset PAD `.aqua` rebuilds for enabled PAD kit items with OSCIL harmonic expansion, oracle-reported table base frequency, PAD detune, Zyn PAD profile/bandwidth/position semantics, static/filter-envelope low-pass mapping, low-pass note tracking, explicit low-pass Q, logarithmic Zyn envelope times, and calibrated PAD output gain. 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`, including a test-only `harmonics` mode for dumping normalized OscilGen amplitudes before PAD generation. The optional PAD reports emit note and source-table WAV/listening artifacts. Latest accepted upstream PAD pressure has DoublePadBass close enough by ear with log-mel 0.247748 / score 0.650029 after Q/order cascade, while Ghost Ensemble improves to log-mel 0.369402 / score 0.57229. The remaining Ghost gap is no longer first-32 OscilGen harmonic extraction; oracle harmonic dumps match those ratios. The upstream GPL instrument bank is initialized at `external/zynaddsubfx/instruments` commit c5c912131b31df5fdf372d2f06a25aaf2375837f; survey tests parse 1318/1318 .xiz files, write a pressure report and coverage map, and render a five-patch upstream PAD listening batch under ignored artifacts."
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. `ZynInstrumentSurvey.CoverageDirectory` now classifies observed Zyn features into handled/counted/unknown buckets and writes `artifacts/parity/zyn-upstream-bank-survey/coverage.md` from the upstream GPL bank. First AquaSynth 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. Voices can now use `vowels=` formant frames plus `vowel_hz`; `ZynInstrumentReader.RebuildFirstFormantMotionAsAquaSynthScript` seeds that surface from Zyn `FORMANT_FILTER` vowel sequences for Wide Bass-style workouts while keeping exact Zyn position-driver/smoothing as named missing features. `ZynInstrumentReader.RebuildFirstPadAsAquaSynthScript` now emits per-preset PAD `.aqua` rebuilds for enabled PAD kit items with OSCIL harmonic expansion, oracle-reported table base frequency, PAD detune, Zyn PAD profile/bandwidth/position semantics, static/filter-envelope low-pass mapping, low-pass note tracking, explicit low-pass Q, logarithmic Zyn envelope times, and calibrated PAD output gain. 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`, including a test-only `harmonics` mode for dumping normalized OscilGen amplitudes before PAD generation. The optional PAD reports emit note and source-table WAV/listening artifacts. Latest accepted upstream PAD pressure has DoublePadBass close enough by ear with log-mel 0.247748 / score 0.650029 after Q/order cascade, while Ghost Ensemble improves to log-mel 0.369402 / score 0.57229. The remaining Ghost gap is no longer first-32 OscilGen harmonic extraction; oracle harmonic dumps match those ratios. The upstream GPL instrument bank is initialized at `external/zynaddsubfx/instruments` commit c5c912131b31df5fdf372d2f06a25aaf2375837f; survey tests parse 1318/1318 .xiz files, write a pressure report and coverage map, and render a five-patch upstream PAD listening batch under ignored artifacts."
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."
- "Additive harmonic banks, PAD spectral clouds, and staged voice envelopes now exist; prove any next syntax across more than one target with parity, not parser-only fixtures."
- "Use Wide Bass formant-frame output as the foothold before attempting exact Zyn formant cutoff-position driving."
- "Additive harmonic banks, PAD spectral clouds, staged voice envelopes, and vowel-frame motion now exist; prove any next syntax across more than one target with parity, not parser-only fixtures."
- "Do not add all remaining pressures at once."
faust:
priority: 3

View file

@ -65,6 +65,22 @@ public sealed class PatchScriptTests
Assert.Equal(.01f, lane.Modulator.Bias, 5);
}
[Fact]
public void ParserSupportsVoiceVowelFrames()
{
var patch = PatchScript.Parse("v w=saw f=90 fmix=.7 vowel_hz=.8 vowels=600:90:1,1200:160:.7|500:80:1,900:120:.8");
var voice = Assert.Single(patch.Voices);
Assert.Equal(2, voice.FormantFrames.Count);
Assert.Equal(2, voice.FormantFrames[0].Formants.Count);
Assert.Equal(.8f, voice.FormantFrameRateHz, 5);
var faust = FaustEmitter.Emit(patch).Source;
Assert.Contains("wrap01(age * 0.8)", faust);
Assert.Contains("fi.resonbp(600", faust);
Assert.Contains("fi.resonbp(500", faust);
}
[Fact]
public void ParserPreservesDeclaredPatchParameters()
{

View file

@ -225,6 +225,31 @@ public sealed class ZynInstrumentTests
Assert.True(octavePartials[4] < 0.001f);
}
[Fact]
public void UpstreamWideBassExportsFormantMotionWorkoutWhenAvailable()
{
var root = RepositoryRoot();
var path = Path.Combine(root, "external", "zynaddsubfx", "instruments", "banks", "Companion", "0056-Wide Bass.xiz");
if (!File.Exists(path))
{
return;
}
var rebuild = ZynInstrumentReader.RebuildFirstFormantMotionAsAquaSynthScript(path);
var patch = PatchScript.Parse(rebuild.Script);
var voice = Assert.Single(patch.Voices);
Assert.Equal("Wide Bass", rebuild.InstrumentName);
Assert.True(voice.FormantFrames.Count >= 3);
Assert.All(voice.FormantFrames, frame => Assert.True(frame.Formants.Count >= 3));
Assert.Contains("vowels=", rebuild.Script);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "formant_sequence");
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "zyn_formant_position_driver");
var faust = FaustEmitter.Emit(patch).Source;
Assert.Contains("wrap01(age *", faust);
}
[Fact]
public void RebuildMapsZynStateVariablePadLowPassToAquaFilterAuthority()
{