Apply DX7 envelope lowering

This commit is contained in:
Meta Vens 2026-05-14 13:40:56 +01:00
commit 65f5f84c02
9 changed files with 136 additions and 25 deletions

View file

@ -75,6 +75,12 @@ math is still fake in the places that matter perceptually.
- Aquarium staged operator envelopes now support per-segment curves, e.g.
`env=rl rates=.00145,.0508,.5268,.35 levels=2,.297,.0156,0 curves=lin,exp,exp,exp`.
This is a general staged-contour feature, not a DX7-specific syntax escape.
- `Dx7SysEx.ApproximateAppliedRateLevelEnvelope` lowers a DX7 EG into a
curved staged contour by tracing Dexed-style block-interpolated applied
gain and normalizing it against Aquarium's separate operator-output level.
On `PRC SYNTH1`, this improves log-mel from `0.24476814` to `0.21931955`
and envelope distance from `0.43700194` to `0.10271241`; zero-crossing
remains bad, so oscillator/phase behavior is still open pressure.
7. **Summed/cascaded modulation**
- Render project-authored algorithm-8 stacks that isolate `6 -> 5 -> 3`
@ -90,6 +96,8 @@ math is still fake in the places that matter perceptually.
- Keep the log-mel gate and listening artifacts.
- Raise thresholds only when the calibrated lowering improves for the right
reason.
- Current curved-envelope replay gates log-mel at `<= 0.225`, envelope
distance at `<= 0.12`, RMS ratio near unity, and score at `>= 0.50`.
## Rejected Path

View file

@ -79,15 +79,15 @@ First deliverable:
rendering exist, and `analog1.syx` voice 13 has the first thresholded parity
rebuild. The next pressure is a harder voice that actually needs audible
operator routing rather than a sine-like behavioral match. Staged operator
envelopes now exist as readable `env=rl rates=... levels=...` declarations;
direct DX7 rate/level lowering on `PRC SYNTH1` still fails hard enough to
make EG timing/gain calibration the live pressure. The hard PRC probe now
writes ignored listening artifacts under `artifacts/parity/dx7-prc-synth1`
so metrics and ears can steer the next calibration pass together. Listening
showed aggregate score was a weak judge for this target, so the hard PRC
probe now gates primarily on log-mel distance; remaining mismatch points at
DX7 output/index scaling and algorithm output compensation rather than a
missing authoring syntax. The calibration ladder lives in
envelopes now exist as readable `env=rl rates=... levels=...` declarations
with per-segment curves. Direct DX7 rate/level lowering on `PRC SYNTH1`
forced EG timing/gain calibration; the current applied-envelope lowering
improves log-mel and envelope distance but exposes worse zero-crossing/phase
behavior. The hard PRC probe writes ignored listening artifacts under
`artifacts/parity/dx7-prc-synth1` so metrics and ears can steer the next
calibration pass together. Listening showed aggregate score was a weak judge
for this target, so the hard PRC probe gates primarily on log-mel distance.
The calibration ladder lives in
[dx7-calibration-plan.md](dx7-calibration-plan.md) and should be followed
before more PRC-specific tuning.

View file

@ -275,6 +275,48 @@ public static class Dx7SysEx
"Approximation: DX7 EG uses four rate/level segments; Aquarium staged envelopes preserve the intermediate target levels.");
}
public static Dx7RateLevelEnvelopeApproximation ApproximateAppliedRateLevelEnvelope(
Dx7Envelope envelope,
float gateSeconds,
float durationSeconds = 1.4f,
int sampleRate = 44100)
{
var trace = TraceInterpolatedEnvelope(
envelope,
gateSeconds,
Math.Max(durationSeconds, gateSeconds + 0.55f),
sampleRate: sampleRate);
var firstStage2 = FirstStageIndex(trace, 2);
var firstStage3 = FirstStageIndex(trace, 3);
var peakSample = Math.Min(63, trace.Count - 1);
var level2Sample = firstStage2 >= 0 ? firstStage2 : Math.Min(peakSample + 64, trace.Count - 1);
var sustainSample = Math.Clamp(firstStage3 >= 0 ? firstStage3 : (int)(gateSeconds * sampleRate), 0, trace.Count - 1);
var r1 = Math.Max(64f / sampleRate, trace[peakSample].TimeSeconds);
var r2 = Math.Max(64f / sampleRate, trace[level2Sample].TimeSeconds - r1);
var r3 = Math.Max(64f / sampleRate, trace[sustainSample].TimeSeconds - r1 - r2);
var release = Math.Max(0.02f, Math.Min(0.6f, trace[^1].TimeSeconds - gateSeconds));
const float fullLevelGainNormalization = 0.5f;
return new Dx7RateLevelEnvelopeApproximation(
new RateLevelEnvelope(
r1,
trace[peakSample].Gain * fullLevelGainNormalization,
r2,
trace[level2Sample].Gain * fullLevelGainNormalization,
r3,
trace[sustainSample].Gain * fullLevelGainNormalization,
release,
0f,
RateLevelCurve.Linear,
RateLevelCurve.Exponential,
RateLevelCurve.Exponential,
RateLevelCurve.Exponential),
gateSeconds,
"Approximation: DX7 applied EG gain sampled per 64-sample block and normalized for Aquarium operator-level scaling.");
}
public static IReadOnlyList<Dx7EnvelopeTracePoint> TraceEnvelope(
Dx7Envelope envelope,
float gateSeconds,
@ -334,6 +376,16 @@ public static class Dx7SysEx
return points;
}
private static int FirstStageIndex(IReadOnlyList<Dx7EnvelopeTracePoint> trace, int stage)
{
for (var i = 0; i < trace.Count; i++)
{
if (trace[i].Stage == stage) return i;
}
return -1;
}
public static Dx7VoiceBank ParseBank(ReadOnlySpan<byte> bytes)
{
var data = PackedBankData(bytes);

View file

@ -39,3 +39,4 @@
{"kind":"dx7_envelope_trace","lesson":"Direct DX7 EG tracing shows the current `env=rl` abstraction is the wrong shape for some DX7 envelopes: the traced EG can spike above unit gain and drop below .02 within about 9 ms while `env=rl` is still near 1. A runtime `env=dx7` surface was attempted and cut because matching the local trace did not yet match Dexed plugin audio.","source":"DX7 envelope trace artifact on 2026-05-14"}
{"kind":"dx7_envelope_applied_gain","lesson":"Dexed-style operator audio does not use the raw DX7 EG state directly. It samples the EG once per 64-sample block and linearly interpolates applied gain across the block. A DX7-capable Aquarium envelope must model that applied-gain timebase; matching raw EG trace alone can make a candidate far too quiet or otherwise wrong.","source":"DX7 interpolated envelope trace artifact on 2026-05-14"}
{"kind":"curved_rate_level_envelope","lesson":"Aquarium staged operator envelopes now support per-segment curves and deliberate levels above unity. Keep this authority in `RateLevelEnvelope`: it protects the invariant that multi-target operator contours can model exponential synth envelopes without inventing one-off DX7 syntax.","source":"Curved rate/level envelope implementation on 2026-05-14"}
{"kind":"dx7_applied_envelope_lowering","lesson":"`Dx7SysEx.ApproximateAppliedRateLevelEnvelope` improves the hard PRC target by tracing Dexed-style block-interpolated EG gain, normalizing it against Aquarium operator output level, and emitting curved `env=rl`. Latest PRC metrics: log-mel 0.21931955, envelope distance 0.10271241, RMS ratio 1.0003664, score 0.5026183. Zero-crossing worsened to 0.28642866, so contour parity exposed a remaining oscillator/phase mismatch rather than solving DX7 parity outright.","source":"DX7 applied-envelope PRC probe on 2026-05-14"}

View file

@ -31,6 +31,7 @@
"DX7 topology-aware route lowering",
"DX7 envelope trace helper",
"DX7 block-interpolated envelope applied-gain trace helper",
"DX7 applied-envelope curved lowering",
"staged operator envelope DSL",
"curved staged operator envelope segments",
"DX7 envelope-to-ADSR approximation",
@ -71,9 +72,9 @@
"rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant. Do not consume references that cannot be backed by parity tests; parsing alone is inventory, not evidence of expressivity."
},
"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, envelope-to-ADSR approximations, measured carrier output amplitude, isolated two-op modulation route scale, calibrated feedback amount table, ratio detune lowering, ROM COM output compensation, topology-aware route lowering, staged operator envelopes with per-segment curves, raw and block-interpolated DX7 envelope tracing, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference rendering, project-authored Dexed patch rendering, Aquarium candidate rendering through Faust-generated C#, one thresholded rendered parity test for analog1.syx voice 13, a hard PRC SYNTH1 parity probe with listening WAV artifacts and log-mel <= 0.255, project-authored algorithm-8 summed-stack parity, and renderable delayed operator feedback.",
"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. 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, while summed/cascaded algorithm-8 branches need a lower topology-aware route scale. Feedback now lowers through a fitted table instead of PRC-local constants. Ratio-mode detune belongs in operator frequency lowering. ROM COM values carry output-count compensation; because the amplitude curve was fitted against algorithm 32, DX7 carriers scale by 6/(COM+1). 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. 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, which lets the DSL approximate that contour without a DX7-only syntax escape.",
"next": "Use the curved staged-envelope surface plus `Dx7SysEx.TraceEnvelope`, `Dx7SysEx.TraceInterpolatedEnvelope`, and `artifacts/parity/dx7-envelope-trace/egstep.csv` to calibrate a DX7 lowering that improves rendered PRC log-mel before adding any more public envelope syntax."
"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, measured carrier output amplitude, isolated two-op modulation route scale, calibrated feedback amount table, ratio detune lowering, ROM COM output compensation, topology-aware route lowering, staged operator envelopes with per-segment curves, raw and block-interpolated DX7 envelope tracing, applied-envelope curved lowering, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference rendering, project-authored Dexed patch rendering, Aquarium candidate rendering through Faust-generated C#, one thresholded rendered parity test for analog1.syx voice 13, a hard PRC SYNTH1 parity probe with listening WAV artifacts and log-mel <= 0.225, project-authored algorithm-8 summed-stack parity, and renderable delayed operator feedback.",
"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. 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, while summed/cascaded algorithm-8 branches need a lower topology-aware route scale. Feedback now lowers through a fitted table instead of PRC-local constants. Ratio-mode detune belongs in operator frequency lowering. ROM COM values carry output-count compensation; because the amplitude curve was fitted against algorithm 32, DX7 carriers scale by 6/(COM+1). 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. 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 improves PRC log-mel/envelope metrics, but worsens zero-crossing, so oscillator/phase behavior remains unsolved.",
"next": "Use the PRC zero-crossing/phase mismatch and project-authored envelope sweeps to separate contour wins from timbre damage before broad DX7 import."
},
"note_and_envelope_model": {
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
@ -88,7 +89,7 @@
},
"last_verified": {
"command": "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "65 passed; staged envelope curves parse/render and DX7 trace artifact includes raw/applied/curved gain",
"commit": "pending DX7 interpolated envelope trace"
"result": "66 passed; PRC log-mel 0.21931955 and envelope distance 0.10271241 with applied-envelope lowering",
"commit": "pending DX7 applied-envelope lowering"
}
}

View file

@ -15,6 +15,15 @@ Doctrine update:
Completed this slice:
- Added `Dx7SysEx.ApproximateAppliedRateLevelEnvelope`, which traces the
Dexed-style block-interpolated applied EG gain and lowers it into Aquarium's
curved staged envelope surface, normalized so operator output level remains a
separate authority.
- Retried hard `PRC SYNTH1` with the applied-envelope lowering. Latest focused
run: log-mel `0.21931955` (was `0.24476814`), envelope distance
`0.10271241` (was `0.43700194`), RMS ratio `1.0003664`, score `0.5026183`.
Zero-crossing ratio got worse at `0.28642866`, so oscillator/phase behavior
is the next exposed mismatch.
- Extended staged operator envelopes with per-segment curves:
`curves=lin,exp,exp,lin`. Linear remains the default, and levels can now
express deliberate transient overshoot above `1` instead of clipping away

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-14T05:42:00+01:00
updated: 2026-05-14T12:39:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -57,6 +57,7 @@ live_invariants:
- DX7 rate/level envelopes may lower to staged operator envelopes or labeled ADSR approximations, but exact DX7 EG timing/gain calibration remains missing pressure.
- DX7 envelope trace evidence now lives in `Dx7SysEx.TraceEnvelope`, `Dx7SysEx.TraceInterpolatedEnvelope`, and `artifacts/parity/dx7-envelope-trace/egstep.csv`; Dexed-style audio uses block-interpolated applied gain, not the raw EG state alone.
- Do not add an `env=dx7` runtime surface until it matches Dexed plugin audio with the block-interpolated gain model, not only the raw trace helper.
- DX7 applied envelope lowering now lives in `Dx7SysEx.ApproximateAppliedRateLevelEnvelope`; it normalizes traced applied gain so operator output level remains a separate authority.
- 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.
@ -115,10 +116,10 @@ next_agent_start_here:
two-op modulation index, feedback, ratio detune, ROM COM output
compensation, 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. The attempted exponential EG level curve was cut because it
still failed PRC log-mel. Use `Dx7SysEx.TraceEnvelope`,
`Dx7SysEx.TraceInterpolatedEnvelope`, and the `dx7-envelope-trace` artifact
to design the next envelope model.
outside PRC. Curved applied-envelope lowering now improves the hard PRC
log-mel and envelope metrics, but zero-crossing/phase behavior is worse.
Next pressure is oscillator/phase behavior or a project-authored envelope
sweep that can separate contour wins from timbre damage.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
@ -131,6 +132,16 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-14
summary: >
Added `Dx7SysEx.ApproximateAppliedRateLevelEnvelope` and used it in the
hard PRC SYNTH1 probe. The helper traces Dexed-style block-interpolated
applied EG gain, normalizes it against Aquarium operator-output scaling,
and emits curved `env=rl` syntax. PRC log-mel improved from 0.24476814 to
0.21931955 and envelope distance from 0.43700194 to 0.10271241, with RMS
ratio near unity. Zero-crossing ratio worsened to 0.28642866, so parity
is still not solved.
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore --filter \"PublicDomainDx7PrcSynth1WritesListeningWavsWhenInstalled|ProjectAuthoredDx7AlgorithmEightSummedStackMeetsParityWhenInstalled|ApproximatesDx7AppliedEnvelopeAsCurvedRateLevelEnvelope|TracesDx7EnvelopeAppliedBlockInterpolatedGain\": passed"
- date: 2026-05-14
summary: >
Added per-segment curves to staged operator envelopes. `env=rl` remains

View file

@ -101,9 +101,10 @@ public sealed class Dx7ReferenceParityTests
var script = Dx7VoiceProbeScript(
voice,
graphName: "dx7_prc_synth1_probe",
graphGain: 0.39f,
graphGain: 0.235f,
envelopeScale: 0.62f,
gateSeconds: null);
gateSeconds: null,
useAppliedEnvelope: true);
var candidateSource = FaustEmitter.EmitScript(script);
var candidate = await FaustCompiler.RenderAsync(
candidateSource.Source,
@ -128,8 +129,10 @@ public sealed class Dx7ReferenceParityTests
script,
comparison);
Assert.True(comparison.LogMelDistance <= 0.255f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
Assert.True(comparison.Score >= 0.40f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
Assert.True(comparison.LogMelDistance <= 0.225f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
Assert.True(comparison.EnvelopeDistance <= 0.12f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
Assert.InRange(comparison.RmsRatio, 0.95f, 1.05f);
Assert.True(comparison.Score >= 0.50f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
}
[Fact]
@ -261,7 +264,8 @@ public sealed class Dx7ReferenceParityTests
string graphName,
float graphGain,
float envelopeScale,
float? gateSeconds)
float? gateSeconds,
bool useAppliedEnvelope = false)
{
var topology = Dx7SysEx.AlgorithmTopology(voice.Algorithm);
var builder = new StringBuilder();
@ -286,7 +290,10 @@ public sealed class Dx7ReferenceParityTests
{
builder.AppendLine($" feedback={F(Dx7SysEx.OperatorFeedbackAmount(voice.Feedback))}");
}
builder.AppendLine($" {ScaledEnvelope(Dx7SysEx.ApproximateRateLevelEnvelope(op.Envelope), envelopeScale, gateSeconds).ToScriptSpec()}");
var envelope = useAppliedEnvelope
? Dx7SysEx.ApproximateAppliedRateLevelEnvelope(op.Envelope, gateSeconds ?? 0.85f)
: ScaledEnvelope(Dx7SysEx.ApproximateRateLevelEnvelope(op.Envelope), envelopeScale, gateSeconds);
builder.AppendLine($" {envelope.ToScriptSpec()}");
builder.AppendLine();
}

View file

@ -339,6 +339,28 @@ public sealed class Dx7SysExTests
Assert.InRange(trace[(int)(0.2f * 44100)].Gain, 0.12f, 0.14f);
}
[Fact]
public void ApproximatesDx7AppliedEnvelopeAsCurvedRateLevelEnvelope()
{
var approximation = Dx7SysEx.ApproximateAppliedRateLevelEnvelope(
new Dx7Envelope(
Rate1: 98,
Rate2: 64,
Rate3: 48,
Rate4: 55,
Level1: 99,
Level2: 76,
Level3: 43,
Level4: 0),
gateSeconds: 0.75f);
Assert.InRange(approximation.Envelope.Rate1Seconds, 0.0014f, 0.0015f);
Assert.InRange(approximation.Envelope.Level1, 0.99f, 1.01f);
Assert.Equal(RateLevelCurve.Exponential, approximation.Envelope.Curve2);
Assert.Contains("curves=lin,exp,exp,exp", approximation.ToScriptSpec());
Assert.Contains("64-sample block", approximation.Notes);
}
private static byte[] InitVoice(string name, int algorithm, int feedback)
{
var data = new byte[Dx7SysEx.VoiceEditBufferLength];