Calibrate DX7 carrier output level

This commit is contained in:
Meta Vens 2026-05-14 05:03:50 +01:00
commit d063536009
9 changed files with 134 additions and 36 deletions

View file

@ -0,0 +1,67 @@
# DX7 Calibration Plan
## Objective
Make DX7 reference rebuilds prove Aquarium can emulate the machine, not merely
draw a similar operator graph. The hard `PRC SYNTH1` target showed the current
DSL surface is expressive enough to describe the topology, but the importer
math is still fake in the places that matter perceptually.
## Current Mechanism
- `dexed-py` renders lawful public-domain or project-authored DX7 references.
- Aquarium renders candidate patches through Faust-generated C#.
- Tests compare rendered audio with log-mel, envelope, duration, RMS,
zero-crossing, centroid, and listening WAV artifacts.
- `OperatorGraph` owns topology: operators, routes, carriers, feedback, and
staged operator envelopes.
## Invariants
- Do not add new DSL syntax until a calibration rung proves the current surface
cannot express the behavior.
- Keep calibration fixtures project-authored or public-domain and test-only.
- Treat log-mel distance and listening notes as the primary perceptual evidence
for timbre targets. Aggregate score is secondary.
- Separate carrier amplitude, modulation index, feedback amount, envelope
level curve, and algorithm output compensation as distinct calibration facts.
- Cut PRC-specific constants once a calibrated DX7 lowering owns the same fact.
## Calibration Ladder
1. **Single carrier amplitude**
- Render project-authored DX7 algorithm-32 patches with one audible carrier.
- Sweep operator output levels.
- Fit output level to linear carrier amplitude.
- Verify Aquarium single-carrier renders match relative RMS.
2. **Two-operator modulation index**
- Render a carrier plus one modulator across modulator output levels.
- Fit DX7 output level to Aquarium route/index scaling using log-mel and
harmonic energy, not RMS.
- Keep carrier amplitude fixed while calibrating modulation index.
3. **Feedback scaling**
- Render one self-feedback operator across DX7 feedback values.
- Fit feedback amount separately from route/index scaling.
4. **Envelope level curve**
- Render fixed-output operators with varied DX7 EG levels and rates.
- Fit the rate/level envelope amplitude curve instead of assuming
`level / 99`.
5. **Algorithm output compensation**
- Use algorithm topology/ROM compensation to balance carriers before any
PRC-specific branch boosting.
- Replace manual carrier scale constants with algorithm-derived lowering.
6. **Hard target replay**
- Re-run `PRC SYNTH1`.
- Keep the log-mel gate and listening artifacts.
- Raise thresholds only when the calibrated lowering improves for the right
reason.
## Rejected Path
Do not keep tuning `PRC SYNTH1` with branch-specific magic constants. That patch
is the exam, not the textbook.

View file

