The FM pair's fixed 8x oversampling aliases audibly at high depth, ratio and pitch -- and is waste everywhere else #24
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
bcox/tymbal#24
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?
dsp/fm_pair_layer.hruns at a fixedkOversample = 8with a 513-tapdecimator (
plugin/instrument.h:166and:188). That single number issimultaneously too small for part of the parameter space and far too large for
most of it.
Found while spiking ADAA as a replacement for the oversampling. ADAA lost
decisively and is not proposed here; this is what the spike found on the way.
The defect
At
depth = 8.0 cyclesandratio = 16— both lanes exactly at the maximaFmPairLayer::Rangesdefines — the shipped engine emits alias louder thanthe signal. Measured from
playground::Instrumentitself, not from a modelof it: one voice, wave position 0, no formant band, no second modulator. The
figure is the strongest in-band component that is not on the
fc + k*fmlattice, relative to the strongest component that is, judged over
100 Hz - 18 kHz.
-93 dB is the measurement floor, not the engine's. The only difference between
those two columns is
kOversample8 -> 16 andkDecimatorTaps513 -> 1025, sothe cause is the ratio and not lane clipping or quantisation.
Note that nothing about the patch changes down that column. Only the note
does. Depth and ratio are held at maximum throughout.
Why
Alias becomes audible when sidebands fold below the decimator's stopband edge,
which is at 0.5417 of the output rate (26 kHz) at every ratio. Content above
the internal Nyquist folds to
48000*R - f, so the condition is significantsideband energy above
48000*R - 26000Hz.With
fm = ratio * fc,beta = 2*pi*depth_cyclesand the usualk_max ~= beta + 2.5*beta^(1/3), the ratio a patch needs isfcis a direct multiplier, which is why this is a per-note property and not aper-patch one.
That rule reproduces every cell measured, including the marginal one: ratio 16
at depth 6 and C5 predicts 16x, and the shipped 8x measures -22.3 dB there.
It is a design tool, not yet a validated derivation.
The other half: 8x is mostly waste
Eight voices at C2, one second of audio. Applying the rule above to real
settings:
Proposal
Derive the FM pair's ratio from the patch and the note, the way
VaLayer::DerivedOversampleRatioalready derives the VA layer's from thepatch. Same mechanism, already proven in this tree, and it fixes both halves
at once: the corner rises, and ordinary patches fall below what ships.
Two differences from the VA layer's version, both worth deciding before
building:
VaLayer::DerivedOversampleRatiotakesconst Patch&and nothing else. Thisone needs the key too. That is fine mechanically — both engines decide at
note-on and hold for the voice's life — but two simultaneously sounding
voices of the same patch can then run at different ratios, so the latency
split has to hold across them.
kAlignmentDelayalready solves exactly thisfor the VA layer by keeping both the decimator and the plain delay line
resident.
under-provisions a voice bent upward; deriving from the highest pitch the
patch can reach and rounding up is the conservative form, matching how
MinimumRatioalready rounds up for the shaper.What oversampling cannot fix
The C6 corner needs 32x, which is 1.53 cores for eight voices — below realtime.
That case needs a decision rather than a ratio:
applied to ratio rather than partials).
depth * ratio * pitchproduct so the corner cannot beselected.
Today's behaviour is (3), without having chosen it.
Related, found alongside
At C7 with ratio 16, moving depth from zero to maximum changes nothing at all:
total energy and carrier level both hold at -6.0 dB.
fmis 33.5 kHz, abovethe output Nyquist, so the additive oscillator never generates the modulator's
only partial and the FM silently disappears. That is the band-limiting doing
what it says, and it is an abrupt version of option 2 above — but as a played
result it is a cliff near the top of the keyboard at high ratios.
Reproducing
The probes were throwaway and live outside the tree. They overlay a modified
plugin/instrument.hon the include path to varykOversamplewithouttouching the worktree, and measure alias against an analytic reference
(
sin(2*pi*fc*t + beta*sin(2*pi*fm*t)) = sum_k J_k(beta) sin(2*pi*(fc+k*fm)t))rather than against another renderer. Happy to reconstruct them if this gets
picked up.
Fixed in
57f6860onmain.The ratio is derived per note over
{1, 2, 4, 8, 16}now, from areachable-interval walk over
kFmDepth,kFmRatio,kPitchand theirsecond-modulator twins. Design in
docs/superpowers/specs/2026-08-27-fm-derived-oversampling-design.md.The two open decisions, as decided
The ceiling is 16, so the C6 corner is option 3 -- chosen this time. 32x
measures 1.53 cores for eight voices, below realtime, so provisioning it
correctly would provision a case that cannot then be played polyphonically,
at 2049 taps of resident history in every layer of every voice. That corner
still aliases. The difference from the fixed 8x is that
FmAlias.TheCappedCornerStillAliasesnow asserts it, so the day the ceilingmoves, a test says so rather than the corner quietly going quiet.
Pitch is read as the note's upward reach, not the note-on pitch. Reading
downward reach would under-provision the pitch a note actually starts on.
The rule is validated now, not just reproduced
plugin/fm_alias_test.ccmeasures the strongest off-lattice component againstthe strongest on-lattice one, through
playground::Instrumentrather thanagainst a model of it, and asserts in both directions -- at the derived
ratio the alias stays down, and at a ratio forced one family step lower the
corner cells reproduce the failures measured here. A one-sided test would pass
just as happily against a measurement that had gone blind.
Two things that measurement settled:
-93 dBfloor in the report above was the analysis window, not theengine. Every provisioned cell reads -92.8 to -94.1 dB, which is exactly
where a four-term Blackman-Harris sidelobe floor sits.
+7.2measured here,which is about as close as two independent measurements of that corner get.
Two defects in the measurement design were found by building it. The lattice
has to be
|fc + k*fm|: forknegative enough the sideband reflects topositive frequency, where it is genuine signal roughly as loud as the carrier,
and a one-sided classifier scores it as the worst artifact. And the guard band
is 5 bins, not 3 -- Blackman-Harris's main lobe is eight bins wide.
The other half
Init()'s default patch, C4Measured at
-c optagainst the same benches built onmain: the defaultpatch is 3.3x faster, and the ratio-16/depth-8/C5 corner 1.7x slower -- not
the 2x the arithmetic predicted, because fixed per-sample cost outside the
oversampled loop dilutes the multiple in both directions.
kargyraais unchanged at 816 vs 834 ns/sample, with per-side spread of 6%and 10% against a 2.2% median gap. It names no modulator table, so it takes
the no-modulator branch, which this change does not touch -- the 27x from
5f1bb9asurvives by construction rather than by luck.Every row that got dearer is a row measured here emitting alias at or above
the signal. Nothing that was correct before got more expensive.
One defect found on the way that was not in this report
ReachableLaneMaximumclamped a lane's reach to 1.0, on the reasoning that"the span is where the engine's own scaling saturates". That is false for
kFmDepthandkFmRatio:ModMatrix::Applyreturns aSample,Q1_30saturates at 1.999999999069, and neither
ScaleRationorToPhaseOffsetclamps. So a near-full authored depth plus an additive
velocity -> depthentry -- the commonest route in FM there is -- rendered at up to twice the
provisioned depth, and the derived ratio was up to two rungs short.
Measured before the fix, through the same alias harness: -0.04, +6.05, +6.22,
-8.77 and +7.47 dB against a -60 dB bar. That is this issue's own defect,
reached by a different door, and the derivation would have shipped
under-provisioning it. The clamp now sits at the ceiling the matrix can
actually reach, on all five lanes. Cost:
crystal-bellalone, at seven keys,none of them its demo key.
Related, still open
#32 -- both modulators are band-limited to the output Nyquist, so FM
vanishes entirely above G6 at ratio 16. Orthogonal to this:
harmonic_limit_is the output Nyquist at every ratio, so deriving 16x does not bring the
modulator back. Split out so a moved golden is attributable to one change
rather than to both.