Calibrate DX7 feedback scale

This commit is contained in:
Meta Vens 2026-05-14 05:12:29 +01:00
commit 4c10705320
7 changed files with 47 additions and 11 deletions

View file

@ -215,6 +215,9 @@ public static class Dx7SysEx
public static float OperatorOutputAmplitude(float outputLevel) =>
MathF.Pow(2, (Math.Clamp(outputLevel, 0, 99) - 99) / 8f);
public static float OperatorFeedbackAmount(int feedback) =>
FeedbackAmounts[Math.Clamp(feedback, 0, FeedbackAmounts.Length - 1)];
public static Dx7RateLevelEnvelopeApproximation ApproximateRateLevelEnvelope(Dx7Envelope envelope)
{
var l1 = Level(envelope.Level1);
@ -566,6 +569,11 @@ public static class Dx7SysEx
254
];
private static readonly float[] FeedbackAmounts =
[
0f, 0.01f, 0.02f, 0.05f, 0.10f, 0.19f, 0.38f, 0.66f
];
private static readonly int[] ExpScaleData =
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 14, 16, 19, 23, 27, 33, 39, 47, 56, 66,

View file

@ -31,3 +31,4 @@
{"kind":"dx7_prc_logmel","lesson":"Listening and log-mel agreed that the aggregate PRC SYNTH1 score was misleading. The Dexed reference has strong low/body partials around 131 Hz and 393 Hz; Aquarium's earlier candidate lacked the low body despite a better aggregate score. The probe now gates on log-mel <=0.255 with a loose aggregate floor, and remaining mismatch points at DX7 output/index scaling and algorithm compensation.","source":"PRC SYNTH1 perceptual/log-mel calibration on 2026-05-14"}
{"kind":"dx7_output_level_curve","lesson":"A project-authored Dexed single-carrier sweep shows DX7 carrier amplitude follows 2^((outputLevel - 99) / 8). The previous internal-level proxy made mid-level operators far too loud. `Dx7SysEx.OperatorOutputAmplitude` now owns the measured curve; next calibration rung is two-operator modulation index.","source":"DX7 single-carrier calibration rung on 2026-05-14"}
{"kind":"dx7_modulation_index","lesson":"Isolated two-operator Dexed FM fits full-level modulation at about 12.55 radians. Under Aquarium's current Faust phase formula this maps to route index 6.275 for a full-level modulator. Apply this to isolated two-op branches, but do not blindly apply it to cascaded algorithm stacks before algorithm output compensation is modeled.","source":"DX7 two-operator modulation calibration rung on 2026-05-14"}
{"kind":"dx7_feedback_scale","lesson":"Isolated Dexed feedback sweeps fit a nonlinear Aquarium feedback table: 0, .01, .02, .05, .10, .19, .38, .66 for DX7 feedback values 0-7. PRC feedback value 5 was already accidentally close via feedback*0.04; the table moves that knowledge into Dx7SysEx.OperatorFeedbackAmount.","source":"DX7 feedback calibration rung on 2026-05-14"}

View file

@ -24,6 +24,7 @@
"DX7 effective operator-level approximation",
"DX7 measured carrier output amplitude curve",
"DX7 isolated modulation-index route scale",
"DX7 calibrated feedback amount table",
"staged operator envelope DSL",
"DX7 envelope-to-ADSR approximation",
"standard ADSR envelope model",
@ -63,9 +64,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, staged operator envelopes, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference 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, 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; applying that blindly to cascaded PRC stacks is wrong until algorithm output compensation exists. Staged operator envelopes now let Aquarium express four-rate/four-level contours in readable DSL.",
"next": "Follow docs/dx7-calibration-plan.md: calibrate feedback, envelope level curve, and algorithm output compensation before adding new DSL surface or tuning PRC constants."
"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, staged operator envelopes, two Aquarium DSL rebuild attempts, first-class OperatorGraph support, public-domain SysEx fixture coverage, dexed-py reference 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, 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; applying that blindly to cascaded PRC stacks is wrong until algorithm output compensation exists. Feedback now lowers through a fitted table instead of PRC-local constants. Staged operator envelopes now let Aquarium express four-rate/four-level contours in readable DSL.",
"next": "Follow docs/dx7-calibration-plan.md: calibrate envelope level curve and algorithm output compensation before adding new DSL surface or tuning PRC constants."
},
"note_and_envelope_model": {
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
@ -80,7 +81,7 @@
},
"last_verified": {
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "56 passed with AQUARIUM_DX7_PYTHON set to bundled Python",
"commit": "pending DX7 modulation index calibration"
"result": "57 passed with AQUARIUM_DX7_PYTHON set to bundled Python",
"commit": "pending DX7 feedback calibration"
}
}

