Approximate DX7 envelopes as ADSR

This commit is contained in:
Meta Vens 2026-05-13 16:55:30 +01:00
commit 82f99bc61d
9 changed files with 102 additions and 13 deletions

View file

@ -115,6 +115,9 @@ authorities.
controls with `[midi:on][nvoices:n]` options, leaving MIDI decoding and voice
allocation to Faust architectures unless a concrete target proves that
boundary insufficient.
- DX7 rate/level envelopes may be lowered to labeled ADSR approximations for
rebuild work, but that is not exact DX7 envelope execution. Keep exactness
pressure visible in reference features instead of smuggling it into ADSR.
## Metrics

View file

@ -28,6 +28,17 @@ public sealed record Dx7Envelope(
int Level3,
int Level4);
public sealed record Dx7EnvelopeApproximation(
Envelope Envelope,
float GateSeconds,
string Notes)
{
public string ToScriptSpec() =>
$"env=adsr:{F(Envelope.AttackSeconds)}:{F(Envelope.DecaySeconds)}:{F(Envelope.SustainLevel)}:{F(Envelope.ReleaseSeconds)} gate={F(GateSeconds)}";
private static string F(float value) => value.ToString("0.######", System.Globalization.CultureInfo.InvariantCulture);
}
public sealed record Dx7Operator(
int Number,
Dx7Envelope Envelope,
@ -136,6 +147,26 @@ public static class Dx7SysEx
return ParseVoiceEditBuffer(data);
}
public static Dx7EnvelopeApproximation ApproximateEnvelope(Dx7Envelope envelope)
{
var l1 = Level(envelope.Level1);
var l2 = Level(envelope.Level2);
var l3 = Level(envelope.Level3);
var l4 = Level(envelope.Level4);
var peak = Math.Max(0.0001f, l1);
var attack = SegmentSeconds(envelope.Rate1, Math.Abs(l1 - l4));
var decay = SegmentSeconds(envelope.Rate2, Math.Abs(l1 - l2)) +
SegmentSeconds(envelope.Rate3, Math.Abs(l2 - l3));
var release = SegmentSeconds(envelope.Rate4, Math.Abs(l3 - l4));
var sustain = Math.Clamp(l3 / peak, 0, 1);
var gate = Math.Max(attack + decay, 0.02f);
return new Dx7EnvelopeApproximation(
new Envelope(attack, decay, sustain, release),
gate,
"Approximation: DX7 EG uses four rate/level segments; ADSR keeps one sustain level and a key-off release.");
}
public static Dx7VoiceBank ParseBank(ReadOnlySpan<byte> bytes)
{
var data = PackedBankData(bytes);
@ -350,6 +381,15 @@ public static class Dx7SysEx
Value(data, 144));
}
private static float Level(int value) => Math.Clamp(value, 0, 99) / 99f;
private static float SegmentSeconds(int rate, float distance)
{
var normalizedRate = Math.Clamp(rate, 0, 99) / 99f;
var scaledDistance = Math.Clamp(distance, 0, 1);
return 0.004f + 12f * scaledDistance * MathF.Pow(1 - normalizedRate, 2.2f);
}
private static byte[] VoiceEditBuffer(ReadOnlySpan<byte> bytes)
{
if (bytes.Length == VoiceEditBufferLength) return bytes.ToArray();

View file

@ -23,10 +23,11 @@ public static class ReferenceRebuildCatalog
[
new("carrier_operators", "1,2,3,4,5,6", "Six independent carriers map cleanly to six Aquarium voices."),
new("modulation_edge_count", "0", "No operator-to-operator modulation is needed for the additive shape."),
new("self_feedback_operators", "6", "Approximated with fold/drive on the highest partial.")
new("self_feedback_operators", "6", "Approximated with fold/drive on the highest partial."),
new("operator_envelope_approximation", "DX7 EG -> ADSR", "DX7 four-rate/four-level envelopes can now be lowered to labeled ADSR approximations.")
],
[
new("operator_envelopes", "per-operator four-stage DX7 EG", "Aquarium now has ADSR envelopes, but not DX7 rate/level envelopes."),
new("operator_envelope_exactness", "per-operator four-stage DX7 EG", "Aquarium has an ADSR approximation helper, not exact DX7 rate/level envelope execution."),
new("output_compensation", "algorithm ROM COM", "Aquarium voice gains are explicit and not derived from DX7 COM scaling.")
],
"This is the honest easy case: DX7 algorithm 32 is mostly additive synthesis, so the current voice DSL can represent the carrier layout without pretending to own a full operator graph."),
@ -40,11 +41,12 @@ public static class ReferenceRebuildCatalog
new("carrier_operators", "1,3", "Represented as carriers in a first-class Aquarium operator graph."),
new("modulation_edges", "6->5,4+5->3,2->1", "Represented as explicit operator edges in the graph."),
new("feedback_sources", "4", "Represented as operator-local feedback approximation on operator 4."),
new("runtime_macros", "brightness,strike", "Field-site parameters expose useful sound controls without recompilation.")
new("runtime_macros", "brightness,strike", "Field-site parameters expose useful sound controls without recompilation."),
new("operator_envelope_approximation", "DX7 EG -> ADSR", "DX7 four-rate/four-level envelopes can now be lowered to labeled ADSR approximations.")
],
[
new("dx7_feedback_register", "one-sample feedback register", "Aquarium operator feedback currently uses a smoothed self-reference approximation, not exact DX7 feedback register timing."),
new("operator_envelopes", "independent DX7 rate/level envelopes", "Aquarium has simpler per-voice envelopes, not per-operator EGs.")
new("operator_envelope_exactness", "independent DX7 rate/level envelopes", "Aquarium has per-operator ADSR approximation, not exact DX7 EG execution.")
],
"This patch is deliberately a pressure test. The new operator graph owns the topology; exact DX7 envelope and feedback-register behavior remain pressure for later refinement.")
];

View file

@ -18,3 +18,4 @@
{"kind":"note_envelope_model","lesson":"Aquarium's general envelope model is ADSR: attack, decay, sustain level, release. Note timing belongs to `Note` records: frequency, gate duration, and host/MIDI source. SFXR sustain maps to note gate duration; SFXR punch maps to lower sustain level plus voice gain compensation so ADSR peak remains implicit 1 and the held level sits below the transient.","source":"user identified SFXR sustain as note duration and Punch as transient above sustain; punch mapping correction on 2026-05-13"}
{"kind":"faust_polyphony","lesson":"Prefer Faust-managed MIDI polyphony over Aquarium graph inflation. Aquarium should emit a single voice graph with standard `freq`, `gain`, and `gate` controls plus `[midi:on][nvoices:n]`; Faust architectures own MIDI decoding and voice allocation unless a concrete target proves the boundary insufficient.","source":"Faust official MIDI/polyphony docs and user preference for Faust support on 2026-05-13"}
{"kind":"operator_graph_parameters","lesson":"Readable operator graph fields bind runtime parameters at exact `/opgraphs/...` field paths: graph gain/note fields, operator ratio/level/feedback/envelope fields, and route index fields. Keep compact `ops=`/`edges=` as importer scaffolding and avoid graph-wide macro routing.","source":"operator graph field-site binding slice on 2026-05-13"}
{"kind":"dx7_envelopes","lesson":"DX7 four-rate/four-level operator envelopes can be lowered to labeled Aquarium ADSR approximations plus gate duration, but this is not exact DX7 EG execution. Reference rebuilds should record approximation as matched pressure and exactness as still missing.","source":"DX7 envelope approximation slice on 2026-05-13; Yamaha DX7 manual describes EGs as four rates and four levels"}

View file

@ -16,6 +16,7 @@
"reference rebuild catalog",
"DX7 SysEx structural parsing",
"DX7 algorithm topology metadata",
"DX7 envelope-to-ADSR approximation",
"standard ADSR envelope model",
"note frequency/gate model",
"Faust-managed playback/polyphony contract",
@ -51,9 +52,9 @@
"rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant."
},
"dx7_reference_path": {
"status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, two Aquarium DSL rebuild attempts, and first-class OperatorGraph support.",
"status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, envelope-to-ADSR approximations, two Aquarium DSL rebuild attempts, and first-class OperatorGraph support.",
"lesson": "Algorithm 32's additive six-carrier shape maps cleanly to six Aquarium voices. Algorithm 8 justified OperatorGraph: operators, modulation edges, carriers, and feedback belong to a graph authority, not Voice fields. Readable authoring uses operator/route/carrier declarations; compact ops/edges tuples are scaffolding only.",
"next": "Add a neutral DX7 envelope approximation path and refine Faust MIDI integration details; exact DX7 feedback-register timing remains separate pressure."
"next": "Add rendered/audio comparison once a render path is explicit; exact DX7 feedback-register timing and exact DX7 EG execution remain separate pressure."
},
"note_and_envelope_model": {
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
@ -68,7 +69,7 @@
},
"last_verified": {
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "42 passed",
"commit": "pending operator graph parameter binding commit"
"result": "43 passed",
"commit": "pending DX7 envelope approximation commit"
}
}

