Add named layer DSL surface
This commit is contained in:
parent
8426ca50f4
commit
62ea9bee17
11 changed files with 188 additions and 27 deletions
|
|
@ -134,6 +134,12 @@ The current external-bank pressure survey is in
|
|||
signal is that named kit/layer routing should probably precede syntax golf for
|
||||
PAD, additive banks, or formant motion.
|
||||
|
||||
The first language-golf response is `layer`: a named layer declaration owns
|
||||
engine tags, optional MIDI key range metadata, default gain, and an effect-send
|
||||
label while lowering to ordinary Aquarium voices. It is intentionally only an
|
||||
ownership/routing scaffold for now; it does not add PAD synthesis, free
|
||||
envelopes, or effect buses by stealth.
|
||||
|
||||
Sources:
|
||||
|
||||
- [ZynAddSubFX](https://github.com/zynaddsubfx/zynaddsubfx)
|
||||
|
|
|
|||
|
|
@ -61,3 +61,16 @@ 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.
|
||||
|
||||
## First Response
|
||||
|
||||
Aquarium now has a minimal `layer` command:
|
||||
|
||||
```text
|
||||
layer name=pad_low engine=pad min_key=36 max_key=84 gain=.07
|
||||
voice layer=pad_low freq=130.8128
|
||||
```
|
||||
|
||||
The layer owns source identity and metadata while current lowering still emits
|
||||
ordinary voices. This is the scaffold needed before promoting PAD sources,
|
||||
additive harmonic banks, free envelopes, or richer formant motion.
|
||||
|
|
|
|||
|
|
@ -790,10 +790,13 @@ public static class BuiltInScripts
|
|||
vibrato=@/macro/motion
|
||||
vibrato_hz=5.5
|
||||
|
||||
voice freq=220 gain=0.16
|
||||
voice freq=440 gain=0.075
|
||||
voice freq=660 gain=0.045
|
||||
voice freq=880 gain=0.025
|
||||
layer name=body engine=add min_key=36 max_key=96 gain=0.16
|
||||
layer name=shine engine=add min_key=48 max_key=108 gain=0.045
|
||||
|
||||
voice layer=body freq=220
|
||||
voice layer=body freq=440 gain=0.075
|
||||
voice layer=shine freq=660
|
||||
voice layer=shine freq=880 gain=0.025
|
||||
""";
|
||||
|
||||
public const string ZynStylePadTexture = """
|
||||
|
|
@ -827,10 +830,14 @@ public static class BuiltInScripts
|
|||
lpf=0.18
|
||||
gain=0.05
|
||||
|
||||
voice freq=130.8128 gain=0.07 vibrato=@/macro/spread vibrato_hz=0.09
|
||||
voice freq=196 gain=0.052 vibrato=0.012 vibrato_hz=0.13
|
||||
voice freq=261.6256 gain=0.044 wave=triangle vibrato=0.009 vibrato_hz=0.11
|
||||
voice wave=noise freq=1800 gain=0.018 noise=0.82 attack=0.8 sustain=1.7 decay=1.1 lpf=0.36 hpf=0.22
|
||||
layer name=pad_low engine=pad min_key=36 max_key=84 gain=0.07 vibrato=@/macro/spread vibrato_hz=0.09
|
||||
layer name=pad_high engine=pad min_key=48 max_key=108 gain=0.044 vibrato=0.009 vibrato_hz=0.11
|
||||
layer name=air engine=add min_key=0 max_key=127 gain=0.018 wave=noise noise=0.82 attack=0.8 sustain=1.7 decay=1.1 lpf=0.36 hpf=0.22
|
||||
|
||||
voice layer=pad_low freq=130.8128
|
||||
voice layer=pad_low freq=196 gain=0.052 vibrato=0.012 vibrato_hz=0.13
|
||||
voice layer=pad_high freq=261.6256 wave=triangle
|
||||
voice layer=air freq=1800
|
||||
""";
|
||||
|
||||
public const string ZynStyleVocalLayer = """
|
||||
|
|
@ -861,27 +868,27 @@ public static class BuiltInScripts
|
|||
formant_mix=0.22
|
||||
gain=0.04
|
||||
|
||||
layer name=air engine=add min_key=48 max_key=108 gain=0.11 send=formant
|
||||
layer name=body engine=sub min_key=24 max_key=72 gain=0.075
|
||||
layer name=breath engine=add min_key=0 max_key=127 gain=0.018 wave=noise noise=0.75 hpf=0.58 lpf=0.62 send=formant
|
||||
|
||||
voice
|
||||
layer=air
|
||||
wave=saw
|
||||
freq=174.614
|
||||
gain=0.11
|
||||
formants=520:90:0.9,1080:150:1,2450:280:0.35
|
||||
formant_mix=@/macro/vowel
|
||||
|
||||
voice
|
||||
layer=body
|
||||
wave=triangle
|
||||
freq=87.307
|
||||
gain=0.075
|
||||
lpf=0.48
|
||||
drive=0.08
|
||||
|
||||
voice
|
||||
wave=noise
|
||||
layer=breath
|
||||
freq=2600
|
||||
gain=0.018
|
||||
noise=0.75
|
||||
hpf=0.58
|
||||
lpf=0.62
|
||||
formants=700:120:0.5,1600:220:0.6
|
||||
formant_mix=0.3
|
||||
""";
|
||||
|
|
|
|||
|
|
@ -186,6 +186,14 @@ public sealed record ReferenceSource(
|
|||
|
||||
public sealed record ReferenceFeature(string Name, string Value, string Notes = "");
|
||||
|
||||
public sealed record PatchLayer(
|
||||
string Name,
|
||||
string Engine = "",
|
||||
int? MinKey = null,
|
||||
int? MaxKey = null,
|
||||
float Gain = 1,
|
||||
string EffectSend = "");
|
||||
|
||||
public sealed record ReferencePatch(
|
||||
string Id,
|
||||
string Family,
|
||||
|
|
@ -199,6 +207,7 @@ public sealed record Repeat(float IntervalSeconds);
|
|||
|
||||
public sealed record Voice
|
||||
{
|
||||
public PatchLayer? Layer { get; init; }
|
||||
public Oscillator Oscillator { get; init; } = new();
|
||||
public Note Note { get; init; } = new();
|
||||
public Envelope Envelope { get; init; } = new();
|
||||
|
|
@ -217,6 +226,7 @@ public sealed record Voice
|
|||
public sealed record SynthPatch
|
||||
{
|
||||
public IReadOnlyList<Voice> Voices { get; init; } = Array.Empty<Voice>();
|
||||
public IReadOnlyList<PatchLayer> Layers { get; init; } = Array.Empty<PatchLayer>();
|
||||
public IReadOnlyList<OperatorGraph> OperatorGraphs { get; init; } = Array.Empty<OperatorGraph>();
|
||||
public IReadOnlyList<ControlLane> Controls { get; init; } = Array.Empty<ControlLane>();
|
||||
public IReadOnlyList<PatchParameter> Parameters { get; init; } = Array.Empty<PatchParameter>();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ public static class PatchScript
|
|||
private readonly List<OperatorGraph> _operatorGraphs = [];
|
||||
private readonly List<PatchParameter> _parameters = [];
|
||||
private readonly List<ParameterBinding> _parameterBindings = [];
|
||||
private readonly List<PatchLayer> _layers = [];
|
||||
private readonly Dictionary<string, string> _defaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _layerDefaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
private PendingOperatorGraph? _pendingOperatorGraph;
|
||||
|
||||
public List<Voice> Voices { get; } = [];
|
||||
|
|
@ -51,6 +53,7 @@ public static class PatchScript
|
|||
return new SynthPatch
|
||||
{
|
||||
Voices = Voices,
|
||||
Layers = _layers,
|
||||
OperatorGraphs = _operatorGraphs,
|
||||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
|
|
@ -90,6 +93,10 @@ public static class PatchScript
|
|||
var name = Required(fields, "name", line);
|
||||
_templates[name] = Without(fields, "name");
|
||||
break;
|
||||
case "layer":
|
||||
FlushPendingOperatorGraph();
|
||||
AddLayer(fields, line);
|
||||
break;
|
||||
case "voice":
|
||||
FlushPendingOperatorGraph();
|
||||
Voices.Add(ParseVoice(ExpandVoiceFields(fields, line), Voices.Count, line));
|
||||
|
|
@ -130,6 +137,14 @@ public static class PatchScript
|
|||
private Dictionary<string, string> ExpandVoiceFields(Dictionary<string, string> fields, int line)
|
||||
{
|
||||
var expanded = new Dictionary<string, string>(_defaults, StringComparer.OrdinalIgnoreCase);
|
||||
if (TryGetAny(fields, ["layer"], out var layerName))
|
||||
{
|
||||
if (!_layerDefaults.TryGetValue(layerName, out var layer))
|
||||
{
|
||||
throw new PatchScriptException(line, $"unknown layer `{layerName}`");
|
||||
}
|
||||
Merge(expanded, layer);
|
||||
}
|
||||
if (TryGetAny(fields, ["use", "u"], out var templateName))
|
||||
{
|
||||
foreach (var name in templateName.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
|
|
@ -145,6 +160,43 @@ public static class PatchScript
|
|||
return expanded;
|
||||
}
|
||||
|
||||
private void AddLayer(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var name = Required(fields, "name", line);
|
||||
if (_layers.Any(layer => layer.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new PatchScriptException(line, $"duplicate layer `{name}`");
|
||||
}
|
||||
|
||||
var layer = new PatchLayer(
|
||||
name,
|
||||
GetAny(fields, ["engine", "e"], ""),
|
||||
TryGetAny(fields, ["min_key", "key_min", "lo"], out var minKey) ? ParseInt(minKey, line) : null,
|
||||
TryGetAny(fields, ["max_key", "key_max", "hi"], out var maxKey) ? ParseInt(maxKey, line) : null,
|
||||
GetBoundFloat(fields, line, 1, $"/layers/{_layers.Count}/gain", "gain", "g"),
|
||||
GetAny(fields, ["send", "effect", "fx"], ""));
|
||||
if (layer is { MinKey: { } min, MaxKey: { } max } && min > max)
|
||||
{
|
||||
throw new PatchScriptException(line, "layer min_key must be less than or equal to max_key");
|
||||
}
|
||||
|
||||
_layers.Add(layer);
|
||||
_layerDefaults[name] = LayerVoiceFields(layer, Without(fields, "name", "engine", "e", "min_key", "key_min", "lo", "max_key", "key_max", "hi", "send", "effect", "fx"));
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> LayerVoiceFields(PatchLayer layer, IReadOnlyDictionary<string, string> fields)
|
||||
{
|
||||
var result = new Dictionary<string, string>(fields, StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["layer"] = layer.Name
|
||||
};
|
||||
if (!result.ContainsKey("gain"))
|
||||
{
|
||||
result["gain"] = F(layer.Gain);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ApplyPatch(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
if (TryGetAny(fields, ["gain", "g"], out var gain)) Gain = ParseBoundFloat(gain, line, Gain, "/patch/gain");
|
||||
|
|
@ -230,6 +282,9 @@ public static class PatchScript
|
|||
|
||||
return new Voice
|
||||
{
|
||||
Layer = TryGetAny(fields, ["layer"], out var layerName)
|
||||
? _layers.First(layer => layer.Name.Equals(layerName, StringComparison.OrdinalIgnoreCase))
|
||||
: null,
|
||||
Oscillator = new Oscillator(
|
||||
waveform,
|
||||
frequency,
|
||||
|
|
@ -845,6 +900,7 @@ public static class PatchScript
|
|||
"p" or "patch" or "instrument" => "patch",
|
||||
"d" or "default" or "defaults" => "defaults",
|
||||
"def" or "t" or "template" => "template",
|
||||
"layer" or "kit" => "layer",
|
||||
"v" or "voice" => "voice",
|
||||
"opgraph" or "ops" or "operators" => "opgraph",
|
||||
"operator" or "op" => "operator",
|
||||
|
|
@ -959,6 +1015,9 @@ public static class PatchScript
|
|||
? parsed
|
||||
: throw new PatchScriptException(line, $"bad number `{value}`");
|
||||
|
||||
private static string F(float value) =>
|
||||
value.ToString("0.######", CultureInfo.InvariantCulture);
|
||||
|
||||
private static float PunchGain(float value) => 1 + Math.Max(0, Math.Clamp(value, -1, 1));
|
||||
|
||||
private static int ParseInt(string value, int line) =>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public static class ReferenceRebuildCatalog
|
|||
BuiltInScripts.ZynStyleAdditiveLead,
|
||||
[
|
||||
new("engine_add", "1", "Represented as explicit additive sine partial voices."),
|
||||
new("named_layers", "body,shine", "Layer declarations group partial voices under reusable ADD engine intent."),
|
||||
new("partial_voice_count", "4", "The additive shape is represented as four explicit Aquarium partial voices."),
|
||||
new("envelope_count", "2", "Amplitude and pitch contour pressure maps to ADSR plus vibrato in the current patch surface."),
|
||||
new("lfo_count", "1", "The fixture's amplitude/frequency motion is represented as patch-level vibrato/macro motion.")
|
||||
|
|
@ -39,6 +40,7 @@ public static class ReferenceRebuildCatalog
|
|||
BuiltInScripts.ZynStylePadTexture,
|
||||
[
|
||||
new("engine_pad", "1", "Approximated as slow layered oscillators plus an air/noise layer."),
|
||||
new("named_layers", "pad_low,pad_high,air", "Layer declarations separate PAD body from air/noise texture."),
|
||||
new("filter_count", "1", "Represented with low-pass/high-pass field sites."),
|
||||
new("free_envelope_count", "1", "Approximated with slow ADSR timing and macro spread."),
|
||||
new("slow_modulation", "patch_lfo", "The rebuild adds slow patch modulation because PAD texture wants motion even though the minimal fixture only marks PAD/filter/free-envelope pressure.")
|
||||
|
|
@ -58,6 +60,7 @@ public static class ReferenceRebuildCatalog
|
|||
new("engine_add", "1", "The air/formant layer is represented as a formant-filtered oscillator voice."),
|
||||
new("engine_sub", "1", "The body layer is represented as a lower triangle voice."),
|
||||
new("layered_instrument", "yes", "Multiple Zyn kit items map to multiple Aquarium voices."),
|
||||
new("named_layers", "air,body,breath", "Layer declarations preserve source identity before voice expansion."),
|
||||
new("formant_filter_count", "1", "Represented with Aquarium formant filters and a vowel macro."),
|
||||
new("effect_count", "1", "Approximated with local drive/soft clipping rather than a Zyn effect slot.")
|
||||
],
|
||||
|
|
|
|||
|
|
@ -60,3 +60,4 @@
|
|||
{"kind":"zyn_reference_start","lesson":"The ZynAddSubFX phase should start with structural feature extraction, not translation. Upstream Zyn instruments live in a separate bank submodule and the preset provenance is not clear enough to vendor casually, so the first parser slice uses project-authored .xiz fixtures. `ZynInstrumentReader` reads plain or gzip-compressed XML and classifies active ADD/SUB/PAD engines, kit layering, envelopes, free envelopes, LFOs, filters, formants, and effects into ReferencePatch features.","source":"ZynAddSubFX feature extraction kickoff on 2026-05-14"}
|
||||
{"kind":"zyn_reference_rebuilds","lesson":"The first Zyn-style Aquarium rebuilds can sketch additive lead, PAD texture, and vocal/formant layer fixtures with existing voices, macros, formants, filters, noise, and modulation, but the missing abstractions are now concrete: additive harmonic-bank syntax, PAD/spectral source generation, free envelopes for normal voices, named kit/layer routing with per-layer effect sends, and richer formant/vowel morphing. Keep these as named pressure before adding new DSL surface.","source":"ZynAddSubFX rebuild catalog slice on 2026-05-14"}
|
||||
{"kind":"zyn_worst_case_survey","lesson":"Scanning the upstream ZynAddSubFX instrument bank at commit e9f64a9 found 1358 .xiz files and the hardened reader now parses all of them. The nastiest target is `olivers-100/0032-Drum Kit.xiz`: 16 enabled kit items, mixed ADD/SUB/PAD engines, 106 envelopes, 25 free envelopes, 73 LFOs, 37 filters, and 3 formant filters. The broader top list says named kit/layer routing should come before syntax golf for additive banks, PAD sources, or formant motion; otherwise new abstractions will have no coherent owner.","source":"ZynAddSubFX pressure survey on 2026-05-14"}
|
||||
{"kind":"layer_language_golf","lesson":"The first Zyn-driven language-golf abstraction is `layer`: named layer declarations own engine tags, optional MIDI key range metadata, default gain, and effect-send labels, while lowering to ordinary Aquarium voices for now. The abstraction generalizes across additive, PAD-texture, and vocal/formant rebuilds and gives future additive-bank, PAD-source, free-envelope, and formant-motion syntax a coherent owner. It deliberately does not implement those later features yet.","source":"Zyn named layer DSL slice on 2026-05-14"}
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@
|
|||
"next": "Use the now-validated algorithm-8 route scale, transpose lowering, and fixed-frequency lowering as fixed inputs while isolating remaining community-voice timbre mismatch, then expand to broader public-domain banks."
|
||||
},
|
||||
"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.",
|
||||
"lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The hardened reader now parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. 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, but the upstream survey says named kit/layer routing is the owner abstraction needed before syntax golf for additive harmonic banks, PAD/spectral source generation, free envelopes, or richer formant/vowel morphing.",
|
||||
"next": "Map named kit/layer routing first; do not add additive/PAD/free-envelope/formant syntax until layer authority is clear."
|
||||
"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 now provide the first Zyn-driven DSL abstraction.",
|
||||
"lesson": "Start Zyn as structural classification, not translation. Upstream Zyn presets live in a separate bank submodule and should not be vendored until license/provenance is explicit. The hardened reader now parses the observed upstream instrument bank at commit e9f64a9: 1358/1358 .xiz files. 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` now owns name, engine tag, optional key range metadata, default gain, and effect-send labels while lowering to ordinary voices. It gives future additive-bank, PAD-source, free-envelope, and formant-motion syntax a coherent owner.",
|
||||
"next": "Choose the next syntax that attaches to named layers: additive harmonic bank, PAD spectral source, free envelope, or richer formant morphing. Prove one across more than one target."
|
||||
},
|
||||
"note_and_envelope_model": {
|
||||
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "86 passed; Zyn worst-case survey added without regressing DX7 parity",
|
||||
"commit": "pending Zyn pressure survey"
|
||||
"result": "88 passed; named layer DSL added without regressing DX7 parity",
|
||||
"commit": "pending Zyn named layers"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@ Doctrine update:
|
|||
|
||||
Completed this slice:
|
||||
|
||||
- Added Zyn-driven language golf through `layer` declarations. A layer owns
|
||||
name, engine tag, optional MIDI key range metadata, default gain, and an
|
||||
effect-send label, then lowers to ordinary voices for now.
|
||||
- Converted the three Zyn-style rebuilds to use named layers:
|
||||
additive `body`/`shine`, PAD `pad_low`/`pad_high`/`air`, and vocal
|
||||
`air`/`body`/`breath`.
|
||||
- This is an ownership scaffold, not hidden PAD/free-envelope/effect machinery.
|
||||
Future additive-bank, PAD-source, free-envelope, and formant-motion syntax
|
||||
now has somewhere coherent to attach.
|
||||
- Verified with bundled Python/dexed-py still wired:
|
||||
`AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore`:
|
||||
88 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Hardened `ZynInstrumentReader` against the real upstream Zyn instrument bank:
|
||||
it now tolerates leading whitespace before XML declarations and counts only
|
||||
actual `FORMANT_FILTER`/`FILTER` blocks instead of all internal formant/vowel
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ live_invariants:
|
|||
- Reference fixtures must preserve provenance, license status, source hash, and feature pressure.
|
||||
- Public-domain reference fixtures and dexed-py helpers are test/development material only; the NuGet package must ship only AquariumSynth.Dsl.
|
||||
- ZynAddSubFX import starts as feature extraction, not translation. Use project-authored `.xiz` fixtures until external preset-bank license/provenance is explicit.
|
||||
- Named `layer` declarations own Zyn-style kit/layer identity: engine tag, optional key range metadata, default gain, and effect-send labels. They lower to ordinary voices for now; do not smuggle PAD/free-envelope/effect behavior into them.
|
||||
- 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.
|
||||
- Harsh calibration probes belong in tests and ignored artifacts, not in the patch library unless they are musically useful stock.
|
||||
|
|
@ -94,9 +95,9 @@ 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. First Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the next likely owner abstraction."
|
||||
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. First Aquarium rebuilds exist for additive lead, PAD texture, and vocal/formant layer and now use named `layer` declarations. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the next likely owner abstraction."
|
||||
first_slice:
|
||||
- "Map named kit/layer routing first; the upstream worst-case survey says it is the owner needed before additive harmonic bank, PAD spectral source, free envelope, or richer formant morphing syntax."
|
||||
- "Use named layers as the owner before promoting additive harmonic bank, PAD spectral source, free envelope, or richer formant morphing syntax."
|
||||
- "Do not add all of them at once."
|
||||
faust:
|
||||
priority: 3
|
||||
|
|
@ -127,11 +128,10 @@ next_agent_start_here:
|
|||
Continue the ZynAddSubFX phase. The first parser/classifier slice exists in
|
||||
`src/AquariumSynth.Dsl/ZynInstrument.cs` with project-authored fixtures
|
||||
under `tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored`.
|
||||
Next pressure is to map named kit/layer routing from the Zyn survey evidence
|
||||
in `docs/zynaddsubfx-pressure-survey.md`. The worst upstream targets combine
|
||||
many enabled kit items and engines, so layer authority should come before
|
||||
syntax golf for additive banks, PAD source generation, free envelopes, or
|
||||
formant morphing. Leave DX7 alone unless a regression test fails.
|
||||
Next pressure is to choose the next syntax that attaches to named layers:
|
||||
additive harmonic bank, PAD spectral source, free envelope, or richer
|
||||
formant morphing. Do one, prove it across more than one target, and leave
|
||||
DX7 alone unless a regression test fails.
|
||||
must_read:
|
||||
- AGENTS.md
|
||||
- docs/audio-doctrine.md
|
||||
|
|
@ -144,6 +144,16 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
Added the first Zyn-driven language-golf abstraction: `layer`.
|
||||
Layer declarations own name, engine tag, optional MIDI key range metadata,
|
||||
default gain, and effect-send labels while lowering to ordinary Aquarium
|
||||
voices. The three Zyn-style rebuilds now use named layers, proving the
|
||||
abstraction generalizes across additive, PAD-texture, and vocal/formant
|
||||
targets without adding fixture-specific shortcuts or hidden PAD/effect
|
||||
behavior.
|
||||
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore: 88 passed"
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
Hardened the Zyn reader against the real upstream instrument bank and
|
||||
|
|
|
|||
|
|
@ -255,8 +255,10 @@ public sealed class PatchScriptTests
|
|||
var export = FaustEmitter.Emit(patch, new FaustExportOptions(rebuild.Id.Replace('/', '_').Replace('-', '_')));
|
||||
|
||||
Assert.Contains("process =", export.Source);
|
||||
Assert.NotEmpty(patch.Layers);
|
||||
Assert.NotEmpty(rebuild.MatchedFeatures);
|
||||
Assert.NotEmpty(rebuild.MissingFeatures);
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "named_layers");
|
||||
Assert.All(rebuild.MissingFeatures, feature => Assert.False(string.IsNullOrWhiteSpace(feature.Notes)));
|
||||
}
|
||||
}
|
||||
|
|
@ -286,6 +288,41 @@ public sealed class PatchScriptTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayerSyntaxNamesReusableVoiceGroups()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
layer name=kick engine=sub min_key=36 max_key=36 gain=.4 wave=sine freq=55 attack=.001 sustain=.04 decay=.18
|
||||
layer name=air engine=add min_key=60 max_key=84 gain=.12 wave=saw lpf=.72
|
||||
voice layer=kick
|
||||
voice layer=air freq=440
|
||||
voice layer=air freq=660 gain=.08
|
||||
""");
|
||||
|
||||
Assert.Equal(2, patch.Layers.Count);
|
||||
Assert.Equal(3, patch.Voices.Count);
|
||||
Assert.Equal("kick", patch.Voices[0].Layer?.Name);
|
||||
Assert.Equal("sub", patch.Voices[0].Layer?.Engine);
|
||||
Assert.Equal(36, patch.Voices[0].Layer?.MinKey);
|
||||
Assert.Equal(36, patch.Voices[0].Layer?.MaxKey);
|
||||
Assert.Equal(.4f, patch.Voices[0].Gain, 5);
|
||||
Assert.Equal("air", patch.Voices[1].Layer?.Name);
|
||||
Assert.Equal("air", patch.Voices[2].Layer?.Name);
|
||||
Assert.Equal(.08f, patch.Voices[2].Gain, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayerSyntaxRejectsUnknownOrDuplicateLayers()
|
||||
{
|
||||
Assert.Throws<PatchScriptException>(() => PatchScript.Parse("""
|
||||
layer name=body
|
||||
layer name=body
|
||||
voice layer=body
|
||||
"""));
|
||||
|
||||
Assert.Throws<PatchScriptException>(() => PatchScript.Parse("voice layer=missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dx7Algorithm32RebuildMatchesAdditiveCarrierShape()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue