Map DX7 detune and output compensation

This commit is contained in:
Meta Vens 2026-05-14 05:27:42 +01:00
commit e745231ed2
8 changed files with 144 additions and 44 deletions

View file

@ -45,17 +45,30 @@ math is still fake in the places that matter perceptually.
- Render one self-feedback operator across DX7 feedback values.
- Fit feedback amount separately from route/index scaling.
4. **Envelope level curve**
4. **Ratio detune**
- Lower DX7 ratio-mode detune into Aquarium operator ratios.
- Keep detune as frequency behavior, not a modulation or chorus feature.
5. **Algorithm output compensation**
- Use ROM `COM` values to balance carriers before any PRC-specific branch
boosting.
- `COM` stores the output count minus one. Because Aquarium's carrier
amplitude curve was fitted against algorithm 32's six-output baseline,
carriers scale by `6 / (COM + 1)` during DX7 lowering.
- Replace manual carrier scale constants with algorithm-derived lowering.
6. **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.
7. **Summed/cascaded modulation**
- Render project-authored algorithm-8 stacks that isolate `6 -> 5 -> 3`
and `4 + 5 -> 3`.
- Replace the hard `PRC SYNTH1` cascaded-route probe constant with a fitted
summed-modulator route rule.
6. **Hard target replay**
8. **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

View file

@ -218,6 +218,28 @@ public static class Dx7SysEx
public static float OperatorFeedbackAmount(int feedback) =>
FeedbackAmounts[Math.Clamp(feedback, 0, FeedbackAmounts.Length - 1)];
public static float OperatorOutputCompensation(Dx7AlgorithmTopology topology, int operatorNumber)
{
var outputStep = topology.RomSteps.FirstOrDefault(step =>
step.TargetOperator == operatorNumber &&
step.OutputCompensation > 0);
return outputStep is null
? 1
: 6f / (outputStep.OutputCompensation + 1);
}
public static float OperatorFrequencyRatio(Dx7Operator op, int midiNote = 60)
{
if (op.FrequencyMode == Dx7FrequencyMode.Fixed)
{
return Math.Max(0.5f, op.FrequencyCoarse);
}
var coarse = op.FrequencyCoarse == 0 ? 0.5f : op.FrequencyCoarse;
var fine = 1 + op.FrequencyFine / 100f;
return coarse * fine * RatioModeDetuneFactor(op.Detune, midiNote);
}
public static Dx7RateLevelEnvelopeApproximation ApproximateRateLevelEnvelope(Dx7Envelope envelope)
{
var l1 = Level(envelope.Level1);
@ -451,6 +473,13 @@ public static class Dx7SysEx
private static float Level(int value) => Math.Clamp(value, 0, 99) / 99f;
private static float RatioModeDetuneFactor(int detune, int midiNote)
{
var logFrequency = MathF.Log2(440f) + (midiNote - 69) / 12f;
var detuneRatio = 0.0209f * MathF.Exp(-0.396f * logFrequency) / 7f;
return MathF.Pow(2, detuneRatio * logFrequency * (Math.Clamp(detune, 0, 14) - 7));
}
private static int ScaleVelocity(int velocity, int sensitivity)
{
var clampedVelocity = Math.Clamp(velocity, 0, 127);

View file

@ -32,3 +32,4 @@
{"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"}
{"kind":"dx7_detune_and_com","lesson":"PRC exposed two separate DX7 facts: ratio-mode detune must lower into operator frequency ratios, and ROM COM output compensation must be applied relative to the algorithm-32 six-output baseline used by the carrier-amplitude calibration. This cut the PRC carrier scale table; the remaining cascaded-route constant is still a fenced hard-target scar needing summed-modulator calibration.","source":"DX7 PRC parity rung on 2026-05-14"}

View file

@ -25,6 +25,8 @@
"DX7 measured carrier output amplitude curve",
"DX7 isolated modulation-index route scale",
"DX7 calibrated feedback amount table",
"DX7 ratio detune lowering",
"DX7 ROM COM output compensation",
"staged operator envelope DSL",
"DX7 envelope-to-ADSR approximation",
"standard ADSR envelope model",
@ -64,9 +66,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, 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."
"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, 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. 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). 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 summed/cascaded modulation 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,8 +82,8 @@
"binding_rule": "Parameter binding uses field-site references such as `lpf=@/macro/brightness` and `/opgraphs/0/operators/2/level`; the field owns meaning and the parameter owns the runtime value. Avoid target-routing semantics unless a new graph authority is deliberately mapped."
},
"last_verified": {
"command": "dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "57 passed with AQUARIUM_DX7_PYTHON set to bundled Python",
"commit": "pending DX7 feedback calibration"
"command": "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore",
"result": "59 passed; latest PRC log-mel 0.24476814, score 0.5708394",
"commit": "pending DX7 detune and COM compensation"
}
}

View file

