Add Zyn formant motion workout
This commit is contained in:
parent
a7e1d75c0a
commit
788e72e177
9 changed files with 248 additions and 15 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)}";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue