diff --git a/docs/dx7-calibration-plan.md b/docs/dx7-calibration-plan.md index 892e963..60fc957 100644 --- a/docs/dx7-calibration-plan.md +++ b/docs/dx7-calibration-plan.md @@ -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 diff --git a/docs/reference-synth-roadmap.md b/docs/reference-synth-roadmap.md index 581c79b..f9eb886 100644 --- a/docs/reference-synth-roadmap.md +++ b/docs/reference-synth-roadmap.md @@ -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. diff --git a/src/AquariumSynth.Dsl/Dx7SysEx.cs b/src/AquariumSynth.Dsl/Dx7SysEx.cs index 0cb6f12..a258cab 100644 --- a/src/AquariumSynth.Dsl/Dx7SysEx.cs +++ b/src/AquariumSynth.Dsl/Dx7SysEx.cs @@ -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 TraceEnvelope( Dx7Envelope envelope, float gateSeconds, @@ -334,6 +376,16 @@ public static class Dx7SysEx return points; } + private static int FirstStageIndex(IReadOnlyList 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 bytes) { var data = PackedBankData(bytes); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 02feabc..9674aab 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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"} diff --git a/state/memory.json b/state/memory.json index f7a333b..cf80a6d 100644 --- a/state/memory.json +++ b/state/memory.json @@ -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= 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" } } diff --git a/state/scratch.md b/state/scratch.md index 4ed2dd3..82be9fb 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -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 diff --git a/state/spine.yaml b/state/spine.yaml index 47f344e..1eca863 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -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= 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 diff --git a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs index 41e5baf..07eeac0 100644 --- a/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/Dx7ReferenceParityTests.cs @@ -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(); } diff --git a/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs b/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs index fc47184..7c7f3dc 100644 --- a/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs +++ b/tests/AquariumSynth.Dsl.Tests/Dx7SysExTests.cs @@ -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];