diff --git a/docs/reference-synth-roadmap.md b/docs/reference-synth-roadmap.md index 85491d6..093d074 100644 --- a/docs/reference-synth-roadmap.md +++ b/docs/reference-synth-roadmap.md @@ -120,10 +120,10 @@ First deliverable: effects. - Rebuild one additive lead, one PAD texture, and one formant/vocal patch. The first rebuild catalog entries now parse/export and record the first Zyn - missing abstractions. Named layers and additive harmonic-bank syntax now - exist; PAD-style spectral source generation, free envelopes for normal - voices, fuller kit/effect routing, and richer formant/vowel morphing remain - pressure. + missing abstractions. Named layers, additive harmonic-bank syntax, and staged + rate/level envelopes for normal voices now exist; PAD-style spectral source + generation, arbitrary Zyn free-envelope point curves, fuller kit/effect + routing, and richer formant/vowel morphing remain pressure. Do not vendor the upstream Zyn instrument-bank submodule until its preset license/provenance is explicit enough for this repo. Use project-authored @@ -148,6 +148,18 @@ and lowers it to ordinary voices. This gives additive lead, drawbar, and future partial-bank targets a reusable surface without pretending to implement Zyn ADDsynth phase, bandwidth, or free-envelope details. +The third response is staged voice envelopes. Normal voices and layer defaults +can now use the existing readable rate/level surface: + +```text +layer name=pad engine=pad env=rl rates=.5,.7,1.0,1.4 levels=1,.85,.7,0 gate=2.2 +voice layer=pad freq=220 +``` + +This promotes the envelope authority already used by operator graphs into the +voice path. It is enough to express layered PAD/vocal onset and release shapes, +but it is not yet a Zyn free-envelope point editor. + Sources: - [ZynAddSubFX](https://github.com/zynaddsubfx/zynaddsubfx) diff --git a/docs/zynaddsubfx-pressure-survey.md b/docs/zynaddsubfx-pressure-survey.md index 14e4c92..c469c5c 100644 --- a/docs/zynaddsubfx-pressure-survey.md +++ b/docs/zynaddsubfx-pressure-survey.md @@ -86,3 +86,15 @@ The harmonic bank records authored additive partial intent under a named layer, then lowers to ordinary sine voices. It does not claim Zyn ADDsynth's full oscillator table: phase, bandwidth, oscillator shaping, and exact free-envelope behavior remain separate pressure. + +Normal voices can also use staged rate/level envelopes through the same +`env=rl` surface already used by operator graphs: + +```text +layer name=pad engine=pad env=rl rates=.5,.7,1.0,1.4 levels=1,.85,.7,0 gate=2.2 +voice layer=pad freq=220 +``` + +That gives PAD and vocal layers asymmetric contour authority without returning +to anonymous ADSR piles. Arbitrary Zyn free-envelope point curves remain +pressure; the staged surface is the first coherent rung, not the whole ladder. diff --git a/src/AquariumSynth.Dsl/BuiltInScripts.cs b/src/AquariumSynth.Dsl/BuiltInScripts.cs index 97fcc55..29a301f 100644 --- a/src/AquariumSynth.Dsl/BuiltInScripts.cs +++ b/src/AquariumSynth.Dsl/BuiltInScripts.cs @@ -828,9 +828,9 @@ public static class BuiltInScripts lpf=0.18 gain=0.05 - 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 + layer name=pad_low engine=pad min_key=36 max_key=84 gain=0.07 vibrato=@/macro/spread vibrato_hz=0.09 env=rl rates=0.55,0.68,0.95,1.4 levels=1,0.86,0.78,0 curves=lin,exp,lin,lin gate=2.18 + layer name=pad_high engine=pad min_key=48 max_key=108 gain=0.044 vibrato=0.009 vibrato_hz=0.11 env=rl rates=0.42,0.58,0.9,1.25 levels=1,0.84,0.72,0 curves=lin,exp,lin,lin gate=1.9 + layer name=air engine=add min_key=0 max_key=127 gain=0.018 wave=noise noise=0.82 env=rl rates=0.8,0.6,1.1,1.1 levels=1,0.82,0.55,0 curves=lin,lin,exp,lin gate=2.5 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 @@ -866,9 +866,9 @@ 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 + layer name=air engine=add min_key=48 max_key=108 gain=0.11 send=formant env=rl rates=0.08,0.24,0.58,0.55 levels=1,0.82,0.7,0 curves=lin,exp,lin,lin gate=0.9 + layer name=body engine=sub min_key=24 max_key=72 gain=0.075 env=rl rates=0.12,0.28,0.55,0.6 levels=1,0.78,0.64,0 curves=lin,exp,lin,lin gate=0.95 + 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 env=rl rates=0.18,0.25,0.48,0.42 levels=1,0.65,0.38,0 curves=lin,lin,exp,lin gate=0.85 voice layer=air diff --git a/src/AquariumSynth.Dsl/FaustExport.cs b/src/AquariumSynth.Dsl/FaustExport.cs index 991b8a4..813313e 100644 --- a/src/AquariumSynth.Dsl/FaustExport.cs +++ b/src/AquariumSynth.Dsl/FaustExport.cs @@ -187,18 +187,20 @@ public static class FaustEmitter var dutyExpression = $"clip01({parameters.Expression(VoiceField(voiceIndex, "osc/duty"), voice.Oscillator.Duty)} + {parameters.Expression(VoiceField(voiceIndex, "duty/ramp"), voice.Duty.RampPerSecond)} * age + patch_mod_duty + {duty})"; var fmIndex = $"max(0.0, {parameters.Expression(VoiceField(voiceIndex, "fm/index"), voice.Fm.Index)} + patch_mod_fm_index + {fmIndexMod}) * {FmDecay(parameters.Expression(VoiceField(voiceIndex, "fm/decay"), voice.Fm.IndexDecaySeconds), voice.Fm.IndexDecaySeconds, parameters.IsBound(VoiceField(voiceIndex, "fm/decay")))}"; var oscillator = OscillatorExpression(patch, voice, voiceIndex, frequency, dutyExpression, fmIndex, parameters); - var envelope = EnvelopeExpression( - voice.Envelope, - noteGate, - UsesHostPlayback(patch.Playback) || voice.Note.Source == NoteSource.Host, - field => parameters.Expression(VoiceField(voiceIndex, field), field switch - { - "env/attack" => voice.Envelope.AttackSeconds, - "env/decay" => voice.Envelope.DecaySeconds, - "env/sustain_level" => voice.Envelope.SustainLevel, - "env/release" => voice.Envelope.ReleaseSeconds, - _ => throw new ArgumentOutOfRangeException(nameof(field), field, null) - })); + var envelope = voice.RateLevelEnvelope is not null + ? RateLevelEnvelopeExpression(voice.RateLevelEnvelope, noteGate) + : EnvelopeExpression( + voice.Envelope, + noteGate, + UsesHostPlayback(patch.Playback) || voice.Note.Source == NoteSource.Host, + field => parameters.Expression(VoiceField(voiceIndex, field), field switch + { + "env/attack" => voice.Envelope.AttackSeconds, + "env/decay" => voice.Envelope.DecaySeconds, + "env/sustain_level" => voice.Envelope.SustainLevel, + "env/release" => voice.Envelope.ReleaseSeconds, + _ => throw new ArgumentOutOfRangeException(nameof(field), field, null) + })); var tremoloDepth = parameters.Expression(VoiceField(voiceIndex, "color/tremolo"), Math.Clamp(voice.Color.TremoloDepth, 0, 1)); var tremoloHz = parameters.Expression(VoiceField(voiceIndex, "color/tremolo_hz"), voice.Color.TremoloHz); var hasTremolo = voice.Color.TremoloDepth > 0 && voice.Color.TremoloHz > 0 || diff --git a/src/AquariumSynth.Dsl/Model.cs b/src/AquariumSynth.Dsl/Model.cs index f0085d6..6af1237 100644 --- a/src/AquariumSynth.Dsl/Model.cs +++ b/src/AquariumSynth.Dsl/Model.cs @@ -218,6 +218,7 @@ public sealed record Voice public Oscillator Oscillator { get; init; } = new(); public Note Note { get; init; } = new(); public Envelope Envelope { get; init; } = new(); + public RateLevelEnvelope? RateLevelEnvelope { get; init; } public PitchMotion Pitch { get; init; } = new(); public DutyMotion Duty { get; init; } = new(); public Filter Filter { get; init; } = new(); diff --git a/src/AquariumSynth.Dsl/PatchScript.cs b/src/AquariumSynth.Dsl/PatchScript.cs index 96b3310..f01dd1f 100644 --- a/src/AquariumSynth.Dsl/PatchScript.cs +++ b/src/AquariumSynth.Dsl/PatchScript.cs @@ -297,8 +297,13 @@ public static class PatchScript { var waveform = TryGetAny(fields, ["wave", "w"], out var wave) ? ParseWaveform(wave, line) : Waveform.Sine; var frequency = GetBoundFloat(fields, line, 440, VoiceField(voiceIndex, "note/frequency"), "freq", "frequency", "f"); - var gateSeconds = GetBoundFloat(fields, line, 0.1f, VoiceField(voiceIndex, "note/gate"), "gate", "hold", "duration", "sustain", "s"); - var sustainLevel = GetBoundFloat(fields, line, 1, VoiceField(voiceIndex, "env/sustain_level"), "sustain_level", "sl"); + var envelopeSpec = TryGetAny(fields, ["env", "envelope"], out var envSpec) + ? ParseEnvelopeSpec(envSpec, fields, line, VoicePath(voiceIndex)) + : null; + var gateSeconds = envelopeSpec?.GateSeconds ?? + GetBoundFloat(fields, line, 0.1f, VoiceField(voiceIndex, "note/gate"), "gate", "hold", "duration", "sustain", "s"); + var sustainLevel = envelopeSpec?.Envelope.SustainLevel ?? + GetBoundFloat(fields, line, 1, VoiceField(voiceIndex, "env/sustain_level"), "sustain_level", "sl"); var gainScale = 1f; if (TryGetAny(fields, ["punch", "pu"], out var punch)) { @@ -345,11 +350,12 @@ public static class PatchScript GetBoundFloat(fields, line, 0.5f, VoiceField(voiceIndex, "osc/duty"), "duty", "du"), GetBoundFloat(fields, line, 0, VoiceField(voiceIndex, "osc/phase"), "phase", "pa")), Note = new Note(frequency, gateSeconds, noteSource), - Envelope = new Envelope( + Envelope = envelopeSpec?.Envelope ?? new Envelope( GetBoundFloat(fields, line, 0, VoiceField(voiceIndex, "env/attack"), "attack", "a"), GetBoundFloat(fields, line, 0, VoiceField(voiceIndex, "env/decay"), "env_decay", "ed"), sustainLevel, GetBoundFloat(fields, line, 0.1f, VoiceField(voiceIndex, "env/release"), "release", "rel", "decay", "d")), + RateLevelEnvelope = envelopeSpec?.RateLevelEnvelope, Pitch = new PitchMotion( GetBoundFloat(fields, line, 20, VoiceField(voiceIndex, "pitch/min_freq"), "min_freq", "min"), GetBoundFloat(fields, line, 0, VoiceField(voiceIndex, "pitch/ramp"), "pitch_ramp", "pr"), @@ -654,6 +660,8 @@ public static class PatchScript return parameter.Default; } + private static string VoicePath(int voiceIndex) => $"/voices/{voiceIndex}"; + private static string VoiceField(int voiceIndex, string field) => $"/voices/{voiceIndex}/{field}"; private static List ParseOperatorNodes(string value, int line) => diff --git a/src/AquariumSynth.Dsl/ReferenceRebuilds.cs b/src/AquariumSynth.Dsl/ReferenceRebuilds.cs index 424df30..42c9f62 100644 --- a/src/AquariumSynth.Dsl/ReferenceRebuilds.cs +++ b/src/AquariumSynth.Dsl/ReferenceRebuilds.cs @@ -43,12 +43,13 @@ public static class ReferenceRebuildCatalog 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("free_envelope_count", "1", "Approximated with staged voice rate/level envelopes and macro spread."), + new("voice_rate_level_envelope", "3", "PAD body and air layers use staged rate/level envelopes attached through layer defaults."), 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.") ], [ new("pad_spectral_source", "PADsynth wavetable/spectral generation", "Aquarium has no PAD-style spectral source; this rebuild is a texture approximation."), - new("free_envelope_shape", "arbitrary Zyn envelope points", "Aquarium has ADSR and staged operator envelopes, but not a general free envelope for normal voices.") + new("free_envelope_shape", "arbitrary Zyn envelope points", "Aquarium has staged voice envelopes now, but not arbitrary point/free-mode Zyn envelope curves.") ], "This is pressure, not a claim of PAD parity. The current voice DSL can sketch the texture, but PAD-style spectral generation deserves its own source authority if repeated targets need it."), new( @@ -62,6 +63,7 @@ public static class ReferenceRebuildCatalog 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("voice_rate_level_envelope", "3", "Each vocal layer uses a staged rate/level voice envelope for asymmetric onset and release shape."), 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.") ], diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 42d4870..f24b6e5 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -62,3 +62,4 @@ {"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"} {"kind":"harmonic_bank_language_golf","lesson":"Additive `harmonics` syntax now records authored ratio/gain partial banks under named layers and lowers them to ordinary Aquarium voices. This generalizes additive lead and future drawbar/partial-bank targets without hiding Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics behind a fake parity claim.","source":"Zyn harmonic-bank DSL slice on 2026-05-14"} +{"kind":"voice_rate_level_envelopes","lesson":"The existing readable `env=rl rates=... levels=... curves=... gate=...` contour now applies to normal voices and therefore to named layer defaults, not only operator graphs. This gives PAD and vocal Zyn rebuilds staged non-ADSR onset/release shape while preserving the invariant that Note owns gate timing and envelope data owns level shape. Arbitrary Zyn free-envelope point curves remain missing pressure.","source":"Zyn staged voice envelope DSL slice on 2026-05-14"} diff --git a/state/memory.json b/state/memory.json index 7179af8..dbd5738 100644 --- a/state/memory.json +++ b/state/memory.json @@ -21,6 +21,7 @@ "structured development .aqua patch library", "ZynAddSubFX .xiz feature extraction with project-authored fixtures", "Zyn additive harmonic-bank DSL surface", + "staged rate/level envelopes for normal voices", "thresholded DX7 rendered parity test", "reference rebuild catalog", "DX7 SysEx structural parsing", @@ -82,9 +83,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. Named `layer` declarations provide the first Zyn-driven DSL abstraction, and additive `harmonics` banks now attach to those layers.", - "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` 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 and lowers them to voices for inspectable Faust output. It does not claim Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics.", - "next": "Choose the next syntax that attaches to named layers: PAD spectral source, free envelope, or richer formant morphing. Prove one across more than one target." + "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` banks attach to those layers, and staged `env=rl` envelopes now work on normal voices/layer defaults.", + "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` 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 and lowers them to voices for inspectable Faust output. 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, PAD spectral sources, arbitrary free-envelope point curves, or richer formant morphing.", + "next": "Choose the next syntax that attaches to named layers: PAD spectral source 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.", @@ -99,7 +100,7 @@ }, "last_verified": { "command": "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore", - "result": "90 passed; additive harmonic-bank DSL added without regressing DX7 parity", - "commit": "pending Zyn harmonic banks" + "result": "91 passed; staged voice envelope DSL added without regressing DX7 parity", + "commit": "pending Zyn staged voice envelopes" } } diff --git a/state/scratch.md b/state/scratch.md index 293946d..c17e9b7 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -15,6 +15,24 @@ Doctrine update: Completed this slice: +- Promoted staged rate/level envelopes to normal voices and layer defaults. + Voice fields now accept `env=rl rates=... levels=... curves=... gate=...`; + the model stores `Voice.RateLevelEnvelope`, and Faust emission uses + `rl4_env` for those voices. +- Converted Zyn PAD texture and vocal-layer rebuilds to use staged envelopes + through named layers. This gives two non-additive targets asymmetric + envelope contour authority without inventing a new parser. +- Arbitrary Zyn free-envelope point curves remain missing pressure. This slice + is the coherent staged-envelope rung, not a free-mode clone. +- Focused verification passed: + `dotnet test tests\AquariumSynth.Dsl.Tests\AquariumSynth.Dsl.Tests.csproj --no-restore --filter "FullyQualifiedName~LayeredVoiceRateLevelEnvelope|FullyQualifiedName~ZynStyleReferenceRebuildsParseExportAndDeclarePressure|FullyQualifiedName~BuiltInReferenceScriptsParseAndExportFaust"`: + 3 passed. +- Full verification with bundled Python/dexed-py: + `AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore`: + 91 passed. + +Previous slice: + - Added additive harmonic-bank syntax attached to named layers: `harmonics layer=body root=220 partials=1:.16,2:.075`. - The parser preserves each bank as `HarmonicBank`/`HarmonicPartial` model diff --git a/state/spine.yaml b/state/spine.yaml index 34f15e3..0628673 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -1,6 +1,6 @@ project: AquariumSynthCSharp spine_version: 1 -updated: 2026-05-14T20:15:00+01:00 +updated: 2026-05-14T20:45:00+01:00 prime_directive: > Build coherent synth machinery. References exist to reveal missing structure, @@ -75,6 +75,7 @@ live_invariants: - 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. - Additive `harmonics` declarations own authored ratio/gain partial banks under named layers. They lower to ordinary voices for now; do not pretend this covers Zyn ADDsynth phase, bandwidth, oscillator shaping, or free-envelope semantics. + - Normal voices may use staged `env=rl` rate/level envelopes. Notes still own gate timing; envelopes own level shape. This is not arbitrary Zyn free-envelope point editing. - 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. @@ -96,10 +97,10 @@ 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 and now use named `layer` declarations. Additive rebuilds can now use `harmonics` banks attached to layers. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first 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. Additive rebuilds can use `harmonics` banks attached to layers, and PAD/vocal rebuilds now use staged voice `env=rl` envelopes through layer defaults. A non-vendored upstream bank survey parsed 1358/1358 .xiz files and identified named kit/layer routing as the first owner abstraction." first_slice: - - "Use named layers as the owner before promoting PAD spectral source, free envelope, or richer formant morphing syntax." - - "Additive harmonic banks now exist; prove any next syntax across more than one target." + - "Use named layers as the owner before promoting PAD spectral source or richer formant morphing syntax." + - "Additive harmonic banks and staged voice envelopes now exist; prove any next syntax across more than one target." - "Do not add all remaining pressures at once." faust: priority: 3 @@ -130,11 +131,12 @@ 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`. - Named `layer` declarations own source identity, and additive `harmonics` - banks now attach to those layers. Next pressure is to choose the next syntax - that attaches to named layers: 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. + Named `layer` declarations own source identity, additive `harmonics` banks + attach to those layers, and staged `env=rl` envelopes now work on normal + voices/layer defaults. Next pressure is to choose the next syntax that + attaches to named layers: PAD spectral source 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 @@ -147,6 +149,17 @@ next_agent_start_here: - "dotnet test AquariumSynthCSharp.slnx --no-restore" completed_slices: + - date: 2026-05-14 + summary: > + Promoted staged rate/level envelopes from operator graphs to normal + voices and layer defaults. Voice parsing now accepts `env=rl + rates=... levels=... curves=... gate=...`, stores the staged contour on + `Voice.RateLevelEnvelope`, and Faust emission uses `rl4_env` for those + voices. The Zyn PAD texture and vocal-layer rebuilds now use staged + envelopes through named layers, proving the surface beyond a single + fixture while leaving arbitrary Zyn free-envelope point curves as missing + pressure. + verification: "AQUARIUM_DX7_PYTHON= dotnet test AquariumSynthCSharp.slnx --no-restore: 91 passed" - date: 2026-05-14 summary: > Added additive harmonic-bank syntax for named layers. `harmonics diff --git a/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs b/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs index aa7363c..4cfaef7 100644 --- a/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs @@ -259,6 +259,10 @@ public sealed class PatchScriptTests Assert.NotEmpty(rebuild.MatchedFeatures); Assert.NotEmpty(rebuild.MissingFeatures); Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "named_layers"); + if (rebuild.ReferenceId is "zyn/project/pad-texture" or "zyn/project/vocal-layer") + { + Assert.Contains(patch.Voices, voice => voice.RateLevelEnvelope is not null); + } Assert.All(rebuild.MissingFeatures, feature => Assert.False(string.IsNullOrWhiteSpace(feature.Notes))); } } @@ -357,6 +361,26 @@ public sealed class PatchScriptTests """)); } + [Fact] + public void LayeredVoiceRateLevelEnvelopeParsesAndExports() + { + var patch = PatchScript.Parse(""" + param path=/macro/gate default=.9 min=.1 max=3 step=.01 + layer name=pad engine=pad gain=.08 env=rl rates=.1,.2,.3,.4 levels=1,.8,.5,0 curves=lin,exp,exp,lin gate=.9 + voice layer=pad freq=220 gate=@/macro/gate + """); + var export = FaustEmitter.Emit(patch); + + var voice = Assert.Single(patch.Voices); + Assert.NotNull(voice.RateLevelEnvelope); + Assert.Equal(.9f, voice.Note.GateSeconds, 5); + Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/voices/0/note/gate"); + Assert.Equal(.1f, voice.Envelope.AttackSeconds, 5); + Assert.Equal(.5f, voice.Envelope.SustainLevel, 5); + Assert.Equal(RateLevelCurve.Exponential, voice.RateLevelEnvelope.Curve2); + Assert.Contains("rl4_env(0.1, 1, 0, 0.2, 0.8, 1, 0.3, 0.5, 1, 0.4, 0, 0, patch_param_0)", export.Source); + } + [Fact] public void Dx7Algorithm32RebuildMatchesAdditiveCarrierShape() {