@ -15,6 +15,21 @@ Doctrine update:
Completed this slice:
- Added the ratio-detune and algorithm-output-compensation rungs to the DX7
lowering. `Dx7SysEx.OperatorFrequencyRatio` now applies ratio-mode detune
from the Dexed/DX7 note formula, and `Dx7SysEx.OperatorOutputCompensation`
maps ROM `COM` values relative to the algorithm-32 six-output baseline.
- The PRC probe no longer has a carrier-level scale table. It still has a
fenced cascaded-route constant (`1.6`) for the `op6/op5/op4 -> op3` stack;
that is now named as the next calibration target, not promoted into core
lowering.
- Latest PRC run with detune, COM compensation, graph gain `.39`, and the
fenced cascaded-route probe: log-mel `0.24476814`, score `0.5708394`, RMS
ratio `1.0108943`, zero-crossing ratio `0.7931764`, centroid ratio
`1.1131053`.
Previous 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`.

View file

@ -1,6 +1,6 @@
project: AquariumSynthCSharp
spine_version: 1
updated: 2026-05-14T04:13:00+01:00
updated: 2026-05-14T04:26:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -47,6 +47,9 @@ live_invariants:
- 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 ratio-mode detune lowers through `Dx7SysEx.OperatorFrequencyRatio`; do not silently drop detune from operator graph scripts.
- DX7 algorithm output compensation lowers through ROM `COM` values relative to the algorithm-32 six-output baseline; do not restore PRC-local carrier scale tables.
- The PRC cascaded-route value is a fenced hard-target probe scar, not a core DX7 fact. Replace it with a summed-modulator calibration rung before broad DX7 import.
- 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.
@ -64,7 +67,7 @@ reference_targets:
priority: 1
purpose: "Structured FM/operator graph pressure test."
source: "https://github.com/asb2m10/dexed"
status: "DX7 SysEx parser, public-domain SysEx fixture, dexed-py reference rendering, first thresholded rendered parity test for analog1.syx voice 13, hard PRC SYNTH1 probe with listening WAV artifacts and log-mel <= 0.255, renderable delayed operator feedback, DX7 effective operator-level approximation, staged operator envelopes, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, first-class operator graph support, and Aquarium candidate rendering through Faust-generated C# exist."
status: "DX7 SysEx parser, public-domain SysEx fixture, dexed-py reference rendering, first thresholded rendered parity test for analog1.syx voice 13, hard PRC SYNTH1 probe with listening WAV artifacts and log-mel <= 0.255, renderable delayed operator feedback, DX7 effective operator-level approximation, measured carrier output amplitude, isolated route index, feedback table, ratio detune lowering, ROM COM output compensation, staged operator envelopes, algorithm topology extraction, envelope-to-ADSR approximation, two Aquarium DSL rebuild attempts, first-class operator graph support, and Aquarium candidate rendering through Faust-generated C# exist."
first_slice:
- "Add a harder public-domain DX7 parity target that audibly needs operator routing."
zynaddsubfx:
@ -101,10 +104,12 @@ 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 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.
`artifacts/parity/dx7-prc-synth1`. Continue the ladder in
`docs/dx7-calibration-plan.md` with envelope level curve and
summed/cascaded modulation calibration. Single-carrier amplitude, isolated
two-op modulation index, feedback, ratio detune, and ROM COM output
compensation are now fitted or mapped; the PRC cascaded-route constant
still needs a real summed-modulator model.
must_read:
- AGENTS.md
- docs/audio-doctrine.md
@ -117,6 +122,15 @@ next_agent_start_here:
- "dotnet test AquariumSynthCSharp.slnx --no-restore"
completed_slices:
- date: 2026-05-14
summary: >
Added DX7 ratio-mode detune and algorithm output compensation to central
lowering. `Dx7SysEx.OperatorFrequencyRatio` preserves detune in operator
graph ratios, and `Dx7SysEx.OperatorOutputCompensation` maps ROM `COM`
output counts relative to the algorithm-32 six-output amplitude baseline.
The PRC probe no longer carries a manual carrier-level scale table; its
remaining cascaded-route value is fenced as a hard-target probe scar.
verification: "AQUARIUM_DX7_PYTHON=<bundled python> dotnet test AquariumSynthCSharp.slnx --no-restore --filter \"Dx7SysExTests|PublicDomainDx7PrcSynth1WritesListeningWavsWhenInstalled\": passed"
- date: 2026-05-14
summary: >
Completed the DX7 feedback calibration rung. Isolated feedback sweeps fit

View file

@ -152,15 +152,16 @@ public sealed class Dx7ReferenceParityTests
builder.AppendLine("opgraph");
builder.AppendLine(" name=dx7_prc_synth1_probe");
builder.AppendLine(" freq=261.6256");
builder.AppendLine(" gain=0.55");
builder.AppendLine(" gain=0.39");
builder.AppendLine();
foreach (var op in voice.Operators.OrderByDescending(op => op.Number))
{
var level = Dx7SysEx.ApproximateOperatorLevel(op).LinearLevel * CarrierLevelScale(op.Number);
var level = Dx7SysEx.ApproximateOperatorLevel(op).LinearLevel *
Dx7SysEx.OperatorOutputCompensation(topology, op.Number);
var envelope = ScaledEnvelope(Dx7SysEx.ApproximateRateLevelEnvelope(op.Envelope), 0.62f);
builder.AppendLine($"operator name=op{op.Number}");
builder.AppendLine($" ratio={F(Dx7Ratio(op))}");
builder.AppendLine($" ratio={F(Dx7SysEx.OperatorFrequencyRatio(op))}");
builder.AppendLine($" level={F(level)}");
if (topology.SelfFeedbackOperators.Contains(op.Number))
{
@ -186,32 +187,11 @@ public sealed class Dx7ReferenceParityTests
return builder.ToString();
}
private static float Dx7Ratio(Dx7Operator op)
{
if (op.FrequencyMode == Dx7FrequencyMode.Fixed)
{
return Math.Max(0.5f, op.FrequencyCoarse);
}
var coarse = op.FrequencyCoarse == 0 ? 0.5f : op.FrequencyCoarse;
return coarse * (1 + op.FrequencyFine / 100f);
}
private static float CarrierLevelScale(int operatorNumber) => operatorNumber switch
{
6 => 0.3f,
5 => 0.55f,
4 => 0.6f,
3 => 2.5f,
2 => 0.8f,
_ => 1
};
private static float PrcSynth1RouteIndex(int sourceOperator, int targetOperator) => (sourceOperator, targetOperator) switch
{
(2, 1) => Dx7SysEx.OperatorModulationRouteIndex,
// Cascaded stacks still need algorithm output compensation; do not apply the isolated two-op scale blindly.
_ => 0.7f
// Hard-target probe scar: cascaded stacks need dedicated summed-modulator calibration.
_ => 1.6f
};
private static Dx7RateLevelEnvelopeApproximation ScaledEnvelope(Dx7RateLevelEnvelopeApproximation approximation, float scale)

View file

@ -150,6 +150,18 @@ public sealed class Dx7SysExTests
Assert.Equal([6], topology.SelfFeedbackOperators);
}
[Fact]
public void MapsDx7AlgorithmOutputCompensationRelativeToAlgorithmThirtyTwo()
{
var algorithm8 = Dx7SysEx.AlgorithmTopology(8);
var algorithm32 = Dx7SysEx.AlgorithmTopology(32);
Assert.Equal(3, Dx7SysEx.OperatorOutputCompensation(algorithm8, 1));
Assert.Equal(3, Dx7SysEx.OperatorOutputCompensation(algorithm8, 3));
Assert.Equal(1, Dx7SysEx.OperatorOutputCompensation(algorithm8, 2));
Assert.Equal(1, Dx7SysEx.OperatorOutputCompensation(algorithm32, 1));
}
[Fact]
public void ApproximatesDx7RateLevelEnvelopeAsAdsr()
{
@ -224,6 +236,18 @@ public sealed class Dx7SysExTests
Assert.Equal(0.66f, Dx7SysEx.OperatorFeedbackAmount(7));
}
[Fact]
public void AppliesDx7RatioModeDetuneToOperatorFrequency()
{
var centered = InitOperator(frequencyCoarse: 1, detune: 7);
var sharp = InitOperator(frequencyCoarse: 1, detune: 14);
var flat = InitOperator(frequencyCoarse: 1, detune: 0);
Assert.Equal(1, Dx7SysEx.OperatorFrequencyRatio(centered, midiNote: 60));
Assert.InRange(Dx7SysEx.OperatorFrequencyRatio(sharp, midiNote: 60), 1.0048f, 1.0049f);
Assert.InRange(Dx7SysEx.OperatorFrequencyRatio(flat, midiNote: 60), 0.9951f, 0.9952f);
}
[Fact]
public void ApproximatesDx7RateLevelEnvelopeAsStagedEnvelope()
{
@ -287,6 +311,28 @@ public sealed class Dx7SysExTests
return data;
}
private static Dx7Operator InitOperator(
int frequencyCoarse,
int frequencyFine = 0,
int detune = 7,
Dx7FrequencyMode frequencyMode = Dx7FrequencyMode.Ratio) =>
new(
Number: 1,
Envelope: new Dx7Envelope(99, 99, 99, 99, 99, 99, 99, 0),
BreakPoint: 39,
LeftDepth: 0,
RightDepth: 0,
LeftCurve: 0,
RightCurve: 0,
RateScaling: 0,
AmplitudeModulationSensitivity: 0,
KeyVelocitySensitivity: 0,
OutputLevel: 99,
FrequencyMode: frequencyMode,
FrequencyCoarse: frequencyCoarse,
FrequencyFine: frequencyFine,
Detune: detune);
private static byte[] PackVoice(byte[] edit)
{
var packed = new byte[Dx7SysEx.PackedVoiceLength];