Skip to content

fix(dx7-fidelity): EG release, detune, LFO, Pitch EG, EG-bias — #92/#96/#94/#93/#97#16

Merged
hakaru merged 6 commits into
mainfrom
fix/dx7-fidelity-audit
Jun 30, 2026
Merged

fix(dx7-fidelity): EG release, detune, LFO, Pitch EG, EG-bias — #92/#96/#94/#93/#97#16
hakaru merged 6 commits into
mainfrom
fix/dx7-fidelity-audit

Conversation

@hakaru

@hakaru hakaru commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Five DX7-fidelity audit fixes for the engine/voice dimensions, each verified against the real DEXED/MSFA source (fetched / curl'd) and TDD'd. Follows the keyboard-scaling fix in #14 (v1.16.0).

# Fix Reference
#92 Drop the Swift-only 2 s release force-kill (truncated + clicked slow tails) — release runs to natural completion DEXED env.cc (no timeout)
#96 Frequency-dependent operator detune (~±17 c bass → ~±3.5 c treble), not a constant ±7 c dx7ref_osc_freq
#94 LFO speed→Hz from the verbatim DEXED lfoSource table (slow vibrato reachable at 0.0625 Hz) asb2m10/dexed lfo.cc (raw)
#93 Pitch EG rewritten to DEXED PitchEnv: start L4 / sustain L3 / release L4, non-linear pitchenv_tab (level 70 → 7.5 st, was 19.6), constant slope asb2m10/dexed pitchenv.cc (raw)
#97 Implement the controller→EG-bias destination (was stored but inert) — real-time operator-level boost through scaleOutputLevel DX7 EG-bias behavior

Verification

  • TDD throughout (RED → GREEN); each fix checked against the real DEXED source where one exists.
  • New coverage where the audit found none: slowReleaseNotKilledAtTwoSeconds, detuneMatchesDexedFrequencyDependent (vs dx7ref_osc_freq), lfoSpeedRange, PitchEGTests, EGBiasTests.
  • swift test: 248 tests pass.

Notes

  • #94: curl'ing the raw asb2m10/dexed lfo.cc corrected the audit, which had cited the MSFA (165·rate)>>6 formula — Dexed actually ships a 100-entry lfoSource[] table (max ~49 Hz, close to the old 47).
  • #97: the EG-bias OL-point scale has no clean DEXED reference (controllers aren't in the C twin); it is routed through the real scaleOutputLevel and is calibratable by ear. With no controller assigned, the render path is byte-identical to v1.16.0.

Closes #92, #93, #94, #96, #97. (App-side #91 velocity is a separate PR.)

🤖 Generated with Claude Code

Hakaru Hirose and others added 6 commits June 30, 2026 10:05
… tails (#92)

DX7Envelope killed the release after a fixed ~2s wall-clock (releaseTimeoutBlocks), forcing level=0/ix=-1. That truncated and clicked slow-release tails (any R4<=35) — a Swift-only divergence the real DX7 and the DEXED C twins do not have. Remove the timeout; the release now runs to natural completion (advance(4)->ix=-1 at the L4 floor), bit-exactly matching the DEXED EG trace. Long-release voice slots are reclaimed by voice stealing, not a wall-clock guillotine.

Test: slowReleaseNotKilledAtTwoSeconds (R4=20 still ringing at 2.5s). 242 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M2DX applied a pitch-independent constant +-7c operator detune, whereas the real DX7/DEXED and the dx7ref C twin (dx7ref_osc_freq) make detune frequency-dependent: ~+-17c in the bass down to ~+-3.5c in the treble. Detuned operator pairs therefore beat at the wrong (uneven) rate, most off in the bass.

Compute the factor per note at note-on from DEXED's formula (0.0209*exp(-0.396*log2 f)/7 * log2 f * (detune-7)) via dexedDetuneFactor; plumb the detune param (detuneCents = detune-7) through OperatorSnapshot -> DX7Operator. Fixed-freq ops unaffected (frequency overridden; detune cancels).

Test: detuneMatchesDexedFrequencyDependent (within 0.4c of dx7ref_osc_freq across C1..C8 x params 0-14, + C1 > 2.5x C8). 243 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ponential (#94)

lfoSpeedToHz was a pragmatic exponential (1.47*e^(0.035*speed)) whose own docstring admitted it was 'pending exact DX7/Dexed reference data'; its 1.47 Hz floor at speed 0 made the DX7's slow vibrato and multi-second LFO sweeps (sub-0.1 Hz) unreachable. Replace it with the verbatim DEXED lfoSource[] table (asb2m10/dexed lfo.cc, fetched raw — the audit's cited MSFA (165*rate)>>6 formula is NOT what Dexed ships): 0.0625 Hz at speed 0 up to ~49.3 Hz at speed 99.

Test: lfoSpeedRange rebaselined to the table (speed 0 ~0.0625 Hz, 35 ~5.6, 99 ~49.3); monotonic test unchanged. 243 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tant slope (#93)

The Pitch EG (a from-scratch Swift reimplementation with no C-twin coverage) diverged from DEXED PitchEnv (pitchenv.cc, fetched raw) three ways: (1) level rotation — started at PL1/sustained PL4/released to PL1, inverting held pitch + adding a spurious release glide; now starts L4, sustains L3, releases L4. (2) level->pitch was linear (~2.6x too sensitive mid-range, level 70 -> 19.6 st); now the non-linear pitchenv_tab (level 70 -> 7.5 st). (3) rate set a fixed per-stage duration; now a constant per-block slope (inc = pitchenv_rate[rate]*unit), time proportional to level distance.

Adds kPitchEnvTab/kPitchEnvRate (verbatim DEXED tables) + PitchEGTests (start-at-L4, non-linear curve, sustain-L3/release-L4) — the dimension had no prior coverage. 246 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wheel/foot/breath/AT EG-bias had snapshot fields + setters but the value was never read in synthesis — the DX7's main expressive dynamics path (breath/AT-controlled brightness swells) was inert. Apply it per render block: each controller's EG-bias range (0-99) x its depth sums to an OL boost (capped 99), routed through the real scaleOutputLevel as an exact level offset on every operator (modulators + carriers), so the bias brightens + swells the voice in real time. egBiasOL 0 is byte-identical to the old inert path. (OL-point scale calibratable by ear.)

Threads egBiasOL: per-slot mod -> DX7Voice -> updateGain. Test: EGBiasTests (exact scaleOutputLevel delta; 0 = no-op). 248 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fixes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hakaru
hakaru merged commit dc400ad into main Jun 30, 2026
1 check failed
@hakaru
hakaru deleted the fix/dx7-fidelity-audit branch June 30, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant