Skip to content

  • Projects
  • Groups
  • Snippets
  • Help
  • This project
    • Loading...
  • Sign in / Register
T
Tidal
  • Overview
    • Overview
    • Details
    • Activity
    • Cycle Analytics
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Charts
  • Issues 0
    • Issues 0
    • List
    • Board
    • Labels
    • Milestones
  • Merge Requests 0
    • Merge Requests 0
  • CI / CD
    • CI / CD
    • Pipelines
    • Jobs
    • Schedules
    • Charts
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Members
    • Members
  • Collapse sidebar
  • Activity
  • Graph
  • Charts
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
  • PLN
  • Tidal
  • Repository

Switch branch/tag
  • Tidal
  • tools
  • button_roles.py
Find file
BlameHistoryPermalink
  • PLN (Algolia)'s avatar
    fix(surface): a button's ROLE decides its row — the remap had been assigning cells in reading order · 5089eec0
    PLN found this by ear, twice in ten minutes, while recording OPAL takes:
    
      "techno drum mask is inverted on d8, should ALWAYS be the mask midiOff on ^60
       and the ply or other multiplier/nassim button effect on the push-release 92"
      "drums in revolution seem way too fast ... yea def an inversion"
    
    ## The bug I shipped
    
    Phase 2 of the surface remap (5910aacc) column-aligned every button reference, and
    assigned each orbit's two cells in FIRST-APPEARANCE ORDER. That is role-blind, and
    tracks conventionally write the gesture line above the gate line:
    
        $ midiOn  "^92" (ply "1 <2!3 4>")      -- momentary flourish
        $ midiOff "^60" (mask "t(4,8,1)")      -- latched gate
    
    so the first control encountered — the gesture — took the LATCHING cell, and the
    gate took the momentary one. Measured on the diff itself rather than on corpus
    history: of the button lines that commit moved, **32 were inverted**, 11 happened
    to be repaired, 10 were already wrong, 2 were right both times. 13 files, nearly
    all of them in the OPAL setlist.
    
    "Way too fast" is the audible signature: a `ply` on a latching button stays
    multiplied after one press. The mirror image is a `mask` that only gates while a
    finger holds it down. Neither errors. Neither is silent. Only the ear catches it —
    which is precisely the class of bug that needs a machine check.
    
    ## The convention, now written down once
    
        row E (41-44, 57-60), latching   -> GATES    (mask, struct)
        row F (73-76, 89-92), momentary  -> GESTURES (ply, fast, stut, chop, slice...)
    
    A real limit of the grid, found while encoding it: **d1-d3 own only ONE button.**
    Their row-F cells (73,74,75) are the per-family mutes gMute1/2/3, so a gesture on
    ^41/^42/^43 is not an inversion — it is the only cell that exists. Every tool here
    skips those columns rather than inventing a slot.
    
    ## tools/button_roles.py — one judgement, three consumers
    
    migrate-columns.py ASSIGNS cells, fix-button-roles.py REPAIRS them, pvlint PV010
    REPORTS them. Three copies of a musical rule is three chances to drift (the #97
    lesson, applied before it could bite). Two parsing rules were earned in the space
    of one afternoon, and both were wrong in my first cut:
    
    1. **A gate beats a gesture at the SAME level.** `mask "t(8,16,1)" . chop 16` is a
       latched break-gate that happens to chop while open. Treating it as ambiguous is
       what hid revolution's d8 pair from the first scan — the very pair PLN heard.
    
    2. **But only at the same level.** `superimpose (struct "t . t(3,8)" . arpeggiate
       . (|+ note 12))` is a HELD FLOURISH; the `struct` builds the added layer's
       rhythm two levels down and says nothing about the button. A plain substring
       search called phunk's d6 a gate and moved it to the latching row — the same
       inversion, one level deeper. So the role belongs to the top-level chain, with
       nested arguments stripped and string literals skipped (`mask "t(4,8,1)"` would
       otherwise look like it opened two parens and swallowed the rest).
    
    3. **The body is not the rest of the line.** `midiOn "^91" ( -- SLICE!` puts the
       function on the FOLLOWING lines. A same-line regex returns "unknown", and an
       unknown control gets a positional fallback — re-creating the inversion. The
       classifier now follows the parentheses, capped at 8 lines.
    
    ## Validation
    
    - 214 tests pass (24 new: 14 for the classifier, 10 for PV010/PV011).
    - The classifier is CONVERGENT: applying the repair twice yields 0 further
      rewrites. That is what caught mistakes 2 and 3 — each fix made the tool disagree
      with its own previous pass, and the disagreement was the bug report.
    - pvlint on the setlist: 0 errors, and PV010's 12 unfixable cases are reported as
      `info`, not warnings, because an orbit with two gestures genuinely cannot put
      both on the momentary row. A lint that nags about the impossible gets ignored,
      and this one has to stay trustworthy enough to gate a gig.
    
    ## PV011 — one FX bus slot shared by two orbits
    
    PLN's ask, right after PV004 surprised him with a shared cut group: "putain a
    unexpected cut shared, well done! like shared busses they screw things. can we
    lint and flag these? diff bus shared button is ok, but to be detected too". A
    `<fx>bus N` is a SLOT in SuperDirt, so two orbits naming the same number share one
    instance and the last event to land sets the amount for both. It immediately found
    take_5_drops putting d4's bass and d7's choir on `crushbus 41` — the standing
    suspect for "d7 feels way more attacky than before". Different effects on the same
    number are fine and are not flagged: the namespace is per effect, exactly as he
    said.
    PLN (Algolia) authored Jul 29, 2026
    5089eec0
button_roles.py 6.59 KB
EditWeb IDE
×

Replace button_roles.py

Attach a file by drag & drop or click to upload


Cancel
A new branch will be created in your fork and a new merge request will be started.