From 7dbb4ec6f8264e016f5066793652cad139bcc87b Mon Sep 17 00:00:00 2001 From: Thiemo Tiziani Date: Sun, 18 Oct 2026 23:29:42 +0200 Subject: [PATCH] Patcher: ledsaver and credit mods, beat jump needs the hot cue engine Co-Authored-By: Claude Opus 5.5 --- README.md | 51 ++++++++++++++++++++++++++++++++++++-------- cdj900nxs_patcher.py | 17 ++++++++++----- test_patcher.py | 14 ++++++++++++ 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2be90fd..781b73d 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,17 @@ From the emulator checkout `../CDJ900NXSVirtual` (`--repo` for another place): | `code` | arnoldasr: BPM with two decimals, two-line phase meter, RGB waveform | | `utility` | arnoldasr: UTILITY > PHASE METER switch (needs `code`) | | `overview` | arnoldasr: overview waveform colour gradient | -| `beatjump` | TRACK SEARCH jumps 1/2/4/8 beats, UTILITY > BEAT JUMP (known faults on a player, see Risk) | +| `beatjump` | TRACK SEARCH jumps 1/2/4/8 beats, UTILITY > BEAT JUMP (needs `hotcuekeys`), see below | | `cuecolours` | memory cues in their rekordbox colours, hot cues shown | -| `hotcuekeys` | hot cues A-E on the BEAT DIVIDE keys (needs `cuecolours`), see below | +| `hotcuekeys` | hot cues A-C on the BEAT DIVIDE keys (needs `cuecolours`), see below | +| `ledsaver` | UTILITY > LED ANIMATION: while the screen saver dims the display, the button LEDs and the jog's rings play a loop | +| `credit` | the boot screen says "Pionaarr Mods by Givou" where it said "Ver 1.31" (ticked at the start, can be unticked) | `hotcuekeys`: UTILITY > HOT CUE KEYS ON; then QUANTIZE pressed briefly switches the hot key mode (HOT KEYS box beside QUANTIZE), held it is -QUANTIZE. In the mode the BEAT DIVIDE keys 3/4, 1/3, 1/2, 1/4, 1/8 are hot -cues A-E: +QUANTIZE. In the mode the BEAT DIVIDE keys 3/4, 1/3, 1/2 are hot cues A-C +(1/4 and 1/8 do nothing and stay dark: the player has three hot cue buffers, +one for each): - press: jump to the hot cue and play (from pause too); - hold 0.5 s or longer, then let go: back to the hot cue, standing (gate); @@ -72,8 +75,36 @@ cues A-E: - DELETE held, a key pressed: its hot cue deleted (DELETE alone is DELETE); - LEDs: off without a hot cue, dim with one, two bright blinks when pressed. -Hot cues set or deleted live until the next track load; nothing is written -to the USB stick. As with CALL, the CUE point moves to the hot cue. +Hot cues set or deleted are written to the USB stick (the track's rekordbox +analysis files, as MEMORY and DELETE keep memory cues), so they are there +after the next load too. As with MEMORY, the player takes this only once the +track has settled (a few seconds after loading). Hot cues A-C start at once: once a track is loaded and the +deck stands at its CUE point for a second (in hot key mode), they are loaded +into the player's three hot cue buffers, silently, and the deck goes back to +its CUE point. PLAY or CUE pressed meanwhile wait for it (PLAY is played +after). A hot cue set later loads into +its own buffer on its first press (about a second). A hot cue played from a buffer +leaves the CUE point where it is; one recalled without a buffer moves it, as +CALL does. + +`ledsaver`: UTILITY > LED ANIMATION, ON from the first start (set it to OFF +to switch it off; the player keeps the choice). When the +player's own screen saver dims the display (5 minutes without a track, or +100 minutes paused), the BEAT DIVIDE keys fade in a wave down and back up, +SYNC / MASTER and MASTER TEMPO / DISC EJECT fade in and out, CUE, PLAY, REV, +SLIP, the SLIP LOOP bar and RELOOP/EXIT light one after another, and the +jog's three rings light in turn from the inside out and back in. Any key ends +it, as it ends the screen saver. + +`beatjump`: UTILITY > BEAT JUMP (where SCREEN SAVER was) sets 1, 2, 4 or 8 +beats (OFF: TRACK SEARCH as before); TRACK SEARCH back and forward then jump +that many beats along the track's beat grid, keeping the position within the +beat. While playing, the jump is loaded into a buffer of the player's own +that only beat jumps use (A-C keep theirs): there is a short silence while it loads, but the +deck comes back in phase, as if it had played on through it (the load time is +measured on every jump and allowed for), and the CUE point stays. Paused, it +is a CALL (the CUE point moves with it). A press while a jump or hot cue is +still loading is ignored. arnoldasr's mods need his repository checked out in `../CDJ900NXSVirtual/mods/cdj900nxs/upstream` (it has no licence, so it is @@ -83,9 +114,11 @@ not copied here); without it they are greyed out. Nothing here talks to a player; you copy the file to a FAT32 USB stick and run the update yourself. Our mods were developed with Claude Code in an emulator. -On a player: `beatjump` jumped off the grid and, after a few jumps, reported a -corrupted file and needed a restart; it is being reworked. `cuecolours` and -earlier versions of `hotcuekeys` ran on a player. Recovery from an +An earlier `beatjump` jumped off the grid on a player and, after a few quick +jumps, reported a corrupted file and needed a restart; it has been rebuilt on +the hot cue engine (no overlapping loads), which has not run on a player yet, +nor has the hot cue engine. A jump while a loop plays ends the loop. `cuecolours` and earlier versions of `hotcuekeys` +ran on a player. Recovery from an application that crashes at start-up has not been tested by anyone. Keep the `--stock` file at hand. diff --git a/cdj900nxs_patcher.py b/cdj900nxs_patcher.py index 20b14ab..d97429a 100644 --- a/cdj900nxs_patcher.py +++ b/cdj900nxs_patcher.py @@ -40,10 +40,11 @@ import upd # noqa: E402 DEFAULT_REPO = os.path.join(os.path.dirname(HERE), 'CDJ900NXSVirtual') UPSTREAM_MODS = ('code', 'utility', 'overview') NEEDS = {'utility': 'code', # the PHASE METER switch chooses the code mod's meter - 'hotcuekeys': 'cuecolours'} # the hot cues the keys play are read by cuecolours + 'hotcuekeys': 'cuecolours', # the hot cues the keys play are read by cuecolours + 'beatjump': 'hotcuekeys'} # hotcuekeys' engine makes the jumps LABEL = '1.32' # not ticked at the start of the list, and why -KNOWN_FAULTS = {'beatjump': 'known faults on a player (jumps off the grid, "file corrupted" after a few jumps)'} +KNOWN_FAULTS = {} def load_repo(repo): @@ -114,13 +115,19 @@ def _toggle(chosen, name, mods): """Tick or untick one mod: ticking one ticks what it needs, unticking one unticks what needs it (so every mod can be unticked).""" if name in chosen: - chosen.discard(name) - chosen.difference_update(n for n, need in NEEDS.items() if need == name) + drop = {name} + while True: + more = {n for n, need in NEEDS.items() if need in drop} - drop + if not more: + break + drop |= more + chosen.difference_update(drop) else: chosen.add(name) need = NEEDS.get(name) - if need and any(m[0] == need and m[2] for m in mods): + while need and any(m[0] == need and m[2] for m in mods): chosen.add(need) + need = NEEDS.get(need) def choose(mods): diff --git a/test_patcher.py b/test_patcher.py index 59e2338..ecdb67d 100644 --- a/test_patcher.py +++ b/test_patcher.py @@ -109,6 +109,10 @@ def test_every_mod_can_be_unticked(): assert {'hotcuekeys', 'cuecolours'} <= chosen # and ticking brings what it needs cli._toggle(chosen, 'overview', MODS) assert 'overview' not in chosen + cli._toggle(chosen, 'cuecolours', MODS) + assert not {'cuecolours', 'hotcuekeys', 'beatjump'} & chosen # and what needs that + cli._toggle(chosen, 'beatjump', MODS) + assert {'beatjump', 'hotcuekeys', 'cuecolours'} <= chosen def test_disclaimer_must_be_accepted(monkeypatch): @@ -125,3 +129,13 @@ def test_disclaimer_without_a_terminal(monkeypatch): monkeypatch.setattr(cli.sys.stdin, 'isatty', lambda: False, raising=False) with pytest.raises(SystemExit, match='--accept'): cli.confirm(False) + + +def test_credit_can_be_unticked(): + import types + mods = cli.available(types.SimpleNamespace(MODS={"code": "c", "credit": "x"}), cli.DEFAULT_REPO) + assert [m[0] for m in mods] == ['code', 'credit'] + chosen = {m[0] for m in mods if m[0] not in cli.KNOWN_FAULTS} + assert 'credit' in chosen # ticked at the start + cli._toggle(chosen, 'credit', mods) + assert chosen == {'code'}