View file

@ -15,6 +15,16 @@ Doctrine update:
Completed this slice:
- Completed the feedback calibration rung. Isolated Dexed feedback sweeps fit a
nonlinear Aquarium feedback table: `0, .01, .02, .05, .10, .19, .38, .66`
for DX7 feedback values `0..7`.
- Added `Dx7SysEx.OperatorFeedbackAmount` and moved the PRC feedback lowering
off the old `voice.Feedback * 0.04` constant. PRC barely changed because
feedback value `5` maps to `.19`, close to the previous accidental `.20`.
The win is ownership: feedback scaling is no longer folklore.
Previous slice:
- Completed the second calibration rung for isolated two-operator FM. A Dexed
sweep fits full-scale DX7 modulation at about `12.55` radians. Given the
current Aquarium Faust formula, that maps to an Aquarium route index of

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-14T04:10:00+01:00
updated: 2026-05-14T04:13:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -46,6 +46,7 @@ live_invariants:
- DX7 operator output level approximation belongs in reference import/rebuild helpers, not the generic DSL model.
- DX7 carrier output amplitude follows the measured `2^((outputLevel - 99) / 8)` curve; do not restore the old internal-level proxy.
- DX7 isolated two-operator modulation uses Aquarium route index `6.275` for full-level modulators; do not apply that blindly to cascaded stacks before algorithm output compensation is modeled.
- DX7 feedback lowers through `Dx7SysEx.OperatorFeedbackAmount`; do not restore PRC-local `feedback * constant` folklore.
- DX7 calibration must proceed through isolated rungs in `docs/dx7-calibration-plan.md`; do not keep tuning hard PRC-specific constants as a substitute for a level/index model.
- Staged operator envelopes own multi-target operator level contours through `env=rl rates=... levels=...`; do not force DX7-style contours back into the general ADSR voice envelope model.
- DX7 rate/level envelopes may lower to staged operator envelopes or labeled ADSR approximations, but exact DX7 EG timing/gain calibration remains missing pressure.
@ -100,10 +101,10 @@ reference_targets:
next_agent_start_here:
recommended_first_task: >
Listen to the latest generated PRC SYNTH1 WAVs under
Continue the ladder in `docs/dx7-calibration-plan.md` with feedback scaling
and algorithm output compensation. Single-carrier amplitude and isolated
two-op modulation index are now fitted; cascaded PRC stacks still need a
real compensation model.
Continue the ladder in `docs/dx7-calibration-plan.md` with envelope level
curve and algorithm output compensation. Single-carrier amplitude, isolated
two-op modulation index, and feedback are now fitted; cascaded PRC stacks
still need a real compensation model.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
@ -116,6 +117,13 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-14
summary: >
Completed the DX7 feedback calibration rung. Isolated feedback sweeps fit
Aquarium feedback amounts `0, .01, .02, .05, .10, .19, .38, .66` for DX7
feedback values 0-7. `Dx7SysEx.OperatorFeedbackAmount` now owns that
table, and the PRC probe no longer uses a local `feedback * constant`.
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore --filter \"CalibratesDx7FeedbackToAquariumFeedbackAmount|PublicDomainDx7PrcSynth1WritesListeningWavsWhenInstalled\": passed"
- date: 2026-05-14
summary: >
Completed the isolated two-operator modulation-index rung. Dexed sweeps

View file

@ -164,7 +164,7 @@ public sealed class Dx7ReferenceParityTests
builder.AppendLine($" level={F(level)}");
if (topology.SelfFeedbackOperators.Contains(op.Number))
{
builder.AppendLine($" feedback={F(voice.Feedback * 0.04f)}");
builder.AppendLine($" feedback={F(Dx7SysEx.OperatorFeedbackAmount(voice.Feedback))}");
}
builder.AppendLine($" {envelope.ToScriptSpec()}");
builder.AppendLine();

View file

@ -216,6 +216,14 @@ public sealed class Dx7SysExTests
Assert.InRange(fullScalePhaseDeviationRadians, 12.5f, 12.6f);
}
[Fact]
public void CalibratesDx7FeedbackToAquariumFeedbackAmount()
{
Assert.Equal(0, Dx7SysEx.OperatorFeedbackAmount(0));
Assert.Equal(0.19f, Dx7SysEx.OperatorFeedbackAmount(5));
Assert.Equal(0.66f, Dx7SysEx.OperatorFeedbackAmount(7));
}
[Fact]
public void ApproximatesDx7RateLevelEnvelopeAsStagedEnvelope()
{