The bug that ate PLN's whole afternoon, and the reason he was working around it by
hand: "so for now hush then ctrl+enter seems like a fix ahah but not a lovely one :D".
## The symptom, and the clue that cracked it
"wait i feel theres still a xfade issue? when i leave d5 on the 33 effcet, it
fades in oblivion!" (and d4, d7, d8, d12...)
"reboot-then-ctrl-enter: doesnt seem to obliviate, loops forever. sounds like a
track-to-track state bug"
Same code, two outcomes, depending on history. That is the whole diagnosis in one
sentence — and `hush` curing it is the confirmation, because `streamHush`
PREPENDS silence to the pattern history.
## The mechanism
`BootTidal.hs:100` made every dN a 4-cycle crossfade (#13):
xfade i = transition tidal True (Sound.Tidal.Transition.xfadeIn 4) i
d5 = xfade 5 . (|< orbit 4)
Tidal's xfadeIn is one line:
xfadeIn t now (pat:pat':_) = overlay (pat |* gain rising) (pat' |* gain falling)
The envelopes are correct — measured with queryArc, the incoming rises 0.442 ->
1.0 and the outgoing falls 0.990 -> 0.0 across the 4 cycles, then both saturate.
The bug is that the outgoing pattern is turned DOWN but never OFF. `|*` takes
structure from the LEFT, so `pat'` keeps every event it ever had. Measured: at
cycle 64 of a 4-cycle fade, the outgoing pattern still emits one onset per cycle.
**And a gain-0 event is not a harmless event.** SuperDirt broadcasts the cut group
in `playSynths` (DirtEvent.sc:182-189) *before* amplitude is ever read — `~amp`
only appears later, at line 160, as an argument to the gate synth — and for a
positive `cut` it sets `cutAllSamples: 1`, releasing EVERY voice in the group
regardless of sample. So the silent ghost keeps killing the incoming pattern's
voice on every single hit, forever. The orbit is not fading out. It is being cut
to death by its own predecessor.
Every observation follows, including the ones that looked contradictory:
* only orbits that own a `# cut` die. In take_5_drops, where PLN said "weirdly
all d123 stay", d1/d2/d3 have NO cut group — and the ones he reported dying in
vague_de_crime (d4, d5, d7) have cut 4, cut 5, cut 7.
* a fresh boot is fine: one pattern in history, and `xfadeIn _ _ (pat:[]) = pat`
returns it untouched, envelopes and all.
* "4 bars" is the fade length. You hear the OLD pattern fade out — it wins the
cut, being overlaid second and therefore sent last — and the new one never
arrives at all.
* d8's "weird glitches instead of proper heading breaks": cut 8, a chopped break
fighting its own ghost.
Not explained by this, and still open: take_5_drops' d4, which has no cut group at
all. That fade is a separate lead (it shares `crushbus 41` with d7 — see the new
pvlint PV011).
## The fix
Keep Tidal's envelopes EXACTLY, and stop the outgoing pattern when its fade ends:
xfadeCutIn t now (q:q':_) =
overlay (q |* gain rising) (playFor now (now + t) q' |* gain falling)
Measured, onsets per cycle (incoming/outgoing), fading at cycle 0 over 4:
xfadeIn 4 1/1 1/1 1/1 1/1 | 1/1 1/1 1/1 1/1 ghost forever
clutchIn 4 0/1 0/1 1/0 1/0 | 1/0 1/0 1/0 1/0 clean, never overlaps
xfadeCut 4 1/1 1/1 1/1 1/1 | 1/0 1/0 1/0 1/0 clean from cycle 4
`clutchIn` is also clean — it degrades one pattern into the other so they never
overlap at all — and is arguably the better transition for a rig where most orbits
own a cut group. It is NOT the default here because it changes the feel of every
transition on the rig, and 7 days before a gig is not when to do that. It stays
available as `clutchIn`, and Tidal's original stays reachable as `xfadeLeaky` for
A/B comparison.
Audibly, for a cut orbit, the transition becomes: the old pattern fades out over 4
bars, the new one arrives at full exactly as its envelope reaches 1.0. Which is
what PLN already describes hearing — except that now the new one arrives.
## Validation
- The binding was lifted VERBATIM out of BootTidal.hs and typechecked against the
type `transition` demands (`Time -> [ControlPattern] -> ControlPattern`). The
stream block cannot be typechecked in place — it closes over a live `Stream` —
so this is how that part of the file gets checked at all.
- `tools/check-boot.sh` gains **Pass 5**, a permanent gate: it lifts `xfadeCutIn`
out of the boot file and counts onsets from the outgoing pattern at cycles
4/5/8/16/64/256. Must be 0.
- Mutation-tested, which is the only reason to trust it: with `playFor` removed
the gate reports 6 ghost onsets and fails. Passes 1-4 all stay green on the
broken version — it typechecks, it parses as Pulsar sends it, and it emits
events. Counting the outgoing onsets is the only check that can see this class
of bug, which is exactly why it now runs on every boot check.
- All 32 blocks still parse as single statements (#79 seam intact) — the new
multi-line `case` sits inside the existing `let`, so it is still one statement.
| Name |
Last commit
|
Last update |
|---|---|---|
| .. | ||
| at | Loading commit data... | |
| bridge | Loading commit data... | |
| foundry | Loading commit data... | |
| pulsar-parvagues-hud | Loading commit data... | |
| pvlint | Loading commit data... | |
| tests | Loading commit data... | |
| README.md | Loading commit data... | |
| analyze_samples.py | Loading commit data... | |
| button_roles.py | Loading commit data... | |
| check-boot-blocks.py | Loading commit data... | |
| check-boot.sh | Loading commit data... | |
| check-drift.sh | Loading commit data... | |
| check-mix.py | Loading commit data... | |
| check-tracks.sh | Loading commit data... | |
| control-lens.py | Loading commit data... | |
| deshadow-helpers.py | Loading commit data... | |
| fan_check.sh | Loading commit data... | |
| fix-button-roles.py | Loading commit data... | |
| gen_tokens.py | Loading commit data... | |
| gen_ts_types.py | Loading commit data... | |
| gig-log.py | Loading commit data... | |
| gig_record.sh | Loading commit data... | |
| lcxl-init.py | Loading commit data... | |
| lcxl-leds-watch.service | Loading commit data... | |
| lcxl-leds.py | Loading commit data... | |
| lcxl_grid.json | Loading commit data... | |
| lcxl_grid.py | Loading commit data... | |
| migrate-columns.py | Loading commit data... | |
| mock-lcxl.py | Loading commit data... | |
| mute.tidal | Loading commit data... | |
| orphan-orbits.py | Loading commit data... | |
| probe-chain.py | Loading commit data... | |
| sample_tfidf.py | Loading commit data... | |
| setlist_samples.py | Loading commit data... | |
| shipowiz.py | Loading commit data... | |
| silent-eval.py | Loading commit data... | |
| solo.tidal | Loading commit data... | |
| surface-columns.py | Loading commit data... | |
| tempo-lens.py | Loading commit data... | |
| tidal-remote.py | Loading commit data... | |
| tools.png | Loading commit data... | |
| unmute.tidal | Loading commit data... | |
| unsolo.tidal | Loading commit data... |