Every Lfo starts from the same sample-and-hold seed #33
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
bcox/tymbal#33
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?
Every
Lfois value-initialized with the same sample-and-hold seed, so two S&H LFOs never have independent sequences.Symptom
Two LFOs on one layer, both set to
kSampleHold:This does not need a chord, a cold instrument, or polyphony. One voice, one layer, two LFOs is enough, which is why it is worth fixing rather than working around.
Cause
dsp/lfo.h:317—std::uint32_t random_ = 0x2545F491u;Nothing ever writes it. Every
Lfoarray is value-initialized, so every element takes that constant:dsp/layer.h:489— 2 per voicedsp/fm_pair_layer.h:3380— 2 per FM pair layerdsp/va_layer.h:1622— 3 per VA layerWith 8 layers per voice and 200 voices, every S&H generator in the instrument starts from one word.
Lfo::NoteOndeliberately does not reseed (dsp/lfo.h:97-99), and that is correct — it is what makes a second note a new draw rather than a replay. The defect is that there is no seed to preserve in the first place.Second axis: across voices
The same constant also correlates voices. Two voices' generators differ only by their accumulated note-ons and rendered wraps (
OnWrapatdsp/lfo.h:224-232, called fromNoteOnatdsp/lfo.h:124), so ordinary playing separates them within a note or two — but a fresh instrument struck with a simultaneous chord gives every voice the identical S&H stream, and identical rates keep them locked.This is the case that surfaced it: 30 voices of an authored pitch wander, all drawing the same numbers.
Precedent
6776866"Give every voice its own random sequence" fixed exactly this shape forRandomA/RandomB, scoped to the sources the modulation matrix routes. TheLfogenerator is the second copy of the same bug, one level in, and was never in that diff.That commit's reasoning applies unchanged here, including the part that matters most: the seed must be a bit-mixing finalizer of the index, not an affine function of it. An LCG seeded affinely gives a first draw affine in the index — a chord comes out as a ramp.
Fix direction
Seed each
Lfofrom the voice seed mixed with the LFO's identity (layer slot and LFO index), using the same finalizer asVoiceAllocator::SeedFor(dsp/voice_allocator.h:49). Seed once, at the same point the voice is seeded; do not reseed on note-on, and do not reseed onReset— a panic stops the sound, it does not rewind the dice.Tests
dsp/lfo_test.cchasSampleAndHoldDoesNotReplayItsSequenceOnEveryNote, which covers one LFO across notes. Nothing covers independence between instances. Add:6776866's test already established. A minimum-gap assertion would be backwards: a ramp has perfectly even gaps.Verify both by injecting the shared seed and watching them fail.
Expected fallout
plugin/render_golden_test.ccrendersDefaultFmPairPatch, which does not selectkSampleHold, so the goldens should not move. Confirm rather than assume.Three factory patches do use S&H —
arp2600_burble,rattlesnake,kargyraa— and will change how they sound. That is the fix working, not a regression, but it is worth an audition pass.