Start ZynAddSubFX feature extraction
This commit is contained in:
parent
bfa70f9754
commit
9715923c7b
13 changed files with 442 additions and 21 deletions
|
|
@ -112,9 +112,19 @@ Pressure on Aquarium DSL:
|
|||
First deliverable:
|
||||
|
||||
- Parse a small set of Zyn instrument XML files into a neutral reference model.
|
||||
The first parser/classifier slice exists for project-authored `.xiz`
|
||||
fixtures and supports plain XML plus gzip-compressed XML.
|
||||
- Classify features used by each instrument before trying to synthesize them.
|
||||
The current classifier identifies active ADD/SUB/PAD engines, enabled kit
|
||||
items, layering, envelopes, free envelopes, LFOs, filters, formants, and
|
||||
effects.
|
||||
- Rebuild one additive lead, one PAD texture, and one formant/vocal patch.
|
||||
|
||||
Do not vendor the upstream Zyn instrument-bank submodule until its preset
|
||||
license/provenance is explicit enough for this repo. Use project-authored
|
||||
fixtures for parser shape and only promote external fixtures with clean source
|
||||
notes.
|
||||
|
||||
Sources:
|
||||
|
||||
- [ZynAddSubFX](https://github.com/zynaddsubfx/zynaddsubfx)
|
||||
|
|
|
|||
169
src/AquariumSynth.Dsl/ZynInstrument.cs
Normal file
169
src/AquariumSynth.Dsl/ZynInstrument.cs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AquariumSynth.Dsl;
|
||||
|
||||
public sealed record ZynInstrument(
|
||||
string Name,
|
||||
string Author,
|
||||
string Comments,
|
||||
IReadOnlyList<ZynKitItem> KitItems)
|
||||
{
|
||||
public IReadOnlyList<ReferenceFeature> Features()
|
||||
{
|
||||
var enabled = KitItems.Where(item => item.Enabled).ToList();
|
||||
var engineCounts = enabled
|
||||
.SelectMany(item => item.Engines)
|
||||
.GroupBy(engine => engine)
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
var layered = enabled.Count > 1 || enabled.Any(item => item.Engines.Count > 1);
|
||||
|
||||
var features = new List<ReferenceFeature>
|
||||
{
|
||||
new("kit_item_count", KitItems.Count.ToString(), "Declared Zyn instrument kit items."),
|
||||
new("enabled_kit_items", enabled.Count.ToString(), "Enabled Zyn instrument kit items."),
|
||||
new("layered_instrument", layered ? "yes" : "no", "True when multiple kit items or multiple engines are active."),
|
||||
};
|
||||
|
||||
foreach (var engine in new[] { ZynEngine.AddSynth, ZynEngine.SubSynth, ZynEngine.PadSynth })
|
||||
{
|
||||
features.Add(new($"engine_{EngineName(engine)}", engineCounts.GetValueOrDefault(engine).ToString()));
|
||||
}
|
||||
|
||||
features.Add(new("envelope_count", enabled.Sum(item => item.EnvelopeCount).ToString()));
|
||||
features.Add(new("free_envelope_count", enabled.Sum(item => item.FreeEnvelopeCount).ToString()));
|
||||
features.Add(new("lfo_count", enabled.Sum(item => item.LfoCount).ToString()));
|
||||
features.Add(new("filter_count", enabled.Sum(item => item.FilterCount).ToString()));
|
||||
features.Add(new("formant_filter_count", enabled.Sum(item => item.FormantFilterCount).ToString()));
|
||||
features.Add(new("effect_count", enabled.Sum(item => item.EffectCount).ToString()));
|
||||
return features;
|
||||
}
|
||||
|
||||
public ReferencePatch ToReferencePatch(string id, ReferenceSource source) =>
|
||||
new(id, "zynaddsubfx", Name, source, Features(), Parameters: []);
|
||||
|
||||
private static string EngineName(ZynEngine engine) => engine switch
|
||||
{
|
||||
ZynEngine.AddSynth => "add",
|
||||
ZynEngine.SubSynth => "sub",
|
||||
ZynEngine.PadSynth => "pad",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null)
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record ZynKitItem(
|
||||
int Id,
|
||||
string Name,
|
||||
bool Enabled,
|
||||
IReadOnlyList<ZynEngine> Engines,
|
||||
int EnvelopeCount,
|
||||
int FreeEnvelopeCount,
|
||||
int LfoCount,
|
||||
int FilterCount,
|
||||
int FormantFilterCount,
|
||||
int EffectCount);
|
||||
|
||||
public enum ZynEngine
|
||||
{
|
||||
AddSynth,
|
||||
SubSynth,
|
||||
PadSynth
|
||||
}
|
||||
|
||||
public static class ZynInstrumentReader
|
||||
{
|
||||
public static ZynInstrument ParseFile(string path) =>
|
||||
Parse(File.ReadAllBytes(path));
|
||||
|
||||
public static ZynInstrument Parse(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
using var xml = OpenXmlStream(bytes);
|
||||
var document = XDocument.Load(xml, LoadOptions.None);
|
||||
var instrument = document.Descendants("INSTRUMENT").FirstOrDefault()
|
||||
?? throw new ArgumentException("Zyn instrument XML is missing INSTRUMENT");
|
||||
var info = instrument.Element("INFO");
|
||||
var kitItems = instrument
|
||||
.Descendants("INSTRUMENT_KIT_ITEM")
|
||||
.Select(ParseKitItem)
|
||||
.ToList();
|
||||
|
||||
return new ZynInstrument(
|
||||
StringValue(info, "name", "unnamed"),
|
||||
StringValue(info, "author", ""),
|
||||
StringValue(info, "comments", ""),
|
||||
kitItems);
|
||||
}
|
||||
|
||||
public static ReferenceSource SourceForBytes(string uri, string license, ReadOnlySpan<byte> bytes, string notes = "") =>
|
||||
new("zyn-xiz", uri, license, Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), notes);
|
||||
|
||||
private static ZynKitItem ParseKitItem(XElement item)
|
||||
{
|
||||
var engines = new List<ZynEngine>();
|
||||
if (EngineEnabled(item, "add_enabled", "ADD_SYNTH_PARAMETERS")) engines.Add(ZynEngine.AddSynth);
|
||||
if (EngineEnabled(item, "sub_enabled", "SUB_SYNTH_PARAMETERS")) engines.Add(ZynEngine.SubSynth);
|
||||
if (EngineEnabled(item, "pad_enabled", "PAD_SYNTH_PARAMETERS")) engines.Add(ZynEngine.PadSynth);
|
||||
|
||||
return new ZynKitItem(
|
||||
Id: IntAttribute(item, "id"),
|
||||
Name: StringValue(item, "name", ""),
|
||||
Enabled: BoolParam(item, "enabled"),
|
||||
Engines: engines,
|
||||
EnvelopeCount: CountElements(item, name => name.EndsWith("_ENVELOPE", StringComparison.OrdinalIgnoreCase)),
|
||||
FreeEnvelopeCount: item.Descendants()
|
||||
.Count(element => element.Name.LocalName.EndsWith("_ENVELOPE", StringComparison.OrdinalIgnoreCase) &&
|
||||
BoolParam(element, "free_mode")),
|
||||
LfoCount: CountElements(item, name => name.EndsWith("_LFO", StringComparison.OrdinalIgnoreCase)),
|
||||
FilterCount: CountElements(item, name => name.Contains("FILTER", StringComparison.OrdinalIgnoreCase)),
|
||||
FormantFilterCount: CountElements(item, name => name.Contains("FORMANT", StringComparison.OrdinalIgnoreCase)),
|
||||
EffectCount: CountElements(item, name => name.Contains("EFFECT", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private static MemoryStream OpenXmlStream(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if (bytes.Length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b)
|
||||
{
|
||||
using var compressed = new MemoryStream(bytes.ToArray());
|
||||
using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
|
||||
var decompressed = new MemoryStream();
|
||||
gzip.CopyTo(decompressed);
|
||||
decompressed.Position = 0;
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
return new MemoryStream(bytes.ToArray());
|
||||
}
|
||||
|
||||
private static int CountElements(XElement root, Func<string, bool> predicate) =>
|
||||
root.Descendants().Count(element => predicate(element.Name.LocalName));
|
||||
|
||||
private static bool EngineEnabled(XElement item, string flagName, string sectionName) =>
|
||||
BoolParamValue(item, flagName) ?? item.Element(sectionName) is not null;
|
||||
|
||||
private static string StringValue(XElement? root, string name, string fallback) =>
|
||||
root?.Elements("string")
|
||||
.FirstOrDefault(element => AttributeValue(element, "name") == name)
|
||||
?.Value ?? fallback;
|
||||
|
||||
private static bool BoolParam(XElement root, string name) =>
|
||||
BoolParamValue(root, name) ?? false;
|
||||
|
||||
private static bool? BoolParamValue(XElement root, string name)
|
||||
{
|
||||
var value = root.Elements("par_bool")
|
||||
.FirstOrDefault(element => AttributeValue(element, "name") == name)
|
||||
?.Attribute("value")
|
||||
?.Value;
|
||||
if (value is null) return null;
|
||||
return value.Equals("yes", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
|
||||
value == "1";
|
||||
}
|
||||
|
||||
private static int IntAttribute(XElement element, string name) =>
|
||||
int.TryParse(AttributeValue(element, name), out var value) ? value : 0;
|
||||
|
||||
private static string? AttributeValue(XElement element, string name) =>
|
||||
element.Attribute(name)?.Value;
|
||||
}
|
||||
|
|
@ -57,3 +57,4 @@
|
|||
{"kind":"dx7_modulator_peak_headroom","lesson":"Listening found ANLGSYN 1 and RES SYNTH1 blowing out into a drone at the modulator-envelope peak while Piano Bass was spot on. The surviving DX7 probe change caps only algorithm-2 non-carrier applied-envelope Level1 at 0.92, preserving Piano Bass and improving RES SYNTH1 while keeping ANLGSYN judged by log-mel and sustained high-band evidence rather than aggregate score. A wider summed-route headroom experiment for Mooger fan-in was tested and cut because it worsened Mooger metrics and did not isolate the overtone-emphasis issue.","source":"community DX7 listening correction on 2026-05-14"}
|
||||
{"kind":"dx7_mellowsolo_tail_window","lesson":"MELLOWSOLO pressure artifacts need a longer render window than 1.0s because the Aquarium candidate emits a 0.6s release from a 0.65s gate, so the old WAV cut the tail before it could finish. Extending only the MELLOWSOLO pressure render to 1.25s improves log-mel from about 0.429 to 0.347 and makes the tail audible without changing the synth lowering or passing parity targets. A global traced-release-duration experiment was tested and cut because it shortened releases too aggressively and broke PRC, Piano Bass, RES SYNTH1, and ANLGSYN 1. RES SYNTH1 has oscillator sync enabled, so its phase-like attack issue is more likely relative operator attack/peak shape than free-running oscillator phase.","source":"community DX7 listening correction on 2026-05-14"}
|
||||
{"kind":"dx7_graph_pitch_lfo","lesson":"ANLGSYN 1's missing beat is DX7 pitch LFO, not amplitude LFO: raw analog1.syx has AMD=71 but all operator AMS values are 0, while PMD=32/PMS=1/sine speed 38/delay 33 are active. Aquarium operator graphs now own graph-level vibrato (`vibrato`, `vibrato_hz`, `vibrato_delay`) and the DX7 probe lowers that LFO to about depth 0.003694 at 5.993 Hz with a 0.233s fade-in. A hard delay gate was tested and cut because it pushed log-mel over the ANLGSYN gate; fade-in keeps ANLGSYN passing at log-mel about 0.19497 and score about 0.6979.","source":"ANLGSYN 1 pitch-LFO beat correction on 2026-05-14"}
|
||||
{"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"}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ surfaces:
|
|||
tests: tests/AquariumSynth.Dsl.Tests
|
||||
reference_roadmap: docs/reference-synth-roadmap.md
|
||||
dx7_public_domain_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain
|
||||
zyn_project_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored
|
||||
package: AquariumSynth.Dsl 0.1.0-aquarium.1
|
||||
modules:
|
||||
model: src/AquariumSynth.Dsl/Model.cs
|
||||
|
|
@ -23,6 +24,7 @@ modules:
|
|||
faust: src/AquariumSynth.Dsl/FaustExport.cs
|
||||
faust_rendering: src/AquariumSynth.Dsl/FaustExport.cs
|
||||
dx7_reference_render_tests: tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
|
||||
zyn_reference_reader: src/AquariumSynth.Dsl/ZynInstrument.cs
|
||||
package_boundary_tests: tests/AquariumSynth.Dsl.Tests/PackageBoundaryTests.cs
|
||||
sfxr: src/AquariumSynth.Dsl/SfxrParams.cs
|
||||
analysis: src/AquariumSynth.Dsl/AudioAnalysis.cs
|
||||
|
|
@ -44,6 +46,7 @@ boundaries:
|
|||
- Faust source generation, target-code generation, and candidate rendering
|
||||
dev_only:
|
||||
- public-domain DX7 SysEx fixtures
|
||||
- project-authored ZynAddSubFX .xiz fixtures
|
||||
- dexed-py reference renderer
|
||||
- structured .aqua patch library
|
||||
checks:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"test-only project-authored Dexed patch renderer",
|
||||
"ignored listening WAV parity artifacts",
|
||||
"structured development .aqua patch library",
|
||||
"ZynAddSubFX .xiz feature extraction with project-authored fixtures",
|
||||
"thresholded DX7 rendered parity test",
|
||||
"reference rebuild catalog",
|
||||
"DX7 SysEx structural parsing",
|
||||
|
|
@ -79,6 +80,11 @@
|
|||
"lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, feedback, and graph-level pitch motion belong to a graph authority, not Voice fields. The first rendered parity rung is behavioral and sine-like, not exact DX7 operator execution. A harder algorithm-8 target exposed that feedback must be delayed recursion in Faust, not a direct cyclic self-reference. Static DX7 output-level approximation helped import/rebuild math but was mathematically wrong for carrier amplitude. A single-carrier Dexed sweep shows carrier amplitude follows 2^((outputLevel - 99) / 8). Isolated two-op DX7 FM maps full-level modulation to Aquarium route index 6.275; summed/cascaded algorithm-8 branches now use 6.0 after PRC listening, a project-authored combined stack probe, and split cascade/summed-pair probes showed lower values were under-bright. Feedback lowers through a fitted table instead of PRC-local constants; ANLGSYN 1 showed max feedback value 7 needs a hotter Aquarium amount 2.2 to recover buzzy high bands, while value 5 remains 0.19 for PRC. ANLGSYN 1 then exposed a sustained-buzz gap: output soft clip helped but did not stop mid-note drift; max-feedback source operators need an applied-envelope sustain floor so the feedback loop stays driven after the attack. ANLGSYN 1's remaining beat is pitch LFO: AMD is nonzero but all operator AMS values are 0, while PMD/PMS lower cleanly to operator-graph vibrato with faded delay. Ratio-mode detune belongs in operator frequency lowering. DX7 voice transpose belongs in graph base frequency: transpose 24 is neutral, transpose 12 lowers MIDI note 60 to 130.8128 Hz. DX7 fixed-frequency operators are absolute Hz, not note ratios; ANLGSYN 1 proved fixed op1/op3 must lower to ratios around 0.007615/0.009227 against a 130.8128 Hz graph base to preserve low buzzing modulation. ROM COM carrier compensation was cut: user listening found no-COM renders harmonically closer and mainly too quiet, so `OperatorOutputCompensation` returns unity and graph gain owns loudness recovery. Operator key-scaling and rate-scaling must lower against the effective played MIDI note after transpose; hardcoded MIDI 60 made RES SYNTH1 too soft on attack and Piano Bass less harmonically accurate. Project-authored Dexed patch rendering now gives lawful calibration targets. Reusing the operator output curve for EG levels was tested and cut because it failed hard-target log-mel; later output-level-dependent and naive traced-release applied-envelope experiments were also cut because they worsened broader parity. Direct envelope tracing shows current linear `env=rl` cannot express DX7 EG spike/drop behavior. Dexed-style audio samples EG once per 64-sample block and interpolates applied gain, so raw EG parity alone is not audio parity. Staged operator envelopes now support linear/exponential segments and supranormal levels. `ApproximateAppliedRateLevelEnvelope` uses that surface and now picks the first near-peak before gate so slower rising modulators hit early instead of swelling late; it can also preserve a floor for max-feedback source sustain. Community gains with no COM and played-note scaling: Mooger 0.75, Piano Bass 0.72, RES SYNTH1 0.75. Algorithm-2 non-carrier applied envelopes currently cap the first peak at 0.92 to prevent ANLGSYN 1/RES SYNTH1 peak blowout while preserving Piano Bass. MELLOWSOLO is a pressure artifact test, not passing parity; its render window is 1.25s so the candidate release is audible instead of cut at the artifact boundary. Mooger needs a more aggressive attack contour, and RES SYNTH1's phase-like attack issue is likely relative operator attack/peak shape because oscillator sync is enabled. PRC remains passing on log-mel/envelope/RMS, with score floor lowered to 0.74 after rate scaling changed the aggregate score.",
|
||||
"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.",
|
||||
"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 current classifier identifies active ADD/SUB/PAD engines, enabled kit items, layering, envelopes, free envelopes, LFOs, filters, formants, and effects so rebuild targets can pressure Aquarium's DSL without pretending parser success is expressivity.",
|
||||
"next": "Rebuild one additive lead, one PAD texture, and one vocal/formant layer from the project-authored fixtures in readable Aquarium DSL, then name the missing abstractions."
|
||||
},
|
||||
"note_and_envelope_model": {
|
||||
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
|
||||
"sfxr_mapping": "SFXR sustain is note gate duration. SFXR punch maps to lower ADSR sustain level plus voice gain compensation so the implicit attack peak stays at 1 and the held level sits below it.",
|
||||
|
|
@ -92,7 +98,7 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore",
|
||||
"result": "76 passed; MELLOWSOLO pressure artifact render extended to capture release tail; global traced-release experiment cut",
|
||||
"commit": "pending DX7 mellowsolo tail artifact"
|
||||
"result": "81 passed; Zyn .xiz parser/classifier added without regressing DX7 parity",
|
||||
"commit": "pending Zyn feature extraction"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ Doctrine update:
|
|||
|
||||
Completed this slice:
|
||||
|
||||
- Started the ZynAddSubFX phase and stopped treating DX7 as the active trench.
|
||||
- Added `ZynInstrumentReader` for `.xiz`-shaped XML. It reads plain XML and
|
||||
gzip-compressed XML, extracts instrument metadata and kit items, and emits
|
||||
neutral `ReferencePatch` features.
|
||||
- Added project-authored Zyn fixtures under
|
||||
`tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored` instead
|
||||
of vendoring upstream preset-bank files with unclear root provenance.
|
||||
- Current Zyn classifier detects active ADD/SUB/PAD engines, enabled kit items,
|
||||
layering, envelopes, free envelopes, LFOs, filters, formant filters, and
|
||||
effects. This is inventory for choosing rebuild targets, not translation yet.
|
||||
- Verified with bundled Python/dexed-py still wired:
|
||||
`AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore`:
|
||||
81 passed.
|
||||
|
||||
Previous slice:
|
||||
|
||||
- Addressed the remaining `ANLGSYN 1` beat as DX7 pitch-LFO pressure, not
|
||||
amplitude-LFO pressure. Raw `analog1.syx` bytes show `AMD=71` but every
|
||||
operator has `AMS=0`; the active path is `PMD=32`, `PMS=1`, sine LFO speed
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
project: AquariumSynthCSharp
|
||||
spine_version: 1
|
||||
updated: 2026-05-14T14:51:00+01:00
|
||||
updated: 2026-05-14T19:45:00+01:00
|
||||
|
||||
prime_directive: >
|
||||
Build coherent synth machinery. References exist to reveal missing structure,
|
||||
|
|
@ -23,6 +23,8 @@ current_mechanism:
|
|||
dx7_references: src/AquariumSynth.Dsl/Dx7SysEx.cs
|
||||
dx7_reference_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain
|
||||
dx7_reference_renderer_tests: tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs
|
||||
zyn_reference_reader: src/AquariumSynth.Dsl/ZynInstrument.cs
|
||||
zyn_project_fixtures: tests/AquariumSynth.Dsl.Tests/Fixtures/ZynAddSubFX/ProjectAuthored
|
||||
reference_rebuilds: src/AquariumSynth.Dsl/ReferenceRebuilds.cs
|
||||
tests: tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs
|
||||
doctrine: docs/audio-doctrine.md
|
||||
|
|
@ -69,6 +71,7 @@ live_invariants:
|
|||
- Recompile only for graph shape changes; vary declared parameters through the hosted DSP control API.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -90,9 +93,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_slice:
|
||||
- "Parse selected instrument XML files into neutral reference records."
|
||||
- "Classify additive, PAD, formant, and effect usage before translation."
|
||||
- "Choose the first three Aquarium rebuild targets from the project-authored additive lead, PAD texture, and vocal/formant layer fixtures."
|
||||
- "Design the smallest readable Aquarium DSL patches that express those categories, then decide which missing abstractions are real."
|
||||
faust:
|
||||
priority: 3
|
||||
purpose: "General DSP graph topology pressure test."
|
||||
|
|
@ -119,22 +123,13 @@ reference_targets:
|
|||
|
||||
next_agent_start_here:
|
||||
recommended_first_task: >
|
||||
Listen to the latest generated PRC SYNTH1 WAVs under
|
||||
`artifacts/parity/dx7-prc-synth1`. Continue the ladder in
|
||||
`docs/dx7-calibration-plan.md` with envelope level curve calibration.
|
||||
Single-carrier amplitude, isolated
|
||||
two-op modulation index, feedback, ratio detune, voice transpose,
|
||||
fixed-frequency operators, ROM COM compensation cut, operator key/rate
|
||||
scaling, and topology-aware route lowering are now fitted or mapped;
|
||||
a project-authored algorithm-8 stack test now backs the summed route scale
|
||||
outside PRC. Curved applied-envelope lowering plus stronger summed/cascaded
|
||||
route scale now gives PRC the same character with closer harsh stacked
|
||||
modulation. Split project-authored algorithm-8 cascade and summed-pair
|
||||
probes now back the 6.0 summed/cascaded route scale. Next pressure is
|
||||
residual community harmonic mismatch, especially `MELLOWSOLO`, plus
|
||||
missing LFO/operator attack behavior on `ANLGSYN 1`. The community gate
|
||||
now includes `{ Mooger }`, fixed `Piano Bass`, `RES SYNTH1`, and
|
||||
`ANLGSYN 1`; `MELLOWSOLO` is a pressure artifact test, not passing parity.
|
||||
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 rebuild one additive lead, one PAD texture, and one
|
||||
vocal/formant layer in readable Aquarium DSL, then name the missing
|
||||
abstractions instead of translating Zyn internals blindly. Leave DX7 alone
|
||||
unless a regression test fails.
|
||||
must_read:
|
||||
- AGENTS.md
|
||||
- docs/audio-doctrine.md
|
||||
|
|
@ -147,6 +142,17 @@ next_agent_start_here:
|
|||
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
|
||||
|
||||
completed_slices:
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
Started the ZynAddSubFX phase after closing the current DX7 community
|
||||
batch. Added `ZynInstrumentReader` for `.xiz`-shaped XML, including
|
||||
gzip-compressed XML support, and project-authored Zyn fixtures for an
|
||||
additive lead, PAD texture, and vocal/formant layer. The classifier emits
|
||||
neutral `ReferencePatch` features for active ADD/SUB/PAD engines, enabled
|
||||
kit items, layering, envelopes, free envelopes, LFOs, filters, formants,
|
||||
and effects. Upstream Zyn bank files were inspected but not vendored
|
||||
because preset provenance was not explicit enough for this first slice.
|
||||
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore: 81 passed"
|
||||
- date: 2026-05-14
|
||||
summary: >
|
||||
Reframed the latest community listening as envelope pressure rather than
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# Project-Authored ZynAddSubFX Fixtures
|
||||
|
||||
These `.xiz` files are tiny Aquarium-authored XML fixtures shaped like
|
||||
ZynAddSubFX instrument files. They are not copied from an upstream Zyn bank.
|
||||
|
||||
Purpose:
|
||||
|
||||
- exercise Zyn instrument feature extraction without vendoring unclear preset
|
||||
licenses;
|
||||
- keep the first Zyn slice focused on structural classification, not audio
|
||||
translation;
|
||||
- preserve a clean NuGet boundary. Test fixtures are development material only.
|
||||
|
||||
License: project-authored for AquariumSynthCSharp tests.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE ZynAddSubFX-data>
|
||||
<ZynAddSubFX-data version-major="1" version-minor="0">
|
||||
<INSTRUMENT>
|
||||
<INFO>
|
||||
<string name="name">AQ Additive Lead</string>
|
||||
<string name="author">AquariumSynthCSharp tests</string>
|
||||
<string name="comments">Project-authored additive fixture.</string>
|
||||
<par name="type" value="0"/>
|
||||
</INFO>
|
||||
<INSTRUMENT_KIT>
|
||||
<par name="kit_mode" value="0"/>
|
||||
<par_bool name="drum_mode" value="no"/>
|
||||
<INSTRUMENT_KIT_ITEM id="0">
|
||||
<par_bool name="enabled" value="yes"/>
|
||||
<string name="name">Additive Lead</string>
|
||||
<par_bool name="add_enabled" value="yes"/>
|
||||
<ADD_SYNTH_PARAMETERS>
|
||||
<AMPLITUDE_PARAMETERS>
|
||||
<AMPLITUDE_ENVELOPE>
|
||||
<par_bool name="free_mode" value="no"/>
|
||||
</AMPLITUDE_ENVELOPE>
|
||||
<AMPLITUDE_LFO>
|
||||
<par_real name="freq" value="5.5"/>
|
||||
</AMPLITUDE_LFO>
|
||||
</AMPLITUDE_PARAMETERS>
|
||||
<FREQUENCY_PARAMETERS>
|
||||
<FREQUENCY_ENVELOPE>
|
||||
<par_bool name="free_mode" value="no"/>
|
||||
</FREQUENCY_ENVELOPE>
|
||||
</FREQUENCY_PARAMETERS>
|
||||
</ADD_SYNTH_PARAMETERS>
|
||||
</INSTRUMENT_KIT_ITEM>
|
||||
</INSTRUMENT_KIT>
|
||||
</INSTRUMENT>
|
||||
</ZynAddSubFX-data>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE ZynAddSubFX-data>
|
||||
<ZynAddSubFX-data version-major="1" version-minor="0">
|
||||
<INSTRUMENT>
|
||||
<INFO>
|
||||
<string name="name">AQ Pad Texture</string>
|
||||
<string name="author">AquariumSynthCSharp tests</string>
|
||||
<string name="comments">Project-authored PAD fixture.</string>
|
||||
<par name="type" value="0"/>
|
||||
</INFO>
|
||||
<INSTRUMENT_KIT>
|
||||
<INSTRUMENT_KIT_ITEM id="0">
|
||||
<par_bool name="enabled" value="yes"/>
|
||||
<string name="name">Pad Texture</string>
|
||||
<par_bool name="pad_enabled" value="yes"/>
|
||||
<PAD_SYNTH_PARAMETERS>
|
||||
<AMPLITUDE_PARAMETERS>
|
||||
<AMPLITUDE_ENVELOPE>
|
||||
<par_bool name="free_mode" value="yes"/>
|
||||
</AMPLITUDE_ENVELOPE>
|
||||
</AMPLITUDE_PARAMETERS>
|
||||
<FILTER_PARAMETERS>
|
||||
<par name="category" value="2"/>
|
||||
</FILTER_PARAMETERS>
|
||||
</PAD_SYNTH_PARAMETERS>
|
||||
</INSTRUMENT_KIT_ITEM>
|
||||
</INSTRUMENT_KIT>
|
||||
</INSTRUMENT>
|
||||
</ZynAddSubFX-data>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE ZynAddSubFX-data>
|
||||
<ZynAddSubFX-data version-major="1" version-minor="0">
|
||||
<INSTRUMENT>
|
||||
<INFO>
|
||||
<string name="name">AQ Vocal Layer</string>
|
||||
<string name="author">AquariumSynthCSharp tests</string>
|
||||
<string name="comments">Project-authored layered vocal/formant fixture.</string>
|
||||
<par name="type" value="0"/>
|
||||
</INFO>
|
||||
<INSTRUMENT_KIT>
|
||||
<INSTRUMENT_KIT_ITEM id="0">
|
||||
<par_bool name="enabled" value="yes"/>
|
||||
<string name="name">Air Layer</string>
|
||||
<par_bool name="add_enabled" value="yes"/>
|
||||
<ADD_SYNTH_PARAMETERS>
|
||||
<FILTER_PARAMETERS>
|
||||
<FORMANT_FILTER>
|
||||
<par name="vowel" value="3"/>
|
||||
</FORMANT_FILTER>
|
||||
</FILTER_PARAMETERS>
|
||||
</ADD_SYNTH_PARAMETERS>
|
||||
</INSTRUMENT_KIT_ITEM>
|
||||
<INSTRUMENT_KIT_ITEM id="1">
|
||||
<par_bool name="enabled" value="yes"/>
|
||||
<string name="name">Body Layer</string>
|
||||
<par_bool name="sub_enabled" value="yes"/>
|
||||
<SUB_SYNTH_PARAMETERS>
|
||||
<FILTER_PARAMETERS>
|
||||
<par name="category" value="1"/>
|
||||
</FILTER_PARAMETERS>
|
||||
<INSTRUMENT_EFFECT>
|
||||
<par name="type" value="reverb"/>
|
||||
</INSTRUMENT_EFFECT>
|
||||
</SUB_SYNTH_PARAMETERS>
|
||||
</INSTRUMENT_KIT_ITEM>
|
||||
</INSTRUMENT_KIT>
|
||||
</INSTRUMENT>
|
||||
</ZynAddSubFX-data>
|
||||
|
|
@ -29,6 +29,7 @@ public sealed class PackageBoundaryTests
|
|||
Assert.DoesNotContain(entries, entry => entry.StartsWith("patches/", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".aqua", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".syx", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".xiz", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(entries, entry => entry.EndsWith(".py", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
91
tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs
Normal file
91
tests/AquariumSynth.Dsl.Tests/ZynInstrumentTests.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using AquariumSynth.Dsl;
|
||||
|
||||
namespace AquariumSynth.Dsl.Tests;
|
||||
|
||||
public sealed class ZynInstrumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParsesProjectAuthoredAdditiveLeadFixture()
|
||||
{
|
||||
var path = FixturePath("ZynAddSubFX", "ProjectAuthored", "additive-lead.xiz");
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
|
||||
var instrument = ZynInstrumentReader.Parse(bytes);
|
||||
var reference = instrument.ToReferencePatch(
|
||||
"zyn/project/additive-lead",
|
||||
ZynInstrumentReader.SourceForBytes("fixture://zyn/project/additive-lead.xiz", "project-authored", bytes));
|
||||
|
||||
Assert.Equal("AQ Additive Lead", instrument.Name);
|
||||
Assert.Equal("AquariumSynthCSharp tests", instrument.Author);
|
||||
Assert.Contains(reference.Features, feature => feature.Name == "engine_add" && feature.Value == "1");
|
||||
Assert.Contains(reference.Features, feature => feature.Name == "engine_sub" && feature.Value == "0");
|
||||
Assert.Contains(reference.Features, feature => feature.Name == "engine_pad" && feature.Value == "0");
|
||||
Assert.Contains(reference.Features, feature => feature.Name == "envelope_count" && feature.Value == "2");
|
||||
Assert.Contains(reference.Features, feature => feature.Name == "lfo_count" && feature.Value == "1");
|
||||
Assert.Equal("zynaddsubfx", reference.Family);
|
||||
Assert.Equal("zyn-xiz", reference.Source.Kind);
|
||||
Assert.Equal(64, reference.Source.Hash.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifiesPadAndFormantLayerPressure()
|
||||
{
|
||||
var pad = ZynInstrumentReader.ParseFile(FixturePath("ZynAddSubFX", "ProjectAuthored", "pad-texture.xiz"));
|
||||
var vocal = ZynInstrumentReader.ParseFile(FixturePath("ZynAddSubFX", "ProjectAuthored", "vocal-layer.xiz"));
|
||||
|
||||
AssertFeature(pad, "engine_pad", "1");
|
||||
AssertFeature(pad, "filter_count", "1");
|
||||
AssertFeature(pad, "free_envelope_count", "1");
|
||||
|
||||
AssertFeature(vocal, "layered_instrument", "yes");
|
||||
AssertFeature(vocal, "engine_add", "1");
|
||||
AssertFeature(vocal, "engine_sub", "1");
|
||||
AssertFeature(vocal, "formant_filter_count", "1");
|
||||
AssertFeature(vocal, "effect_count", "1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesGzippedXizXml()
|
||||
{
|
||||
var xml = File.ReadAllText(FixturePath("ZynAddSubFX", "ProjectAuthored", "additive-lead.xiz"));
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.SmallestSize, leaveOpen: true))
|
||||
{
|
||||
gzip.Write(Encoding.UTF8.GetBytes(xml));
|
||||
}
|
||||
|
||||
var instrument = ZynInstrumentReader.Parse(output.ToArray());
|
||||
|
||||
Assert.Equal("AQ Additive Lead", instrument.Name);
|
||||
Assert.Single(instrument.KitItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EngineEnableFlagsOverrideStaleParameterSections()
|
||||
{
|
||||
var instrument = ZynInstrumentReader.Parse(Encoding.UTF8.GetBytes("""
|
||||
<ZynAddSubFX-data>
|
||||
<INSTRUMENT>
|
||||
<INFO><string name="name">Stale Engine Section</string></INFO>
|
||||
<INSTRUMENT_KIT>
|
||||
<INSTRUMENT_KIT_ITEM id="0">
|
||||
<par_bool name="enabled" value="yes"/>
|
||||
<par_bool name="pad_enabled" value="no"/>
|
||||
<PAD_SYNTH_PARAMETERS/>
|
||||
</INSTRUMENT_KIT_ITEM>
|
||||
</INSTRUMENT_KIT>
|
||||
</INSTRUMENT>
|
||||
</ZynAddSubFX-data>
|
||||
"""));
|
||||
|
||||
AssertFeature(instrument, "engine_pad", "0");
|
||||
}
|
||||
|
||||
private static void AssertFeature(ZynInstrument instrument, string name, string value) =>
|
||||
Assert.Contains(instrument.Features(), feature => feature.Name == name && feature.Value == value);
|
||||
|
||||
private static string FixturePath(params string[] parts) =>
|
||||
Path.Combine([AppContext.BaseDirectory, "Fixtures", .. parts]);
|
||||
}
|
||||
Loading…
Reference in a new issue