fix(dx7-fidelity): EG release, detune, LFO, Pitch EG, EG-bias — #92/#96/#94/#93/#97#16
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).dx7ref_osc_freqlfoSourcetable (slow vibrato reachable at 0.0625 Hz)PitchEnv: start L4 / sustain L3 / release L4, non-linearpitchenv_tab(level 70 → 7.5 st, was 19.6), constant slopescaleOutputLevelVerification
slowReleaseNotKilledAtTwoSeconds,detuneMatchesDexedFrequencyDependent(vsdx7ref_osc_freq),lfoSpeedRange,PitchEGTests,EGBiasTests.swift test: 248 tests pass.Notes
curl'ing the raw asb2m10/dexedlfo.cccorrected the audit, which had cited the MSFA(165·rate)>>6formula — Dexed actually ships a 100-entrylfoSource[]table (max ~49 Hz, close to the old 47).scaleOutputLeveland 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