@ -87,7 +87,9 @@ First deliverable:
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.
missing authoring syntax. The calibration ladder lives in
[dx7-calibration-plan.md](dx7-calibration-plan.md) and should be followed
before more PRC-specific tuning.
Source: [Dexed](https://github.com/asb2m10/dexed)

View file

@ -191,7 +191,7 @@ public static class Dx7SysEx
int midiNote = 60,
int velocity = 100)
{
var scaled = ScaleOutLevel(op.OutputLevel);
var scaled = Math.Clamp(op.OutputLevel, 0, 99);
var keyScaling = ScaleLevel(
midiNote,
op.BreakPoint,
@ -200,8 +200,8 @@ public static class Dx7SysEx
op.LeftCurve,
op.RightCurve);
var velocityOffset = ScaleVelocity(velocity, op.KeyVelocitySensitivity);
var internalLevel = Math.Max(0, (Math.Min(127, scaled + keyScaling) << 5) + velocityOffset);
var normalized = Math.Clamp(internalLevel / 4096f, 0, 1.5f);
var effectiveLevel = Math.Clamp(scaled + keyScaling + velocityOffset / 32f, 0, 99);
var normalized = OperatorOutputAmplitude(effectiveLevel);
return new Dx7OperatorLevelApproximation(
normalized,
@ -211,6 +211,9 @@ public static class Dx7SysEx
"Approximation: DX7 output level uses nonlinear scaling plus key/velocity offsets before envelope gain.");
}
public static float OperatorOutputAmplitude(float outputLevel) =>
MathF.Pow(2, (Math.Clamp(outputLevel, 0, 99) - 99) / 8f);
public static Dx7RateLevelEnvelopeApproximation ApproximateRateLevelEnvelope(Dx7Envelope envelope)
{
var l1 = Level(envelope.Level1);
@ -444,12 +447,6 @@ public static class Dx7SysEx
private static float Level(int value) => Math.Clamp(value, 0, 99) / 99f;
private static int ScaleOutLevel(int outLevel)
{
var value = Math.Clamp(outLevel, 0, 99);
return value >= 20 ? 28 + value : LevelLookup[value];
}
private static int ScaleVelocity(int velocity, int sensitivity)
{
var clampedVelocity = Math.Clamp(velocity, 0, 127);
@ -559,11 +556,6 @@ public static class Dx7SysEx
private static byte SevenBit(byte value) => (byte)(value & 0x7F);
private static readonly int[] LevelLookup =
[
0, 1, 3, 5, 6, 8, 10, 11, 13, 14, 16, 17, 19, 20, 21, 23, 24, 25, 27, 28
];
private static readonly int[] VelocityData =
[
0, 70, 86, 97, 106, 114, 121, 126, 132, 138, 142, 148, 152, 156, 160, 163,

View file

@ -29,3 +29,4 @@
{"kind":"dx7_prc_listening_artifacts","lesson":"The hard PRC SYNTH1 parity probe now writes ignored WAV artifacts for ear checks under artifacts/parity/dx7-prc-synth1. A calibrated readable operator-graph candidate with staged env=rl envelopes scores ~0.458: duration is close, while RMS and timbre remain off. Use listening plus metrics to choose the next calibration target instead of blindly adding DSL surface.","source":"PRC SYNTH1 listening WAV probe on 2026-05-13"}
{"kind":"dx7_prc_calibration","lesson":"PRC SYNTH1 improved to a modest passing score of ~0.606 without new DSL surface. Removing soft clipping, using a 392 Hz graph base, and boosting the op3 carrier branch helped; global detune and per-edge route damping worsened metrics and were cut. Remaining pressure is DX7 output/index scaling or algorithm output compensation, not more envelope syntax.","source":"PRC SYNTH1 calibration pass on 2026-05-14"}
{"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"}

View file

@ -22,6 +22,7 @@
"DX7 SysEx structural parsing",
"DX7 algorithm topology metadata",
"DX7 effective operator-level approximation",
"DX7 measured carrier output amplitude curve",
"staged operator envelope DSL",
"DX7 envelope-to-ADSR approximation",
"standard ADSR envelope model",
@ -44,6 +45,7 @@
],
"doctrine_refs": [
"docs/audio-doctrine.md",
"docs/dx7-calibration-plan.md",
"docs/migration-checklist.md",
"docs/reference-synth-roadmap.md",
"state/spine.yaml"
@ -60,9 +62,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, effective operator-level approximations, 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 helps import/rebuild math but does not solve hard parity alone. Staged operator envelopes now let Aquarium express four-rate/four-level contours in readable DSL. PRC SYNTH1 listening proved aggregate score can reward the wrong thing; the current probe gates on log-mel and restores low harmonic body, but zero-crossing/centroid mismatch still exposes weak DX7 output/index scaling.",
"next": "Use PRC SYNTH1 listening artifacts and spectral peaks to calibrate DX7 output/index scaling or algorithm output compensation before adding new DSL surface."
"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, 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.26, 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). Staged operator envelopes now let Aquarium express four-rate/four-level contours in readable DSL. PRC SYNTH1 still exposes weak modulation-index and output-compensation calibration.",
"next": "Follow docs/dx7-calibration-plan.md: calibrate single-carrier amplitude, two-op modulation index, feedback, envelope level curve, and algorithm output compensation before adding new DSL surface or continuing PRC-specific tuning."
},
"note_and_envelope_model": {
"rule": "Notes own frequency, gate duration, and host/MIDI source. Envelopes own ADSR level shape.",
@ -77,7 +79,7 @@
},
"last_verified": {
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "54 passed with AQUARIUM_DX7_PYTHON set to bundled Python",
"commit": "pending PRC SYNTH1 log-mel calibration"
"result": "55 passed with AQUARIUM_DX7_PYTHON set to bundled Python",
"commit": "pending DX7 carrier output calibration"
}
}

View file

@ -15,6 +15,24 @@ Doctrine update:
Completed this slice:
- Wrote `docs/dx7-calibration-plan.md` and referenced it from memory/spine.
The ladder is now: single-carrier amplitude, two-op modulation index,
feedback scaling, envelope level curve, algorithm output compensation, then
hard PRC replay.
- Completed the first calibration rung. A project-authored Dexed single-carrier
output-level sweep fits `2^((outputLevel - 99) / 8)` for carrier amplitude.
`Dx7SysEx.OperatorOutputAmplitude` now owns that curve, and
`ApproximateOperatorLevel` uses it instead of the old internal-level proxy.
- Added tests for the measured output-level curve. The hard PRC probe now gates
log-mel at `<= 0.26` while the next rung calibrates modulation index; latest
run after the carrier fix: log-mel `0.2562075`, score `0.40430218`.
- Calibration lesson: the old operator level helper was not a harmless
approximation. It made mid-level operators far too loud and let PRC-specific
constants compensate for a broken foundation. Next pressure is route/index
scaling for isolated two-operator FM.
Previous slice:
- Reoriented the `PRC SYNTH1` hard probe around the perceptual metric that
matched listening: log-mel distance. The probe now asserts log-mel
`<= 0.255` plus a loose aggregate score floor instead of treating aggregate

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-14T03:15:00+01:00
updated: 2026-05-14T04:04:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -25,6 +25,7 @@ current_mechanism:
reference_rebuilds: src/AquariumSynth.Dsl/ReferenceRebuilds.cs
tests: tests/AquariumSynth.Dsl.Tests/PatchScriptTests.cs
doctrine: docs/audio-doctrine.md
dx7_calibration_plan: docs/dx7-calibration-plan.md
roadmap: docs/reference-synth-roadmap.md
parameter_contracts: src/AquariumSynth.Dsl/Model.cs
@ -43,6 +44,8 @@ live_invariants:
- Operator graph parameter binding also uses exact field-site paths under `/opgraphs/...`; do not add graph-wide macro routing.
- Operator feedback must render through Faust as delayed recursion, not as a direct cyclic self-reference.
- 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 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.
- Recompile only for graph shape changes; vary declared parameters through the hosted DSP control API.
@ -96,14 +99,14 @@ reference_targets:
next_agent_start_here:
recommended_first_task: >
Listen to the latest generated PRC SYNTH1 WAVs under
`artifacts/parity/dx7-prc-synth1/<run>`. The current candidate improved on
log-mel by restoring the low harmonic body, but aggregate score fell because
zero-crossing/centroid still disagree. The next calibration should model
DX7 output/index scaling and algorithm output compensation rather than
optimizing the aggregate score.
Continue the ladder in `docs/dx7-calibration-plan.md` with isolated
two-operator modulation-index calibration. Single-carrier output amplitude
is now fitted; do not keep tuning PRC constants until route/index scaling is
measured.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
- docs/dx7-calibration-plan.md
- docs/reference-synth-roadmap.md
- state/spine.yaml
- state/memory.json
@ -112,6 +115,15 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-14
summary: >
Added `docs/dx7-calibration-plan.md` and completed the first calibration
rung. A project-authored Dexed single-carrier output-level sweep showed
carrier amplitude follows `2^((outputLevel - 99) / 8)`. `Dx7SysEx` now
exposes `OperatorOutputAmplitude` and `ApproximateOperatorLevel` uses that
measured curve instead of the old internal-level proxy. The PRC log-mel
gate is temporarily 0.26 while the next rung calibrates modulation index.
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore --filter \"Dx7SysExTests|PublicDomainDx7PrcSynth1WritesListeningWavsWhenInstalled\": passed"
- date: 2026-05-14
summary: >
Reoriented the hard DX7 `PRC SYNTH1` probe around log-mel distance after

View file

@ -122,7 +122,7 @@ public sealed class Dx7ReferenceParityTests
script,
comparison);
Assert.True(comparison.LogMelDistance <= 0.255f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
Assert.True(comparison.LogMelDistance <= 0.26f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
Assert.True(comparison.Score >= 0.40f, $"{ParityReport(comparison)}{Environment.NewLine}artifacts: {artifactDir}");
}
@ -174,7 +174,7 @@ public sealed class Dx7ReferenceParityTests
{
foreach (var source in edge.SourceOperators)
{
builder.AppendLine($"route from=op{source} to=op{edge.TargetOperator} index={F(RouteIndex(source, edge.TargetOperator))}");
builder.AppendLine($"route from=op{source} to=op{edge.TargetOperator} index=0.7");
}
}
@ -202,16 +202,11 @@ public sealed class Dx7ReferenceParityTests
6 => 0.3f,
5 => 0.55f,
4 => 0.6f,
3 => 2.3f,
3 => 2.5f,
2 => 0.8f,
_ => 1
};
private static float RouteIndex(int sourceOperator, int targetOperator) => (sourceOperator, targetOperator) switch
{
_ => 0.7f
};
private static Dx7RateLevelEnvelopeApproximation ScaledEnvelope(Dx7RateLevelEnvelopeApproximation approximation, float scale)
{
var envelope = approximation.Envelope;

View file

@ -192,13 +192,22 @@ public sealed class Dx7SysExTests
FrequencyFine: 0,
Detune: 4));
Assert.Equal(108, operatorLevel.ScaledOutputLevel);
Assert.Equal(80, operatorLevel.ScaledOutputLevel);
Assert.Equal(-5, operatorLevel.KeyScalingOffset);
Assert.Equal(0, operatorLevel.VelocityOffset);
Assert.InRange(operatorLevel.LinearLevel, 0.80f, 0.81f);
Assert.Equal(.125f, operatorLevel.LinearLevel, 5);
Assert.Contains("nonlinear", operatorLevel.Notes);
}
[Fact]
public void MapsDx7OutputLevelToMeasuredCarrierAmplitudeCurve()
{
Assert.Equal(1, Dx7SysEx.OperatorOutputAmplitude(99));
Assert.Equal(MathF.Pow(2, -1), Dx7SysEx.OperatorOutputAmplitude(91), 5);
Assert.Equal(MathF.Pow(2, -2), Dx7SysEx.OperatorOutputAmplitude(83), 5);
Assert.Equal(MathF.Pow(2, (80 - 99) / 8f), Dx7SysEx.OperatorOutputAmplitude(80), 5);
}
[Fact]
public void ApproximatesDx7RateLevelEnvelopeAsStagedEnvelope()
{