FM kFrequency: max_deviation_hz needs a keytrack, because no constant is right across a range #65
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
bcox/tymbal#65
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Patch::Ranges::max_deviation_hzis the full scale of the FM depth lane underPatch::IndexLaw::kFrequency, and it defaults to 4000.0. That default was chosenon paper. It has now been checked against a sound --
patches/chowning_horn.histhe bank's first frequency-law patch -- and the conclusion is that the default
is fine and the parameterization is the problem.
Under this law the index is derived,
I = d / fm. Withda constant in hertzand
fmrising with the note, the index falls as1/fm: across chowning-horn'sdeclared C2-C5 that is a 16x span by construction. So:
dabout 654 Hz --16% of a 4000 Hz lane.
Lowering 4000 makes the top worse; raising it makes the bottom worse. No constant
is right at both ends.
Why the obvious fixes are wrong. "The bottom of the lane is too coarse" does
not hold: the lane is Q1.30, one LSB is 3.7 uHz, and a timeline pixel is 0.18 of
index at C3. The useful settings are compressed into the bottom of the travel,
not hard to dial. And making the lane logarithmic would turn every authored
deviation ramp into an exponential index ramp -- exactly what a brass attack needs
it not to do, and most of what this law was adopted for.
The actual lever is a keytrack on the deviation: scale
dby some power offcso the index holds where the author put it, with the exponent as the authoredquantity. Exponent 0 is today's behavior (constant hertz, index falls 16x across
four octaves); exponent 1 is the phase law (constant index); the interesting
instruments live between. That makes the two laws the ends of one continuum rather
than a switch, which is worth deciding before building it.
Not urgent: chowning-horn sounds right across its declared range as it is, and
max_deviation_hzis serialized at patch minor 31, so changing the default is notfree. Filed so the reasoning is not re-derived.
Found while validating the 4000 Hz default on branch
fm-followups.Addendum, 2026-10-05
Three things, from reading the tree at
f6aa8eba. Nothing built or tried.1. Status against #34
The curves landed, so this no longer waits on them.
ModOp::kExpScaleisa * 2^(4 * param * b)(dsp/mod_ops.h:150-162), which is the exponential-in-key shape the keytrack needs. The travel half still stands: a scale-mode entry's factor is1 + depth * (product - 1), clamped, so it only attenuates (dsp/mod_matrix.h:173-181), and the depth lane's ceiling is unchanged (kLaneReachCeiling,dsp/fm_pair_layer.h:1307). The one matrix route today is to author the lane for the top of the range and attenuate downward withkExpScaleon the note source. Untried; the note source's pivot at key 60 and the op's four-octave span need checking against a real range before it counts as a workaround.2. The same need under the phase law: keyboard level scaling with a break point
The row this ticket is linked from in
moregaps.md's feature table is "level scaled along the keyboard, with a break point or curve", and that is wider than a keytrack on the frequency law's deviation:dx7/translate.cc:680-683), as is rate scaling (:684-687).kPhaseas much as underkFrequency, so the fix is not specific to the frequency law.Whether #34's ops can express a break point with a different curve on each side (a
kMaxorkMinagainst a constant, then a curve) has not been tried, and the exponent field proposed above would not cover a break point either. A design here should take the break point in.3. Two corrections to the text above
patches/chowning_horn.h's own header agrees: 16.5 at C2 to 2.1 at C5.kIntegratorResponse,dsp/fm_pair_layer.h:2707-2717), so exponent 1 equalskPhaseonly for a sine modulator. The enum selects the spectral axis and the exponent the pitch axis; they are orthogonal, not two ends of one line. That is also why theIndexLawswitch does not close this ticket.Deferred: this waits on log/lin/exp curves for the mod matrix, so that the right home for the lever gets decided rather than assumed.
Why wait. A curve on
kSharedNotewould make the note source exponential in key, which is the shape a keytrack needs. If that lands, a matrix entry may be the right place for this rather than a seventh scalar inRangeson one engine.Curves are necessary but not sufficient, which is the part worth not re-deriving. Two independent blockers, measured:
Shape.
kSharedNoteisclamp((key - 60)/64, ±1)-- linear in key (dsp/layer.h:353). Constant index needsdproportional tofc, exponential in key. An endpoint-matched straight line across chowning-horn's three octaves overshoots by 1.67x, +4.4 dB of index. Curves fix this.Travel. The depth lane's hard ceiling is
kLaneReachCeiling=Sample::kMaxRaw~= 2.0 (dsp/fm_pair_layer.h:1337). Constant index over three octaves needs 8x. Two times of travel buys an exponent of exactly 1/3, and less in practice because the envelope contour needs the lane too. Curves do not fix this.ModMatrix::Applyis(base + offset) * scale.kAddreaches the 2x ceiling but only by adding a constant, which shifts the authored contour rather than scaling it.kMulhas the right shape, but its factor is1 + depth*(product - 1)withproduct - 1in[-2, 0], so it is always <= 1 -- it can only attenuate (dsp/mod_matrix.h:148). Neither mode multiplies a lane above what the author wrote.So the curve design is where to decide whether it also raises the lane ceiling or adds an above-unity multiply mode. If curves ship as shape-only, this issue comes back needing the
Rangesfield after all.Two corrections to the text above.
patches/chowning_horn.h's own header agrees: an index of 16.5 at C2 falling to 2.1 at C5, a factor of 7.9. The rest of the arithmetic holds (654 Hz at C3, 5233 Hz at C6, 131% of the lane).kIntegratorResponse, applied atdsp/fm_pair_layer.h:3776), which is pitch-independent and survives any exponent. Exponent 1 equals kPhase only for a sine modulator. So the enum selects the spectral axis and the exponent the pitch axis: orthogonal, rather than two ends of one continuum. A keytrack applied under both laws would give that continuum properly -- kPhase at exponent -1 reproduces kFrequency at 0 -- at the cost of making every existing kPhase patch's bandwidth demand key-dependent, which re-opens the alias grid.One note for whoever picks this up, in whatever form it takes:
RequiredBandwidthRatiohas to see the same factor the render path does.dsp/fm_pair_layer.h:1863-1878documents that kFrequency's bandwidth demand is pitch-invariant precisely becausefmdivides out ofbeta * edge. Scalingdby the key breaks that invariance by construction, so a keytrack applied at note-on but not in the derivation under-provisions oversampling up the keyboard.plugin/fm_alias_test.ccis what would catch it.When mod-matrix curves land, update the DX7 importer (
dx7/translate.cc, branchdx7-import) to use them. It currently drops or linearizes the DX7's scaling, each reported in its loss report:sens/7route rather than Dexed'sScaleVelocitytable.These should become curve-shaped routes from note number and velocity, the same capability this issue waits on.