View file

@ -7,6 +7,16 @@ the synth library while Aquarium work continues elsewhere.
Completed this slice:
- Added `Dx7EnvelopeApproximation`, which lowers a DX7 four-rate/four-level EG
to a labeled Aquarium ADSR approximation plus gate duration.
- Reference rebuilds now record `operator_envelope_approximation` as matched
pressure and `operator_envelope_exactness` as still missing.
- Doctrine now states that DX7 EG approximation is not exact DX7 envelope
execution.
- Verified with `dotnet test AquariumSynthCSharp.slnx --no-restore`: 43 passed.
Previous slice:
- Added field-site parameter binding for readable operator graph authoring:
graph gain/note fields, operator ratio/level/feedback/envelope fields, and
route index fields now accept `@/param`.

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-13T12:55:00+01:00
updated: 2026-05-13T13:11:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -37,6 +37,7 @@ live_invariants:
- Exposed patch parameters are runtime controls, not compile-time constants.
- Parameter binding uses field-site references like `lpf=@/macro/brightness`; do not add target-routing semantics unless a later map proves a new authority is needed.
- Operator graph parameter binding also uses exact field-site paths under `/opgraphs/...`; do not add graph-wide macro routing.
- DX7 rate/level envelopes may lower to labeled ADSR approximations, but exact DX7 EG execution remains missing pressure.
- 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.
- Audio metrics are evidence, not final judgment.
@ -48,9 +49,8 @@ reference_targets:
priority: 1
purpose: "Structured FM/operator graph pressure test."
source: "https://github.com/asb2m10/dexed"
status: "DX7 SysEx parser, algorithm topology extraction, two Aquarium DSL rebuild attempts, and first-class operator graph support exist."
status: "DX7 SysEx parser, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, and first-class operator graph support exist."
first_slice:
- "Add a neutral DX7 envelope approximation path."
- "Add rendered/audio comparison once a render path is explicit."
zynaddsubfx:
priority: 2
@ -85,8 +85,9 @@ reference_targets:
next_agent_start_here:
recommended_first_task: >
Add a neutral DX7 envelope approximation path, keeping readable operator
graph authoring syntax as operator/route/carrier declarations.
Add rendered/audio comparison once a render path is explicit, keeping
readable operator graph authoring syntax as operator/route/carrier
declarations.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
@ -98,6 +99,13 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-13
summary: >
Added `Dx7EnvelopeApproximation`, a labeled helper that lowers DX7
four-rate/four-level envelopes to Aquarium ADSR plus gate duration.
Reference rebuilds now distinguish approximated envelope lowering from
still-missing exact DX7 EG execution.
verification: "dotnet test AquariumSynthCSharp.slnx --no-restore: 43 passed"
- date: 2026-05-13
summary: >
Added field-site parameter binding for readable operator graphs. Graph

View file

@ -150,6 +150,28 @@ public sealed class Dx7SysExTests
Assert.Equal([6], topology.SelfFeedbackOperators);
}
[Fact]
public void ApproximatesDx7RateLevelEnvelopeAsAdsr()
{
var approximation = Dx7SysEx.ApproximateEnvelope(new Dx7Envelope(
Rate1: 99,
Rate2: 70,
Rate3: 55,
Rate4: 40,
Level1: 99,
Level2: 80,
Level3: 50,
Level4: 0));
Assert.InRange(approximation.Envelope.AttackSeconds, 0.003f, 0.006f);
Assert.InRange(approximation.Envelope.DecaySeconds, 0.2f, 2.5f);
Assert.Equal(50 / 99f, approximation.Envelope.SustainLevel, 5);
Assert.InRange(approximation.Envelope.ReleaseSeconds, 1f, 5f);
Assert.True(approximation.GateSeconds >= approximation.Envelope.AttackSeconds + approximation.Envelope.DecaySeconds);
Assert.Contains("Approximation", approximation.Notes);
Assert.StartsWith("env=adsr:", approximation.ToScriptSpec());
}
private static byte[] InitVoice(string name, int algorithm, int feedback)
{
var data = new byte[Dx7SysEx.VoiceEditBufferLength];

View file

@ -220,6 +220,8 @@ public sealed class PatchScriptTests
Assert.Contains("process =", export.Source);
Assert.NotEmpty(rebuild.MatchedFeatures);
Assert.NotEmpty(rebuild.MissingFeatures);
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "operator_envelope_approximation");
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "operator_envelope_exactness");
Assert.All(rebuild.MissingFeatures, feature => Assert.False(string.IsNullOrWhiteSpace(feature.Notes)));
}
}