Commit 3685c743 by PLN (Algolia)

fix(boot): an UNTOUCHED "^NN" yields no events, not 0 — the real mute-bomb (#55)

The bug behind "no sound on d5 in perfect.tidal, meter moves, master silent,
plays a bar then slowly fades to zero, then crackles". Also, finally, the
true root of #21 — which was diagnosed as a missing `djfbus` and fixed as
"safe at rest (0)", when the actual defect was one layer deeper.

FINDING, proven by queryArc against an empty controls map (not by ear):

    gF1 $ sound "bd", knob never touched  ->  SILENT (no events)
    gMask  untouched                      ->  SILENT
    gMute3 untouched                      ->  SILENT
    gain ("^77" * 1.3) untouched          ->  SILENT
    gPanic untouched                      ->  SILENT

A bare `"^NN"` whose CC has never arrived does NOT default to 0 — it yields
NO EVENTS AT ALL. And `#` (i.e. `|>`) cannot emit without a value on its
right-hand side, so the entire stream disappears. Every g*-wrapped stream in
the corpus is therefore silent until that physical control is moved once.

This is why "move all the controls" was always step one of a gig, and why
PLN's rig felt fine for months: the habit was masking a mute-bomb. Today's
BootTidal restart (to pick up the 77903244 DJF fix) wiped the control map and
re-armed it across the whole surface at once — so the fix appeared to make
things worse.

It also explains the odd "plays one bar then slowly fades to zero": `dN` is
`xfade N`, which crossfades the outgoing pattern out over 4 cycles while
fading the new one in. The new one had no events, so what PLN heard was the
OLD pattern dying gracefully into nothing — meter moving throughout, master
quiet once the fade completed. The symptom looked like a filter closing;
it was a crossfade to silence.

TWO-LAYER FIX, because one layer cannot reach everything:

1. `orDef` in the helper let-block — substitutes a default when a control
   pattern is empty. Applied to gDJF (0.5 = centre = true bypass),
   midiOn/midiOff/midiNo/midiBoth (0/1/1/0 = "not pressed"), midiGGlobal
   (0.769, so *1.3 == unity rather than 0), and gPanic (0 = normal, not
   silent). It must live at the HELPER level, not per call site, because
   tracks combine controls arithmetically — `midiOn ("^34" - "^18")` in
   perfect.tidal:63 — and `touched - untouched` is itself empty, so a
   per-CC default could never catch it.

2. Boot-time control seeding via `setF` — the corpus-wide half. `orDef`
   cannot reach the 1860 sites across 343 track files that apply controls
   directly (`# crushbus 41 (range 16 3.5 "^53")`). Editing those is
   #46-scale work. Seeding every LCXL control at boot fixes all of them at
   once while touching no track file; a real CC overwrites its seed the
   instant the knob moves.

Seed policy follows the authoring convention `range <neutral> <extreme>`,
so 0 == neutral: A/B knobs 0, C1-C3 0.5 (DJF centre), C4-C8 0, fader 77
0.769 (unity), faders 78-84 1.0 (gains open = audible), all buttons 0.

Seed coverage was verified by set-difference, not assumed: 49 seeded vs 54
distinct "^NN" in live/. The 5 stragglers were each resolved rather than
waved off — ^45 needs no seed (it flows through midiOn, so orDef covers
it), ^6 and ^61 are commented out at their only call sites, and ^1 (an
external keyboard modwheel) and ^21 were added explicitly.

Verification of the fix, all four knob positions, via an explicit controls
map — untouched now bypasses and NO touched behaviour changed:

    untouched -> cutoff=20000 hcutoff=20     (was: SILENT)
    at 0.0    -> cutoff=180   hcutoff=20     (low-pass to subbass)
    at 0.5    -> cutoff=20000 hcutoff=20     (bypass)
    at 1.0    -> cutoff=20000 hcutoff=8000   (high-pass to superhigh)
    gMute3 pressed -> SILENT                 (mute still mutes)
    gPanic armed   -> gain 0.0               (panic still panics)

tools/check-boot.sh gains a second pass over the seed block (pinning the
`streamSetF :: Stream -> String -> Pattern Double -> IO ()` signature it
calls through), so neither half of this can rot silently. Its own first
attempt at that pass was itself broken — a sed that produced
`_seed = _seed = concat` — and the checker caught it. Same lesson as
77903244: run the guard, don't trust that you wrote it correctly.

prelude.tidal (the stale-boot rescue sheet) carries the matching orDef form
plus a ready-to-eval `_seedNow` list, for the case where a gig laptop boots
silent and the cheapest fix is one paste rather than sweeping 40 controls.
parent 77903244
......@@ -284,19 +284,44 @@ let -- DPV specific parameters
drumFrom name drum = s (name ++| drum)
drumM = drumMachine
drumF = drumFrom
-- Midi boolean buttons
-- midiOn
midiOn ch pat = someCyclesBy ch pat
midiOff ch pat = someCyclesBy (1 - ch) pat
midiNo c1 c2 pat = someCyclesBy (1 - c1 - c2) pat
midiBoth c1 c2 pat = someCyclesBy (c1 + c2 - 1) pat
-- =============================================
-- orDef — THE UNTOUCHED-CONTROL FIX (#55, 2026-07-27)
-- A bare "^NN" does NOT default to 0 when the CC has never been
-- received: it yields NO EVENTS AT ALL. And `#`/`|>` cannot emit
-- without a value on its right, so ANY g*-wrapped stream is
-- COMPLETELY SILENT until that physical control is moved once.
-- Proven by queryArc on an empty controls map, not by ear.
-- This is why "move all the controls" was always step one of a gig:
-- the habit was masking a mute-bomb, and a Tidal restart re-armed it.
-- orDef substitutes a default when the control pattern is empty. It
-- must live HERE, at the helper level, rather than at each call site,
-- because tracks combine controls arithmetically —
-- midiOn ("^34" - "^18") (perfect.tidal:63)
-- and `touched - untouched` is itself empty, so a per-CC default
-- could not catch it. (Caveat, accepted: with one of the two
-- touched and the other not, the combination still falls back to the
-- default rather than to a partial value. Safe — audible, branch off
-- — but not clairvoyant.)
orDef :: a -> Pattern a -> Pattern a
orDef d p = Pattern $ \st -> case query p st of
[] -> query (pure d) st
es -> es
-- =============================================
-- Midi boolean buttons — default OFF (untouched button == not pressed)
midiOn ch pat = someCyclesBy (orDef 0 ch) pat
midiOff ch pat = someCyclesBy (orDef 1 (1 - ch)) pat
midiNo c1 c2 pat = someCyclesBy (orDef 1 (1 - c1 - c2)) pat
midiBoth c1 c2 pat = someCyclesBy (orDef 0 (c1 + c2 - 1)) pat
-- Parameterized DJF
_LPF lMin lMax ch = (# lpf (min lMax ((range lMin ((lMax*2) - lMin) ch))))
_HPF hMin hMax ch = (# hpf (max hMin (min hMax (range (hMin - hMax) (hMax - hMin) ch))))
midiDJF ch lMin lMax hMin hMax = (_LPF lMin lMax ch) . (_HPF hMin hMax ch)
-- FIXME: Seems to cut some lows when lMin != 0
-- Midi gain control (faders)
midiGGlobal = "^77" * 1.3
-- Untouched fader 77 defaults to 0.769 so the product is unity (1.0) —
-- NOT 0, which would silence every midiG-using stream on a fresh boot.
-- (#53 wants this fixed pre-set rather than live-MIDI at all.)
midiGGlobal = orDef 0.769 "^77" * 1.3
_gainG ch = (gain (midiGGlobal * ch)) -- gain Global
midiG' ch l h = _gainG (range l h ch) -- midiGain control
midiGdef = midiG' 1 0 1 -- midiGain default
......@@ -326,8 +351,12 @@ let -- DPV specific parameters
-- infixl 9 — the nested form is a hard PARSE ERROR that takes the whole
-- `let` block (i.e. every g* helper) down with it. Typechecked against
-- tidal-1.9.5 with `ghc -fno-code` before landing.
gDJF ch = (# lpf (range 180 20000 (fmap (\v -> 1 - 2 * max 0 (0.5 - v)) ch)))
. (# hpf (range 20 8000 (fmap (\v -> 2 * max 0 (v - 0.5)) ch)))
-- orDef 0.5 (#55): an UNTOUCHED knob sends nothing at all, which without
-- a default makes the whole stream silent — see the orDef note above.
-- 0.5 == the centre detent == full bypass, so untouched really is "no
-- effect, ever", which is what #21 was reaching for all along.
gDJF ch = (# lpf (range 180 20000 (fmap (\v -> 1 - 2 * max 0 (0.5 - v)) (orDef 0.5 ch))))
. (# hpf (range 20 8000 (fmap (\v -> 2 * max 0 (v - 0.5)) (orDef 0.5 ch))))
gF1 = gDJF "^49" -- knob C1 = DJ filter (centre=open, left=lpf/subbass, right=hpf/superhigh)
gF2 = gDJF "^50" -- knob C2 = DJ filter, same shape
gF3 = gDJF "^51" -- knob C3 = DJ filter, same shape
......@@ -336,7 +365,9 @@ let -- DPV specific parameters
-- "^93" toggle, echoed on the LCXL LEDs (lit = active). Tidal side is just
-- a plain 0/1 gain gate. Not applied to any stream by default — wrap it on
-- whichever track you're actively composing: d1 $ gPanic $ gF1 $ …
gPanic = (# gain (range 1 0 "^93")) -- ^93=0 normal, ^93=1 silent
gPanic = (# gain (range 1 0 (orDef 0 "^93"))) -- ^93=0 normal, =1 silent
-- orDef 0 (#55): untouched
-- must be NORMAL, not silent
-- Global masks (defaults)
gMask = (midiOn "^41" (mask "t!7 f"))
gMute1 = (midiOn "^73" (mask "f*16"))
......@@ -611,5 +642,57 @@ jroot input = n (parseBP_E $ extractRoots input)
splitChordSymbol [] = ("C", "")
:}
-- =====================================================================
-- SEED THE CONTROL MAP (#55, 2026-07-27) — the corpus-wide half of the
-- untouched-control fix.
--
-- A bare "^NN" whose CC has never arrived yields NO EVENTS, and `#` cannot
-- emit without a right-hand value, so the stream goes SILENT (not "gets a
-- default of 0" — silent). `orDef` guards the boot-level helpers, but it
-- cannot reach the 1860 places across 343 track files that apply controls
-- directly, e.g.
-- # crushbus 41 (range 16 3.5 "^53")
-- # legato (range 0.05 2 "^52")
-- Editing those is #46-scale work. Seeding is the one move that fixes the
-- whole corpus at once, touching no track file: give every LCXL control a
-- value at boot so nothing is ever in the "never received" state. A real
-- CC overwrites its seed the moment the knob moves.
--
-- Defaults follow the authoring convention `range <neutral> <extreme>`,
-- so 0 == neutral for effect knobs:
-- A/B knobs (13-20, 29-36) -> 0 effect amount off
-- C1-C3 (49,50,51) -> 0.5 DJF CENTRE = true bypass
-- C4-C8 (52-56) -> 0 effect amount off
-- fader 77 -> 0.769 (*1.3 == unity master gain)
-- faders 78-84 -> 1.0 channel gains OPEN, i.e. audible
-- E/F buttons -> 0 not pressed
-- 93 (panic toggle) -> 0 not armed
--
-- Known outlier, left alone deliberately: piment_bresilien's
-- `legato (range 0.05 2 "^52")` seeds to legato 0.05 (clicky 5%-length
-- notes) because its `range` is written low-to-high with a non-neutral
-- low end. That is an authoring bug in the track, not in the seed policy
-- — fix it there (swap to `range 1 2`), don't special-case it here.
:{
let _seed = concat
[ [ (show n, 0) | n <- [13..20] ++ [29..36] ] -- A/B knobs
, [ (show n, 0.5) | n <- [49,50,51] ] -- C1-C3 = DJF centre
, [ (show n, 0) | n <- [52..56] ] -- C4-C8 effects
, [ ("77", 0.769) ] -- master -> unity
, [ (show n, 1) | n <- [78..84] ] -- channel gains open
, [ (show n, 0) | n <- [41..44] ++ [57..60] ] -- E buttons
, [ (show n, 0) | n <- [73..76] ++ [89..92] ] -- F buttons
, [ ("93", 0) ] -- panic toggle
-- Non-LCXL stragglers found by diffing the seed set against every
-- "^NN" in live/ (49 seeded vs 54 used). ^1 is an external keyboard
-- modwheel (hello_world, jazz_tech_piano); ^21 is a one-off in
-- collab/igor. ^45 needs no seed — it goes through midiOn, so orDef
-- already covers it. ^6 and ^61 are commented out at both call sites.
, [ ("1", 0), ("21", 0) ]
] :: [(String, Double)]
:}
mapM_ (\(k, v) -> setF k (pure v)) _seed
putStrLn ("[BootTidal] seeded " ++ show (length _seed) ++ " LCXL controls (#55) — untouched knobs no longer silence their stream")
:set prompt "tidal> "
-- :set prompt-cont ""
......@@ -26,14 +26,31 @@
-- same "untouched = no effect, ever" guarantee from #21, just recentred).
-- =====================================================================
:{
let gDJF ch = (# lpf (range 180 20000 (fmap (\v -> 1 - 2 * max 0 (0.5 - v)) ch)))
. (# hpf (range 20 8000 (fmap (\v -> 2 * max 0 (v - 0.5)) ch)))
let orDef d p = Pattern $ \st -> case query p st of { [] -> query (pure d) st; es -> es }
gDJF ch = (# lpf (range 180 20000 (fmap (\v -> 1 - 2 * max 0 (0.5 - v)) (orDef 0.5 ch))))
. (# hpf (range 20 8000 (fmap (\v -> 2 * max 0 (v - 0.5)) (orDef 0.5 ch))))
gF1 = gDJF "^49" -- knob C1 = DJ filter (centre=open, left=lpf/subbass, right=hpf/superhigh)
gF2 = gDJF "^50" -- knob C2 = DJ filter, same shape
gF3 = gDJF "^51" -- knob C3 = DJ filter, same shape
:}
-- try: d1 $ gF1 $ sound "bd*4" then sweep knob 49 both ways — bypass at centre.
-- (Old broken form was: gF1 = (# djfbus 1 (range 0.05 0.95 "^49")) — do not use.)
--
-- #55 — INSTANT RESCUE if a fresh boot is SILENT on every gF/midiOn stream.
-- An untouched "^NN" yields NO EVENTS (not 0), and `#` can't emit without a
-- right-hand value, so the stream vanishes. Two ways out, cheapest first:
-- 1. Move every knob/button once. (This is why "move all the controls" was
-- always step one — the habit was masking this bug.)
-- 2. Or eval this one-liner to seed the whole surface at once:
:{
let _seedNow = concat [ [ (show n, 0) | n <- [13..20] ++ [29..36] ++ [52..56]
++ [41..44] ++ [57..60]
++ [73..76] ++ [89..92] ++ [93] ]
, [ (show n, 0.5) | n <- [49,50,51] ]
, [ ("77", 0.769) ]
, [ (show n, 1) | n <- [78..84] ] ] :: [(String, Double)]
:}
-- then: mapM_ (\(k,v) -> setF k (pure v)) _seedNow
-- =====================================================================
......
......@@ -68,3 +68,36 @@ if [ "$RC" -ne 0 ]; then
exit 1
fi
echo "check-boot: OK — helper block typechecks against tidal-1.9.5."
# --- Pass 2: the #55 control-seed block ---------------------------------
# Seeding runs at boot and silences nothing if it fails, but a type error in
# it aborts the rest of the boot file. Check the seed list's shape and the
# streamSetF signature it depends on.
SEED_START="$(grep -n '^let _seed = concat' "$BOOT" | cut -d: -f1)"
if [ -n "$SEED_START" ]; then
SEED_END="$(awk -v s="$SEED_START" 'NR>=s && /\] :: \[\(String, Double\)\]/ {print NR; exit}' "$BOOT")"
{
echo 'module SeedCheck where'
echo 'import Sound.Tidal.Context'
# Pin the exact signature the seed block calls through `setF`.
echo 'sigF :: Stream -> String -> Pattern Double -> IO ()'
echo 'sigF = streamSetF'
sed -n "${SEED_START},${SEED_END}p" "$BOOT" | sed '1s/^let //'
echo 'useSeed :: Stream -> IO ()'
echo 'useSeed t = mapM_ (\(k, v) -> streamSetF t k (pure v)) _seed'
} > "$WORK/SeedCheck.hs"
set +e
SOUT="$(ghc -fno-code -package tidal "$WORK/SeedCheck.hs" 2>&1)"
SRC=$?
set -e
if [ "$SRC" -ne 0 ]; then
echo "" >&2
printf '%s\n' "$SOUT" | sed "s#$WORK/SeedCheck.hs#<seed-block>#" >&2
echo "check-boot: FAIL — the #55 control-seed block does not typecheck." >&2
exit 1
fi
echo "check-boot: OK — #55 control-seed block typechecks."
else
echo "check-boot: WARN — no #55 control-seed block found (untouched knobs" >&2
echo " will silence their streams; see BootTidal.hs history)." >&2
fi
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment